darktable-org / darktable-org/darktable
_auto_set_exposure() is manual-mode logic kept out of automatic mode by a reset call, not a guard
Nobody has claimed this yet.
- Dominant language
- C
- Stars
- 13.1k
- Forks
- 1.4k
- Avg merge
- 22h 14m
- Merged PRs (30d)
- 198
Description
Found by code analysis, 2026-09-02. Split out of the #21974 Defect 3 work.
Updated 2026-09-06 against branch fix-21974-applied-exposure-disagreement at
11093f0767b118d8f046562026e37a29dec8aacf; all line references below are that HEAD.
Severity: low. Reachable, but only through a user-assigned shortcut. The sibling
entry points were given an explicit mode guard by the #21974 Defect 3 fix; this one
was left relying on a behavioural assumption.
Note on wording: the mode is EXPOSURE_MODE_DEFLICKER in the source, but the
combobox shows it as automatic (exposure.c:52). This report says "automatic".
Summary
_auto_set_exposure() (src/iop/exposure.c:918) is manual-mode logic. Both its
branches assume the pipe applies the manual composition:
if(mode == DT_SPOT_MODE_MEASURE)
{
const float exposure_adjustment = _total_adjustment_ev(self, p); // :957
... // displays it in the GUI
}
else if(mode == DT_SPOT_MODE_CORRECT)
{
_exposure_set_white(self, XYZ[1] / XYZ_target[1]); // :994 writes p->exposure
}
In automatic mode neither holds. _process_common_setup() (:476-517) replaces the
committed exposure with the histogram-derived correction, so p->exposure is inert:
MEASURE would display a number the pipe does not apply, and CORRECT would write a
parameter the pipe ignores while pushing a history item for it.
Nothing in _auto_set_exposure() tests p->mode.
Why the widgets are reachable
The controls that drive it are packed outside g->mode_stack, so they stay
visible when the stack shows the "deflicker" page (gui_init()):
self->widget = dt_gui_vbox(); // :1310 outer box
g->mode = dt_bauhaus_combobox_from_params(self, N_("mode"));
dt_gui_box_add(self->widget, g->mode_stack); // :1314
g->black = dt_bauhaus_slider_from_params(self, "black"); // :1316 outside the stack
dt_gui_new_collapsible_section(&g->cs, ...); // :1326 outside the stack
whereas g->exposure and the two compensation toggles are inside vbox_manual
(:1252-1280), and therefore hidden in automatic mode.
So in automatic mode the user is shown an "area exposure mapping" section: spot mode
combobox, lightness slider, origin/target swatches, all belonging to a mode they are
not in.
The behavioural chain that normally keeps it out of automatic mode
Both call sites into _auto_set_exposure() are gated on the picker attached to
g->exposure being active:
void color_picker_apply(...) // :999
{ DT_GUARD_GUI_UPDATE(); _auto_set_exposure(self, pipe); } // :1004, driven by the picker
static void _spot_settings_changed_callback(...) // :1213
{
...
if(mode == DT_SPOT_MODE_CORRECT && dt_iop_color_picker_is_active(g->exposure))
_auto_set_exposure(self, darktable.develop->full.pipe); // :1234
}
and entering automatic mode deactivates that picker, from both directions:
static void _autoexp_disable(dt_iop_module_t *self) // :690
{ dt_iop_color_picker_reset(self, TRUE); }
gui_update(): case EXPOSURE_MODE_DEFLICKER: _autoexp_disable(self); // :755
gui_changed(): case EXPOSURE_MODE_DEFLICKER: _autoexp_disable(self); // :1036
The keep = TRUE argument matters: dt_iop_color_picker_reset()
(src/gui/color_picker_proxy.c:122-136) skips the reset when the picker widget is
named "keep-active". exposure.c never sets that name; only colorequal.c:3798,
colorzones.c:2633, rgbcurve.c:1495 and blend_gui.c:2610 do, so the reset does
fire.
So the chain is:
- every path into automatic mode calls
_autoexp_disable(); - the picker is not
"keep-active", so the reset is not skipped; - the picker cannot be re-armed while its widget is hidden.
That is a behavioural chain, not a guard, and it was not written to be one.
Reachability: step 3 does not hold
Step 3 was previously untraced. It is false. darktable's shortcut system can
dispatch to a widget on a non-selected GtkStack page, and g->exposure carries a
registered shortcut (GDK_KEY_e, exposure.c:1280), so its quad, i.e. the picker
toggle, can be given a shortcut by the user and pressed in automatic mode.
- shortcut dispatch accepts a widget that passes
dt_action_widget_invisible()
(src/gui/accelerators.c:4123) - that predicate reads only the
visibleproperty of the widget and of its
immediate parent (accelerators.c:3810-3817). It does not look at the stack's
selected page GtkStackhides the unselected page withgtk_widget_set_child_visible()
(gtk/gtkstack.c:1118, GTK 3.24.52), which does not clear thevisible
property. So the predicate reports the hidden slider as visible- the slider's button element goes to
_action_process_button()
(src/bauhaus/bauhaus.c:4269, body at:4195), which presses and releases the
quad and emits"quad-pressed"(bauhaus.c:1616, signal defined at:4139) - that signal is connected to the colour picker
(src/gui/color_picker_proxy.c:549). Activation installs the picker proxy and
setsrequest_color_pick(color_picker_proxy.c:185), with no test of exposure's
mode - incoming samples then reach the module through
module->color_picker_apply()(color_picker_proxy.c:426), i.e.
_auto_set_exposure()
The default bare e shortcut targets the slider's value element, not the button, so
this needs a shortcut the user assigned to the button/quad element. That is the only
thing keeping the severity low. Not reproduced interactively: the chain above is
read off the source, not observed in a GTK session.
Suggested fix
Return early from _auto_set_exposure() unless p->mode == EXPOSURE_MODE_MANUAL.
One line, and it makes the precondition local to the function that depends on it
instead of distributed across two mode-switch handlers.
Optionally, and separately, hide the "area exposure mapping" section in automatic
mode by moving g->cs inside vbox_manual, or by toggling its sensitivity from the
g->mode branch of gui_changed(). That is a UX question, not a correctness one,
and it changes the panel layout, so it is worth deciding on its own rather than
bundling.
Do not re-derive _auto_set_exposure()'s sign conventions while fixing this. The
double negation that looks wrong was cleared as a negative result while investigating
potential issues found durint the #21974 fix,
and re-confirmed on 2026-09-02 when the #21974 Defect 3 fix replaced the CORRECT
branch's twelve hand-rolled lines with _exposure_set_white(self, XYZ[1] / XYZ_target[1]).
That equivalence holds only for a positive luminance ratio. The old round trip
through white2exposure() and exposure2white() also clamped a non-positive ratio
to a small positive white before it reached the black comparison inside
_exposure_set_white(); the short form does not.
Dependencies and neighbours
- #21974, Defect 3: depends on it, do not land before. That fix gave the two
sibling clamps ingui_changed()an explicitp->mode == EXPOSURE_MODE_MANUAL
guard, for exactly this reason:g->blacksits outside the mode stack, so its
clamp did run in automatic mode against a manual white point.
_auto_set_exposure()is the remaining unguarded entry point of that same set. As
of 2026-09-06 Defect 3 is committed on branch
fix-21974-applied-exposure-disagreement, not merged.
A mode guard here is nevertheless valid on its own: it does not need the new
compensation helpers. - #22008: sibling, no code overlap. Same shape one layer up: the exposure
proxy's one write entry point (dt_dev_exposure_handle_event()) lacks the view
guard its three read siblings have. If the two are fixed together, the shared
lesson is worth stating once: in this module the read paths have been audited
repeatedly, and the write paths keep turning out to be missing a precondition. - #22183: same function, different defect.
Both branches model the pipe as a pure gain and ignore the black point, in manual
mode, where this report's guard does not apply. Previously mentioned here as an
unfiled aside; it now has its own record. - same function, branch regression.
A target lightness of 0 lets the CORRECT branch write an extreme negative black.
That one has to be fixed on the #21974 branch; this one does not.
Environment
Present in 54c024cc5a and unchanged in behaviour by #21974. Static
analysis only: no display in this container, nothing verified at runtime. The GTK
and bauhaus line references were read in this checkout and in
gtk-3.24.52.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/iop/exposure.c at _auto_set_exposure() and inspect its callers and the p->mode handling described in the issue. Add the local manual-mode precondition without changing the manual branches, then verify that automatic-mode picker activation no longer reaches this logic; account for dependency #21974 before landing.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c
- Domain
- desktop
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 75/100