IOP Color EQ : Add interactive editing mode for color adjustments in image - #21397
Conversation
|
Hmm, just did a first test. This does work only when used with "default scheduling profile" (this is very likely just one trigger for the issue), otherwise code after is never reached. So how is
|
|
Hmm, I think you're right. 😉 |
|
Hello,
Bottom line: non-default scheduling profiles are now safe, image zoom on scroll is never triggered, and the graph keeps its node-weighted behavior. Thanks again for taking the time, this is a much more solid implementation because of your feedback. |
|
I did some fresh testing, i think it works now as you intended! The way you change the nodes seems good to me too. Don't take me wrong but personally i don't think the strategy "if mouse is in main canvas use scrolling for CE changes" is a good one (knowing hat TE also does it like this ...) . One simple reason is, i miss zooming in/out using the scroll wheel a lot. I would prefer it to be bound to the picker. That would also mean,
|
You can press 'a' while zooming/panning to pass the events to the canvas. But you certainly know that. |
|
I dont have a strong opinion. I thought, if the picker is active, that could be checked and your modify-nodes from hue could step in. No? |
|
There is some irritating effect.
I think we need some marker as it would be very surprising if you hover over green but change reds. Another argument to go via the picker. The TE has a much easier job here going for blurred luminance. |
|
Hello, A new button has been added to allow direct adjustments to the image when it is activated. If this doesn’t work for you, I can always revert the changes with Friendly regards, |
92ce9de to
80dc653
Compare
|
Hello, You're right, it isn't very stable. I've reverted to the previous version, which is consistent with TE. 2026-06-27.13-34-52.mp4A little fresh air from the Luberon, 🍷 |
|
Please shar your test-image ... |
|
@Christian-Bouhon after commenting in #20626 about dt caching i thought again about your code here (and why i don't like the way of caching at all). It simply should not be necessary in the vast majority of use cases as a) all you do here only happens if the CE moduly is the active module and b) we already make sure the input data of the active module is available in the pipe cache for CPU and OpenCL code. So we just need some extra pixelpipe stuff to get access to preview pipe input (and possibly output) data of the active module. that might help in a lot of situations, here for your new feature but also for in-module histograms or stats... I would implement that. |
|
Hi, Best regards, |
|
Hello, I saved this as a draft because I’ve received feedback from users of my experimental fork saying that the module is no longer working as it did before. There are two possible causes, in order:
I’ll look into it and get back to you. Greetings from the Luberon, |
|
How does the preview pipe write the cache if processed on GPU? |
Hello, You're right, it wasn't writing it, that was the bug. With the latest commit, the module is working. |
|
Just being curious; is there a good reason to use data after processing the module? Changing the hue at a certain location results in slightly different access to nodes |
|
hello, |
|
Indeed, from reading the diffs i spotted it wrongly. |
5f24759 to
dd43408
Compare
dd43408 to
a8d29ad
Compare
c82c698 to
bd389fc
Compare
|
Hello,
In addition, the cursor now shows the module input/output colors: the Greetings from the Lubéron, 🌿 |
…w-data service Adds an on-canvas interactive editing mode driven by the hue under the mouse cursor: hovering the image shows a color indicator and lets the scroll wheel apply a Gaussian-weighted adjustment (sigma=35 degrees) to the saturation/hue/brightness nodes of the active channel, Alt+scroll switches channel tabs (on both the image and the graph), and the graph's own scroll handling mirrors the same behavior for the node under the cursor there. The per-pixel hue buffer needed for this is served by a new shared service, dt_preview_data_t (src/develop/preview_data.c/.h): resizing, filling and hashing the preview-pipe buffer under a single GUI lock so readers never observe a resized-but-unfilled buffer, plus freshness checks against the pipe's cumulative hash. gui_focus()/mouse_moved() use it to request a debounced preview reprocess so the indicator works right after opening the module instead of only after an unrelated trigger. The on-canvas cursor itself (crosshair, wedge, circles, text readout) is factored into a shared dt_draw_correction_cursor() helper in src/gui/draw.h and reused by the tone equalizer, so both modules render the same cursor design and future modules can adopt it too.
- don't show a busy/wait cursor while the preview pipe recomputes during hover (toneequal: drop the busy branch in switch_cursors, gate the exposure re-read instead; colorequal: same in _switch_cursors) - draw a white vertical line on the graph at the hue under the mouse cursor, mirroring the tone equalizer's exposure cursor line - open the wedge cursor up to +-90 degrees (clamped at +-1) instead of clamping it to 45 degrees; toneequal pre-scales the correction by 0.5 so the wedge reaches its full span at +-2 EV - store the module input HSB (hue, saturation, brightness) instead of just the hue in the shared preview buffer (3 components per pixel) and show the module input/output colors in the cursor's two circles This brings the branch in line with the review comment on PR darktable-org#21397 and adds the in/out colors feature.
Like the color equalizer's cursor, sample the pixel under the cursor from the preview pipe backbuf and draw the frame lines (wedge, crosshair, outlines) white over dark content and black over bright content. This replaces _match_color_to_background(), which derived grey shades from the estimated output exposure instead of the actual background, so the lines could end up mid-grey. The circles keep their before/after luminance grey shades.
TurboGit
left a comment
There was a problem hiding this comment.
Works for me, thanks!
A nice addition and the generic preview_data could be used in some other modules at some point. Nice!
|
Last point, this needs a release note entry. TIA. |
_switch_cursors() and switch_cursors() restored the default cursor with
a raw gdk_window_set_cursor() on the toplevel, while hiding it through
dt_control_change_cursor("none"). Going through the helper for both
halves brings back three things the raw call skips:
- The lock set by dt_control_forbid_change_cursor(). Shortcut-mapping
mode and help mode (src/libs/tools/global_toolbox.c) hold that lock
for as long as the mode is active, apply their own cursor to the same
toplevel, and still forward motion events to the centre canvas -- so
with the tone equalizer focused, moving the pointer over the canvas
replaced their cursor with the plain arrow. dt_gui_cursor_set_busy()
takes the same lock.
- The -d input tracing in dt_control_cursor_debug(), which is called
from the helpers only.
- The GTK3/GTK4 compatibility boundary. gdk_window_set_cursor() has no
GTK4 equivalent; these were its last uses outside src/gui/gtk.c.
Where the lock is not held the two forms are equivalent: on GTK3
dt_control_change_cursor() ends up calling gdk_cursor_new_from_name()
and gdk_window_set_cursor() on the same toplevel window.
For toneequal.c this restores what was there before 8a26f35
(darktable-org#21397); colorequal.c copied the pattern from it in the same commit.
The behaviour that commit intended -- no wait cursor while hovering,
whatever the pipe is doing -- is unchanged.
Built and verified at runtime on X11/GTK3.
Suggested-by: a darktable contributor
@TurboGit Greetings from the Luberon, |
…image (darktable-org#21397) * colorequal: interactive hue-under-cursor editing with a shared preview-data service Adds an on-canvas interactive editing mode driven by the hue under the mouse cursor: hovering the image shows a color indicator and lets the scroll wheel apply a Gaussian-weighted adjustment (sigma=35 degrees) to the saturation/hue/brightness nodes of the active channel, Alt+scroll switches channel tabs (on both the image and the graph), and the graph's own scroll handling mirrors the same behavior for the node under the cursor there. The per-pixel hue buffer needed for this is served by a new shared service, dt_preview_data_t (src/develop/preview_data.c/.h): resizing, filling and hashing the preview-pipe buffer under a single GUI lock so readers never observe a resized-but-unfilled buffer, plus freshness checks against the pipe's cumulative hash. gui_focus()/mouse_moved() use it to request a debounced preview reprocess so the indicator works right after opening the module instead of only after an unrelated trigger. The on-canvas cursor itself (crosshair, wedge, circles, text readout) is factored into a shared dt_draw_correction_cursor() helper in src/gui/draw.h and reused by the tone equalizer, so both modules render the same cursor design and future modules can adopt it too. * colorequal: address review feedback and show in/out colors under cursor - don't show a busy/wait cursor while the preview pipe recomputes during hover (toneequal: drop the busy branch in switch_cursors, gate the exposure re-read instead; colorequal: same in _switch_cursors) - draw a white vertical line on the graph at the hue under the mouse cursor, mirroring the tone equalizer's exposure cursor line - open the wedge cursor up to +-90 degrees (clamped at +-1) instead of clamping it to 45 degrees; toneequal pre-scales the correction by 0.5 so the wedge reaches its full span at +-2 EV - store the module input HSB (hue, saturation, brightness) instead of just the hue in the shared preview buffer (3 components per pixel) and show the module input/output colors in the cursor's two circles This brings the branch in line with the review comment on PR darktable-org#21397 and adds the in/out colors feature. * Restoring the code after a rebase * toneequal: derive cursor frame color from the sampled background Like the color equalizer's cursor, sample the pixel under the cursor from the preview pipe backbuf and draw the frame lines (wedge, crosshair, outlines) white over dark content and black over bright content. This replaces _match_color_to_background(), which derived grey shades from the estimated output exposure instead of the actual background, so the lines could end up mid-grey. The circles keep their before/after luminance grey shades.
…image (darktable-org#21397) * colorequal: interactive hue-under-cursor editing with a shared preview-data service Adds an on-canvas interactive editing mode driven by the hue under the mouse cursor: hovering the image shows a color indicator and lets the scroll wheel apply a Gaussian-weighted adjustment (sigma=35 degrees) to the saturation/hue/brightness nodes of the active channel, Alt+scroll switches channel tabs (on both the image and the graph), and the graph's own scroll handling mirrors the same behavior for the node under the cursor there. The per-pixel hue buffer needed for this is served by a new shared service, dt_preview_data_t (src/develop/preview_data.c/.h): resizing, filling and hashing the preview-pipe buffer under a single GUI lock so readers never observe a resized-but-unfilled buffer, plus freshness checks against the pipe's cumulative hash. gui_focus()/mouse_moved() use it to request a debounced preview reprocess so the indicator works right after opening the module instead of only after an unrelated trigger. The on-canvas cursor itself (crosshair, wedge, circles, text readout) is factored into a shared dt_draw_correction_cursor() helper in src/gui/draw.h and reused by the tone equalizer, so both modules render the same cursor design and future modules can adopt it too. * colorequal: address review feedback and show in/out colors under cursor - don't show a busy/wait cursor while the preview pipe recomputes during hover (toneequal: drop the busy branch in switch_cursors, gate the exposure re-read instead; colorequal: same in _switch_cursors) - draw a white vertical line on the graph at the hue under the mouse cursor, mirroring the tone equalizer's exposure cursor line - open the wedge cursor up to +-90 degrees (clamped at +-1) instead of clamping it to 45 degrees; toneequal pre-scales the correction by 0.5 so the wedge reaches its full span at +-2 EV - store the module input HSB (hue, saturation, brightness) instead of just the hue in the shared preview buffer (3 components per pixel) and show the module input/output colors in the cursor's two circles This brings the branch in line with the review comment on PR darktable-org#21397 and adds the in/out colors feature. * Restoring the code after a rebase * toneequal: derive cursor frame color from the sampled background Like the color equalizer's cursor, sample the pixel under the cursor from the preview pipe backbuf and draw the frame lines (wedge, crosshair, outlines) white over dark content and black over bright content. This replaces _match_color_to_background(), which derived grey shades from the estimated output exposure instead of the actual background, so the lines could end up mid-grey. The circles keep their before/after luminance grey shades.
_switch_cursors() and switch_cursors() restored the default cursor with
a raw gdk_window_set_cursor() on the toplevel, while hiding it through
dt_control_change_cursor("none"). Going through the helper for both
halves brings back three things the raw call skips:
- The lock set by dt_control_forbid_change_cursor(). Shortcut-mapping
mode and help mode (src/libs/tools/global_toolbox.c) hold that lock
for as long as the mode is active, apply their own cursor to the same
toplevel, and still forward motion events to the centre canvas -- so
with the tone equalizer focused, moving the pointer over the canvas
replaced their cursor with the plain arrow. dt_gui_cursor_set_busy()
takes the same lock.
- The -d input tracing in dt_control_cursor_debug(), which is called
from the helpers only.
- The GTK3/GTK4 compatibility boundary. gdk_window_set_cursor() has no
GTK4 equivalent; these were its last uses outside src/gui/gtk.c.
Where the lock is not held the two forms are equivalent: on GTK3
dt_control_change_cursor() ends up calling gdk_cursor_new_from_name()
and gdk_window_set_cursor() on the same toplevel window.
For toneequal.c this restores what was there before 8a26f35
(darktable-org#21397); colorequal.c copied the pattern from it in the same commit.
The behaviour that commit intended -- no wait cursor while hovering,
whatever the pipe is doing -- is unchanged.
Built and verified at runtime on X11/GTK3.
Suggested-by: a darktable contributor
Hello, Module: Color Equalizer Added an interactive editing mode allowing users to correct a color by selecting it directly on the photo with the mouse wheel, just as was already possible with the "tone equalizer" module. User-facing changes
Technical improvements
|
…ith busy_mutex dt_preview_data_is_fresh() (introduced in 8a26f35 / #21397, the shared preview-data service for interactive editing) walks dev->preview_pipe->nodes and hashes the matching piece from the GUI thread while holding only the module's own gui_lock. pipe->nodes is actually protected by pipe->busy_mutex: dt_dev_pixelpipe_cleanup_nodes() frees every piece under that lock whenever a history change rebuilds the pipe topology (DT_DEV_PIPE_REMOVE in dt_dev_pixelpipe_change), and that can run concurrently on the pipe-processing thread. A caller that walks pipe->nodes off the GUI thread without busy_mutex can therefore dereference a piece that gets freed out from under it -- reproduced as a segfault dereferencing a freed piece->module in _dev_pixelpipe_cache_basichash(), from a module's preview-pipe-finished GUI callback calling dt_preview_data_is_fresh(). colorequal.c's own interactive hue-editing mode (also from #21397) calls dt_preview_data_is_fresh() from mouse-hover/scroll handlers and is exposed to the same race in principle, just from a narrower window that makes it harder to trigger in practice. Take pipe->busy_mutex around the walk. It must be a trylock, not a blocking lock: a module's process() runs under busy_mutex for the whole pipe run (dt_dev_pixelpipe_process) and can itself call dt_preview_data_store() (colorequal.c does, twice), which takes this same module's gui_lock -- the opposite order -- so a blocking lock here would AB-BA deadlock against it. A pipe that's currently busy can't have fresh data for us anyway, so treating "busy" as "not fresh" costs nothing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpGKm49b4w1L51YETXyGpU
Remove unused quality setting dropdown borders: fix memory corruption on non-positive coordinate deltas Commit 257e632 widened the loop index variable in set_pixels() and copy_pixels() from int to size_t. In dt_iop_copy_image_with_border(), these functions are called with slice lengths computed from coordinate differences such as fl_right - image_right. Under boundary conditions (e.g. scaled thumbnail generation or tiling), the difference can be negative. With a signed int counter, 0 < npixels evaluated to false and safely skipped the loop. With size_t, signed-to-unsigned conversion caused the loop to iterate 18446744073709551615 times, corrupting heap memory and causing a SIGSEGV. Revert the loop variables back to int, and clamp image_right and image_bottom to roi_out bounds in dt_iop_setup_binfo. Keep a path's feather when the shape is resized Scroll-resizing a path morphs a cached baseline of its nodes and swaps the result in whole, borders included. The feather branch of the same scroll handler edits every node's border in place without dropping that cache, so the next resize restored the borders the baseline was captured with and the feather collapsed. Invalidate the resize state there, as _path_modify_property already does for DT_MASKS_PROPERTY_FEATHER. Reproduced by resizing a path with the wheel, raising the feather with shift+scroll, then resizing again; circles and ellipses are unaffected as they have no baseline to go stale. Fixes darktable-org#22052 RELEASE_NOTES.md: path feather lost on scroll resize Add protection against memory allocation failure remove mention of Intel Macs from README (darktable-org#22114) * remove mention of Intel Macs from README * update README for the nightly builds RELEASE_NOTES.md: clean-up the changed dependencies. borders: disable frame line controls when border geometry prevents it The frame line width is proportional to the minimum border width across all four sides. When border size is 0 or when horizontal/vertical offsets are set to extreme values (0.0 or 1.0), the minimum border width collapses to 0, making the frame line invisible. Disable frame line width, offset, and color controls when geometry prevents the frame line from rendering, and update their tooltips to explain the required adjustment. borders: fix asymmetric frame line centering and rounding precision Frame line boundary coordinates previously subtracted 1 to represent inclusive pixel indices, but were assigned directly to half-open interval endpoints (fl_right, border_right, fl_bot, border_bot). This caused the inner border on the right and bottom sides to be 1 pixel narrower and the outer border 1 pixel wider, noticeably shifting the frame line off-center at smaller border sizes and downscaled preview resolutions. Define the bottom-right frame coordinates as exclusive upper bounds, matching CPU slice lengths and OpenCL bounding box expectations, and use roundf() on fractional offset and size products. src/tests/integration: Update sub-module. Update expected output after the border precision fixes. RFC: add AGENTS.md (darktable-org#21924) * Add AGENTS.md with conventions for AI coding agents AI-assisted contributions arrive with a recurring set of problems the existing docs do not speak to: commit series where only the final commit compiles, which leaves git bisect useless, and patches written by inferring an API instead of reading the page that documents it. The conventions are not new. They are collected from the Developer's guide, dev-doc/ and the existing history into the file name that most coding assistants read by default. Two wiki corrections are folded in: .clang-format and tools/beautify_style.sh no longer exist, having been removed in 46b054c and b734b01. Related: darktable-org#21881 * Add CLAUDE.md importing AGENTS.md Claude Code reads CLAUDE.md and does not pick up AGENTS.md on its own. The `@AGENTS.md` import keeps the conventions in one file rather than duplicating them, and a one-line pointer works where a symlink would not, such as checkouts with core.symlinks disabled. * AGENTS.md: require a release notes entry with each PR Maintainers now hold a PR until RELEASE_NOTES.md carries an entry for it. Record which changes qualify and which do not, so an agent settles the question while writing the PR rather than at review time. * Support personal agent instructions in AGENTS.local.md Per-project personal instructions have no cross-platform home. Agents offer a global user layer and a committed project layer, but nothing scoped to a single checkout, so carrying the same personal rules across several agents means one file per agent, each excluded by hand. AGENTS.md now points at an optional AGENTS.local.md, written as prose rather than an import directive so that agents which do not expand `@` still act on it, and .gitignore carries the file so nobody has to edit their own exclude list. gui: prevent resize handles from starting graph drags Resizable drawing areas share their widget with the resize wrapper. A press on the resize handle could therefore start the graph gesture as well and change interactive graph parameters while resizing.\n\nRun the resize click and motion controllers in the capture phase. Handle presses are claimed before graph gestures, while presses outside the handle are denied so normal graph interaction remains available.\n\nFixes darktable-org#22106 gui: refresh resize indicators after child motion preview_data: guard the pipe-nodes walk in dt_preview_data_is_fresh with busy_mutex dt_preview_data_is_fresh() (introduced in 8a26f35 / darktable-org#21397, the shared preview-data service for interactive editing) walks dev->preview_pipe->nodes and hashes the matching piece from the GUI thread while holding only the module's own gui_lock. pipe->nodes is actually protected by pipe->busy_mutex: dt_dev_pixelpipe_cleanup_nodes() frees every piece under that lock whenever a history change rebuilds the pipe topology (DT_DEV_PIPE_REMOVE in dt_dev_pixelpipe_change), and that can run concurrently on the pipe-processing thread. A caller that walks pipe->nodes off the GUI thread without busy_mutex can therefore dereference a piece that gets freed out from under it -- reproduced as a segfault dereferencing a freed piece->module in _dev_pixelpipe_cache_basichash(), from a module's preview-pipe-finished GUI callback calling dt_preview_data_is_fresh(). colorequal.c's own interactive hue-editing mode (also from darktable-org#21397) calls dt_preview_data_is_fresh() from mouse-hover/scroll handlers and is exposed to the same race in principle, just from a narrower window that makes it harder to trigger in practice. Take pipe->busy_mutex around the walk. It must be a trylock, not a blocking lock: a module's process() runs under busy_mutex for the whole pipe run (dt_dev_pixelpipe_process) and can itself call dt_preview_data_store() (colorequal.c does, twice), which takes this same module's gui_lock -- the opposite order -- so a blocking lock here would AB-BA deadlock against it. A pipe that's currently busy can't have fresh data for us anyway, so treating "busy" as "not fresh" costs nothing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpGKm49b4w1L51YETXyGpU RELEASE_NOTES.md: ColorEQ interactive editing mode. Introduce `dt_history_hash_unset_mipmap()` Sets the image mipmap hash to zero, used to invalidate that mipmap. Invalidate watermark cachelines for tag/metadata change As reported a metadata or tag change leaves an invalid cacheline, the suggested fix is good and the performance penalty is neglectable as those changes are rare and the module has a high iop_order. Also ensure recalculation of lighttable mipmaps. While being in lighttable view we don't have the iop_order ready so we simply clear the darkroom pipe caches for the next run. Fixes darktable-org#22069 Bump libheif version to the latest for AppImage build
Remove unused quality setting dropdown borders: fix memory corruption on non-positive coordinate deltas Commit 257e632 widened the loop index variable in set_pixels() and copy_pixels() from int to size_t. In dt_iop_copy_image_with_border(), these functions are called with slice lengths computed from coordinate differences such as fl_right - image_right. Under boundary conditions (e.g. scaled thumbnail generation or tiling), the difference can be negative. With a signed int counter, 0 < npixels evaluated to false and safely skipped the loop. With size_t, signed-to-unsigned conversion caused the loop to iterate 18446744073709551615 times, corrupting heap memory and causing a SIGSEGV. Revert the loop variables back to int, and clamp image_right and image_bottom to roi_out bounds in dt_iop_setup_binfo. Keep a path's feather when the shape is resized Scroll-resizing a path morphs a cached baseline of its nodes and swaps the result in whole, borders included. The feather branch of the same scroll handler edits every node's border in place without dropping that cache, so the next resize restored the borders the baseline was captured with and the feather collapsed. Invalidate the resize state there, as _path_modify_property already does for DT_MASKS_PROPERTY_FEATHER. Reproduced by resizing a path with the wheel, raising the feather with shift+scroll, then resizing again; circles and ellipses are unaffected as they have no baseline to go stale. Fixes darktable-org#22052 RELEASE_NOTES.md: path feather lost on scroll resize Add protection against memory allocation failure remove mention of Intel Macs from README (darktable-org#22114) * remove mention of Intel Macs from README * update README for the nightly builds RELEASE_NOTES.md: clean-up the changed dependencies. borders: disable frame line controls when border geometry prevents it The frame line width is proportional to the minimum border width across all four sides. When border size is 0 or when horizontal/vertical offsets are set to extreme values (0.0 or 1.0), the minimum border width collapses to 0, making the frame line invisible. Disable frame line width, offset, and color controls when geometry prevents the frame line from rendering, and update their tooltips to explain the required adjustment. borders: fix asymmetric frame line centering and rounding precision Frame line boundary coordinates previously subtracted 1 to represent inclusive pixel indices, but were assigned directly to half-open interval endpoints (fl_right, border_right, fl_bot, border_bot). This caused the inner border on the right and bottom sides to be 1 pixel narrower and the outer border 1 pixel wider, noticeably shifting the frame line off-center at smaller border sizes and downscaled preview resolutions. Define the bottom-right frame coordinates as exclusive upper bounds, matching CPU slice lengths and OpenCL bounding box expectations, and use roundf() on fractional offset and size products. src/tests/integration: Update sub-module. Update expected output after the border precision fixes. RFC: add AGENTS.md (darktable-org#21924) * Add AGENTS.md with conventions for AI coding agents AI-assisted contributions arrive with a recurring set of problems the existing docs do not speak to: commit series where only the final commit compiles, which leaves git bisect useless, and patches written by inferring an API instead of reading the page that documents it. The conventions are not new. They are collected from the Developer's guide, dev-doc/ and the existing history into the file name that most coding assistants read by default. Two wiki corrections are folded in: .clang-format and tools/beautify_style.sh no longer exist, having been removed in 46b054c and b734b01. Related: darktable-org#21881 * Add CLAUDE.md importing AGENTS.md Claude Code reads CLAUDE.md and does not pick up AGENTS.md on its own. The `@AGENTS.md` import keeps the conventions in one file rather than duplicating them, and a one-line pointer works where a symlink would not, such as checkouts with core.symlinks disabled. * AGENTS.md: require a release notes entry with each PR Maintainers now hold a PR until RELEASE_NOTES.md carries an entry for it. Record which changes qualify and which do not, so an agent settles the question while writing the PR rather than at review time. * Support personal agent instructions in AGENTS.local.md Per-project personal instructions have no cross-platform home. Agents offer a global user layer and a committed project layer, but nothing scoped to a single checkout, so carrying the same personal rules across several agents means one file per agent, each excluded by hand. AGENTS.md now points at an optional AGENTS.local.md, written as prose rather than an import directive so that agents which do not expand `@` still act on it, and .gitignore carries the file so nobody has to edit their own exclude list. gui: prevent resize handles from starting graph drags Resizable drawing areas share their widget with the resize wrapper. A press on the resize handle could therefore start the graph gesture as well and change interactive graph parameters while resizing.\n\nRun the resize click and motion controllers in the capture phase. Handle presses are claimed before graph gestures, while presses outside the handle are denied so normal graph interaction remains available.\n\nFixes darktable-org#22106 gui: refresh resize indicators after child motion preview_data: guard the pipe-nodes walk in dt_preview_data_is_fresh with busy_mutex dt_preview_data_is_fresh() (introduced in 8a26f35 / darktable-org#21397, the shared preview-data service for interactive editing) walks dev->preview_pipe->nodes and hashes the matching piece from the GUI thread while holding only the module's own gui_lock. pipe->nodes is actually protected by pipe->busy_mutex: dt_dev_pixelpipe_cleanup_nodes() frees every piece under that lock whenever a history change rebuilds the pipe topology (DT_DEV_PIPE_REMOVE in dt_dev_pixelpipe_change), and that can run concurrently on the pipe-processing thread. A caller that walks pipe->nodes off the GUI thread without busy_mutex can therefore dereference a piece that gets freed out from under it -- reproduced as a segfault dereferencing a freed piece->module in _dev_pixelpipe_cache_basichash(), from a module's preview-pipe-finished GUI callback calling dt_preview_data_is_fresh(). colorequal.c's own interactive hue-editing mode (also from darktable-org#21397) calls dt_preview_data_is_fresh() from mouse-hover/scroll handlers and is exposed to the same race in principle, just from a narrower window that makes it harder to trigger in practice. Take pipe->busy_mutex around the walk. It must be a trylock, not a blocking lock: a module's process() runs under busy_mutex for the whole pipe run (dt_dev_pixelpipe_process) and can itself call dt_preview_data_store() (colorequal.c does, twice), which takes this same module's gui_lock -- the opposite order -- so a blocking lock here would AB-BA deadlock against it. A pipe that's currently busy can't have fresh data for us anyway, so treating "busy" as "not fresh" costs nothing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpGKm49b4w1L51YETXyGpU RELEASE_NOTES.md: ColorEQ interactive editing mode. Introduce `dt_history_hash_unset_mipmap()` Sets the image mipmap hash to zero, used to invalidate that mipmap. Invalidate watermark cachelines for tag/metadata change As reported a metadata or tag change leaves an invalid cacheline, the suggested fix is good and the performance penalty is neglectable as those changes are rare and the module has a high iop_order. Also ensure recalculation of lighttable mipmaps. While being in lighttable view we don't have the iop_order ready so we simply clear the darkroom pipe caches for the next run. Fixes darktable-org#22069 Bump libheif version to the latest for AppImage build
…image (darktable-org#21397) * colorequal: interactive hue-under-cursor editing with a shared preview-data service Adds an on-canvas interactive editing mode driven by the hue under the mouse cursor: hovering the image shows a color indicator and lets the scroll wheel apply a Gaussian-weighted adjustment (sigma=35 degrees) to the saturation/hue/brightness nodes of the active channel, Alt+scroll switches channel tabs (on both the image and the graph), and the graph's own scroll handling mirrors the same behavior for the node under the cursor there. The per-pixel hue buffer needed for this is served by a new shared service, dt_preview_data_t (src/develop/preview_data.c/.h): resizing, filling and hashing the preview-pipe buffer under a single GUI lock so readers never observe a resized-but-unfilled buffer, plus freshness checks against the pipe's cumulative hash. gui_focus()/mouse_moved() use it to request a debounced preview reprocess so the indicator works right after opening the module instead of only after an unrelated trigger. The on-canvas cursor itself (crosshair, wedge, circles, text readout) is factored into a shared dt_draw_correction_cursor() helper in src/gui/draw.h and reused by the tone equalizer, so both modules render the same cursor design and future modules can adopt it too. * colorequal: address review feedback and show in/out colors under cursor - don't show a busy/wait cursor while the preview pipe recomputes during hover (toneequal: drop the busy branch in switch_cursors, gate the exposure re-read instead; colorequal: same in _switch_cursors) - draw a white vertical line on the graph at the hue under the mouse cursor, mirroring the tone equalizer's exposure cursor line - open the wedge cursor up to +-90 degrees (clamped at +-1) instead of clamping it to 45 degrees; toneequal pre-scales the correction by 0.5 so the wedge reaches its full span at +-2 EV - store the module input HSB (hue, saturation, brightness) instead of just the hue in the shared preview buffer (3 components per pixel) and show the module input/output colors in the cursor's two circles This brings the branch in line with the review comment on PR darktable-org#21397 and adds the in/out colors feature. * Restoring the code after a rebase * toneequal: derive cursor frame color from the sampled background Like the color equalizer's cursor, sample the pixel under the cursor from the preview pipe backbuf and draw the frame lines (wedge, crosshair, outlines) white over dark content and black over bright content. This replaces _match_color_to_background(), which derived grey shades from the estimated output exposure instead of the actual background, so the lines could end up mid-grey. The circles keep their before/after luminance grey shades.
_switch_cursors() and switch_cursors() restored the default cursor with
a raw gdk_window_set_cursor() on the toplevel, while hiding it through
dt_control_change_cursor("none"). Going through the helper for both
halves brings back three things the raw call skips:
- The lock set by dt_control_forbid_change_cursor(). Shortcut-mapping
mode and help mode (src/libs/tools/global_toolbox.c) hold that lock
for as long as the mode is active, apply their own cursor to the same
toplevel, and still forward motion events to the centre canvas -- so
with the tone equalizer focused, moving the pointer over the canvas
replaced their cursor with the plain arrow. dt_gui_cursor_set_busy()
takes the same lock.
- The -d input tracing in dt_control_cursor_debug(), which is called
from the helpers only.
- The GTK3/GTK4 compatibility boundary. gdk_window_set_cursor() has no
GTK4 equivalent; these were its last uses outside src/gui/gtk.c.
Where the lock is not held the two forms are equivalent: on GTK3
dt_control_change_cursor() ends up calling gdk_cursor_new_from_name()
and gdk_window_set_cursor() on the same toplevel window.
For toneequal.c this restores what was there before 8a26f35
(darktable-org#21397); colorequal.c copied the pattern from it in the same commit.
The behaviour that commit intended -- no wait cursor while hovering,
whatever the pipe is doing -- is unchanged.
Built and verified at runtime on X11/GTK3.
Suggested-by: a darktable contributor
…ith busy_mutex dt_preview_data_is_fresh() (introduced in 8a26f35 / darktable-org#21397, the shared preview-data service for interactive editing) walks dev->preview_pipe->nodes and hashes the matching piece from the GUI thread while holding only the module's own gui_lock. pipe->nodes is actually protected by pipe->busy_mutex: dt_dev_pixelpipe_cleanup_nodes() frees every piece under that lock whenever a history change rebuilds the pipe topology (DT_DEV_PIPE_REMOVE in dt_dev_pixelpipe_change), and that can run concurrently on the pipe-processing thread. A caller that walks pipe->nodes off the GUI thread without busy_mutex can therefore dereference a piece that gets freed out from under it -- reproduced as a segfault dereferencing a freed piece->module in _dev_pixelpipe_cache_basichash(), from a module's preview-pipe-finished GUI callback calling dt_preview_data_is_fresh(). colorequal.c's own interactive hue-editing mode (also from darktable-org#21397) calls dt_preview_data_is_fresh() from mouse-hover/scroll handlers and is exposed to the same race in principle, just from a narrower window that makes it harder to trigger in practice. Take pipe->busy_mutex around the walk. It must be a trylock, not a blocking lock: a module's process() runs under busy_mutex for the whole pipe run (dt_dev_pixelpipe_process) and can itself call dt_preview_data_store() (colorequal.c does, twice), which takes this same module's gui_lock -- the opposite order -- so a blocking lock here would AB-BA deadlock against it. A pipe that's currently busy can't have fresh data for us anyway, so treating "busy" as "not fresh" costs nothing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpGKm49b4w1L51YETXyGpU





Hello,
Here’s an old idea that’s resurfaced in my mind: an adaptation of the color equalizer module inspired by the tone equalizer module.
I’ve implemented the ability to correct a color by selecting it directly on the photo, on the fly. Needless to say, the AI has been a big help.
New Features
mouse_moved()– reads the UCS hue from the preview pipe and converts it to GUI degreesmouse_leave()– invalidates the hue when the mouse leaves the imagegui_post_expose()– draws a color circle at the cursor (black + RGB fill)scrolled()– scroll wheel on the image → applies a Gaussian weighting (σ=35°) to the nodes of the active channel, locks zooming withreturn 1_area_scrolled_callback()rewritten: dual mode (classic if no picker, Gaussian otherwise)_get_param_ptr()– direct access to parameters viaoffsetof(), used by the Gaussian loopHere is the forum discussion on pixls.us; I haven’t changed the user interface,
Greetings from the Luberon,
Christian