diff --git a/dev-doc/IOP_Module_API.md b/dev-doc/IOP_Module_API.md
index 3972e2b5b988..79e34fb74e0b 100644
--- a/dev-doc/IOP_Module_API.md
+++ b/dev-doc/IOP_Module_API.md
@@ -4,6 +4,7 @@ This guide documents the functions that darktable Image Operation (IOP) modules
See also:
- [Pixelpipe Architecture](pixelpipe_architecture.md) for pipeline data flow and caching.
+- [Module Lifecycle](Module_Lifecycle.md) for when these callbacks fire and what the system does around them.
- [Introspection System](introspection.md) for parameter management.
- [GUI Architecture](GUI.md) for GUI events, callbacks, thread safety, and widget reparenting.
@@ -360,7 +361,13 @@ Access in `process_cl()` via `self->global_data`.
### `reload_defaults()` - Per-Image Defaults
-Called when switching images. Update defaults based on image properties:
+Called when switching images (and on first load). Its purpose is to make `self->default_params` image-specific — most modules have defaults that depend on whether the image is RAW, HDR, monochrome, etc., and those cannot be compile-time constants.
+
+When invoked through the wrapper `dt_iop_reload_defaults()`, the harness calls `dt_iop_load_default_params()` after the module's own `reload_defaults()` returns, copying `default_params` into `self->params`. So on the wrapper path, writing to `default_params` inside `reload_defaults()` immediately affects `self->params` as well. Most callers use the wrapper (e.g. `_dt_dev_load_pipeline_defaults()`, the per-module Reset button, instance duplication).
+
+**Caveat — direct calls bypass only the framework copy, not the module body.** A few sites invoke `module->reload_defaults(module)` directly instead of through the wrapper (for example `develop.c` calls `temperature->reload_defaults(temperature)` to refresh WB side-effects). Those callers do **not** get the automatic `dt_iop_load_default_params()`, so the wholesale `default_params → self->params` copy does not happen. This does **not** mean `self->params` is left untouched: the module's own `reload_defaults()` body may still write specific `self->params` fields directly. `temperature.reload_defaults()` does exactly this — it writes `p->preset` and `p->late_correction` (via `p = self->params`) in lockstep with `default_params`, while the WB coefficient array is written to `default_params` only. On a direct call, the result is therefore a *partial* update: the fields the body touches change in `self->params`, the rest do not, and no history entry is recorded. If you add such a call, prefer `dt_iop_reload_defaults()`, or audit exactly which `self->params` fields the body mutates and sync the rest yourself.
+
+#### Job 1 (all modules that implement this): compute image-specific default params
```c
void reload_defaults(dt_iop_module_t *self)
@@ -373,8 +380,31 @@ void reload_defaults(dt_iop_module_t *self)
}
```
+Example: `exposure.c` disables itself for non-RAW images by setting `self->default_enabled = FALSE`. The exposure value in `default_params` stays at its static value because exposure is image-type-agnostic beyond that flag.
+
Common checks: `dt_image_is_raw()`, `dt_image_is_hdr()`, `dt_image_is_ldr()`, `dt_image_is_monochrome()`, `dt_image_is_bayerRGB()`.
+#### Job 2 (some modules): write shared inter-module data as a side effect
+
+Some modules use `reload_defaults()` to populate shared state in `dev->chroma` (or similar structures) that other modules depend on. This happens regardless of whether the module is enabled or has a history entry.
+
+Example: `temperature.c` always computes the camera's as-shot WB coefficients and D65 reference coefficients from the image's EXIF data and writes them into `dev->chroma.as_shot[]` and `dev->chroma.D65coeffs[]`. `channelmixerrgb.c` reads these values during its own `commit_params()` to perform chromatic adaptation. If `reload_defaults()` did not run unconditionally, `channelmixerrgb` would have no access to the camera's native WB — even when the white balance module itself is disabled or absent from the history.
+
+For the complete producer/consumer map, the reverse-order default-loading reasoning, and which `dev->chroma` fields each module actually reads, see [iop/wb_and_colorcalibration](iop/wb_and_colorcalibration/README.md).
+
+#### Where `default_params` is consumed
+
+**Initial image load.** Inside `dt_dev_read_history_ext()`, `_dt_dev_load_pipeline_defaults()` calls `reload_defaults()` for every module (in reverse pipe order) and copies each `default_params` into `params`. The function then adds the workflow default modules and any auto-presets, and builds `dev->history` **directly from the database rows** — it does *not* call `dt_dev_pop_history_items`. A module with a history row runs with the saved params from that row; a module without one keeps the image-specific `default_params` just loaded. This is true on both the first open of an image and the reopen of an edited one; the only difference is whether the database has any rows for that module.
+
+**Undo / redo / history-panel navigation.** This is the separate path that uses `dt_dev_pop_history_items_ext()`, which does two passes:
+
+1. Resets all modules: `module->params = module->default_params` — so modules absent from the replayed range start from their image-specific default, not garbage.
+2. Replays history entries: `module->params = hist->params` for each entry up to the target position.
+
+So `default_params` is consumed in two ways: as the starting point for modules with no (or not-yet-replayed) history, and as the value the per-module "Reset to defaults" button restores. It also sets the visual "default" markers on GUI sliders (via `dt_bauhaus_slider_set_default()`).
+
+For the full sequence — load order, the `dev->chroma` side effects, and the reverse-iteration hazard — see [Module_Lifecycle.md](Module_Lifecycle.md).
+
### Pipe Lifecycle Functions
Each pixelpipe has its own copy of every module's data via `dt_dev_pixelpipe_iop_t` ("piece").
diff --git a/dev-doc/Module_Groups.md b/dev-doc/Module_Groups.md
index 7fb27a9c69be..331076785bf8 100644
--- a/dev-doc/Module_Groups.md
+++ b/dev-doc/Module_Groups.md
@@ -9,7 +9,7 @@ Darktable uses a tabs-based interface in the right panel. Each tab corresponds t
- **Technical**: Basic corrections (demosaic, lens, crop).
- **Grading**: Color and tone grading.
- **Effects**: Artistic effects.
-- **Quick Access Panel**: (Special case, detailed in `Quick_Access_Panel.md`).
+- **Quick Access Panel**: (Special case, detailed in [Quick_Access_Panel.md](Quick_Access_Panel.md)).
## Implementation (`modulegroups.c`)
diff --git a/dev-doc/Module_Lifecycle.md b/dev-doc/Module_Lifecycle.md
new file mode 100644
index 000000000000..044e95233f89
--- /dev/null
+++ b/dev-doc/Module_Lifecycle.md
@@ -0,0 +1,457 @@
+# Module Lifecycle: What Happens When the User Interacts
+
+[IOP_Module_API.md](IOP_Module_API.md) documents the callback API: what each function signature
+means and what a module must implement. This document is its companion. It describes *when* those callbacks
+fire and *what else* the system does around them — pipe change flags, cache invalidation,
+history replay, and cross-module data flow.
+
+After reading the API doc you can implement a module. After reading this one you should also
+have a mental model of what darktable does when the user loads an image, drags a slider,
+toggles a module, resets it, or undoes an edit.
+
+References point to code by **file, function, and the relevant block** (an `if`, `for`, or
+`while`), and use short snippets where that is clearer. They avoid line numbers, which drift
+as the source changes.
+
+---
+
+## 1. Background concepts
+
+These are the moving parts the later sections rely on. Each one gets a short definition, not
+a full reference.
+
+### History stack (`dt_dev_history_item_t`)
+
+An ordered list of per-module parameter snapshots on
+[`dt_develop_t`](pixelpipe_architecture.md#dt_develop_t) (the main darkroom session state).
+`dev->history_end` is
+the active cursor: only the entries in the range `[0, history_end)` are "live". Note that a
+history item's **`enabled` flag is stored separately from `params`** — it is not part of the
+params buffer. Toggling a module on or off and editing its parameters are therefore two
+different kinds of history change.
+
+### Pipe types
+
+Three pixelpipes run the same module chain independently, at different resolutions:
+
+- **full** — the main darkroom image
+- **preview** — the low-resolution thumbnail (used for the histogram and navigation)
+- **preview2** — an optional second-window preview
+
+Each is marked dirty and re-run on its own, often in parallel on separate threads. "Mark all
+pipes dirty" below means all of the pipes that exist for the current session.
+
+### Pipe change flags
+
+These are set on `pipe->changed`. They tell `dt_dev_pixelpipe_change()` (pixelpipe_hb.c)
+which resync strategy to use when the pipeline next runs. The constants are defined in
+pixelpipe_hb.h.
+
+| Flag | Meaning | Strategy |
+|---|---|---|
+| `DT_DEV_PIPE_TOP_CHANGED` | Only the topmost history item's params changed | `synch_top`: re-commit that one module; upstream piece hashes stay untouched |
+| `DT_DEV_PIPE_SYNCH` | All items need a param resync, topology unchanged | `synch_all`: re-commit every history item in order |
+| `DT_DEV_PIPE_REMOVE` | A module was added or removed; topology must be rebuilt | full `cleanup_nodes` + `create_nodes` + `synch_all` |
+| `DT_DEV_PIPE_ZOOMED` | Zoom or pan only | skip the param sync; only the `modify_roi_in`/`modify_roi_out` chain re-runs |
+
+`synch_all` re-commits every module, but a module whose hash did not change still produces a
+cache hit, so its `process()` is skipped. In other words, the flag controls *committing*, not
+*reprocessing*. Reprocessing is decided per module by the cache (see
+[section 6](#6-pipeline-execution-detail)).
+
+### Pipe status flags
+
+`DIRTY` (needs reprocessing), `RUNNING` (a thread is processing it now), `VALID` (output is
+current), `INVALID` (unusable, for example during teardown).
+
+### `focus_hash`
+
+A module is **focused** when the user opens its panel or clicks into one of its widgets; the
+darkroom tracks a single focused module at a time. `dt_iop_request_focus()` records that module
+and updates `dev->focus_hash`, a hash of the focused module's widget. Inside
+`_dev_add_history_item_ext()` (develop.c), the relevant test is roughly:
+
+```c
+if(dev->focus_hash != hist->focus_hash) { /* force a new history entry */ }
+```
+
+If the user moved focus since the last commit, a **new** history entry is forced even when
+the same module is edited again. The practical effect is that the edit then takes the `SYNCH`
+path instead of the cheaper `TOP_CHANGED` path. If you write deferred or batched parameter
+changes, keep this in mind, because it changes how much of the pipe re-commits.
+
+### Pixelpipe cache
+
+The cache is hash-based with lazy invalidation. The key for a module's output is the
+**cumulative hash** of every upstream `piece->hash` value (each computed in
+`dt_iop_commit_params()` from the op name, instance, params, and blend_params), combined with
+the relevant color-profile information. It is *not* a hash of one module's data on its own.
+The hashing happens in the basic-hash helper in pixelpipe_cache.c, which seeds the hash from
+the image id and pipe profiles and then walks all modules up to the requested position.
+
+When the key matches a cached entry, the output is reused and `process()` is skipped. Stale
+entries are not actively deleted on a param change; they simply stop being looked up, and the
+fixed-size LRU evicts them later. For the full caching model see
+[pixelpipe_architecture.md](pixelpipe_architecture.md#pipeline-caching).
+
+### `dev->chroma` (shared WB/CAT state)
+
+A struct on `dt_develop_t` used by white balance (`temperature.c`, the producer) and color
+calibration (`channelmixerrgb.c`, the consumer) to communicate. There is **no signal**;
+synchronization is implicit, through the order in which `commit_params()` runs (see
+[section 4](#4-cross-module-interactions) and [section 5](#5-pipeline-ordering-asymmetry)).
+Temperature writes the camera WB reference in `reload_defaults()` and the live coefficients in
+`commit_params()`; channelmixerrgb reads them. The full field-by-field producer/consumer map and
+the reverse-load reasoning live in their own doc — see
+[wb_and_colorcalibration](iop/wb_and_colorcalibration/README.md).
+
+### `module->params` vs `module->default_params`
+
+`default_params` is **image-specific**: it is set by `reload_defaults()` (see
+[IOP_Module_API.md](IOP_Module_API.md#reload_defaults---per-image-defaults)). `params` is the
+**live editing state**.
+`dt_dev_pop_history_items_ext()` resets every module to its `default_params` and then replays
+the history entries on top. As a result, a module with no history entry runs with its
+image-specific defaults, not with compile-time constants.
+
+---
+
+## 2. Image loading
+
+A pristine (never edited) image and one with saved history both go through the **same**
+`dt_dev_load_image()` → `dt_dev_read_history_ext()` chain. The "no history" versus "has
+history" distinction is handled **inside** `dt_dev_read_history_ext()`, not by different
+callers.
+
+### Load sequence (`dt_dev_load_image()`, develop.c)
+
+1. **`_dt_dev_load_raw()`** triggers the rawspeed decode through the mipmap cache (this
+ blocks), then reads the decoded image struct into `dev->image_storage`. The large mipmap
+ buffer is released right after the decode; `image_storage` keeps the metadata. (The *mipmap
+ cache* holds pre-scaled copies of each image at several resolutions, used for fast thumbnail
+ and preview generation — see [mipmap](https://en.wikipedia.org/wiki/Mipmap).)
+2. Mark all pipes `DIRTY` and set `loading = TRUE`.
+3. **`dt_iop_load_modules()`** builds the IOP module list (`dev->iop`).
+4. **`dt_dev_read_history_ext()`** does everything else — defaults, presets, and history
+ replay. See below.
+
+`dt_dev_load_image()` itself does not touch `dev->chroma` and does not load defaults. All of
+that happens inside `dt_dev_read_history_ext()`.
+
+### Inside `dt_dev_read_history_ext()` (develop.c)
+
+This function applies saved history by **building `dev->history` directly from the database** —
+it allocates one history item per stored row and appends it to the list. (This is a different
+mechanism from the reset-then-replay path that undo/redo uses later; that one is described in
+[section 3f](#3f-undo-redo-and-history-panel-navigation).)
+
+**For every image** — this is the `if(!no_image)` block, and it runs whether or not the image
+has saved history:
+
+- Clear the scratch `memory.history` table.
+- **`dt_dev_reset_chroma()`** clears part of the shared WB state
+ ([section 5](#5-pipeline-ordering-asymmetry) lists exactly which fields): `temperature` and
+ `adaptation` become `NULL`, and `wb_coeffs[]` is reset to 1.0.
+- **`_dt_dev_load_pipeline_defaults()`** calls `dt_iop_reload_defaults()` on every module in
+ **reverse** pipe order ([section 5](#5-pipeline-ordering-asymmetry) explains the direction).
+ This sets each module's image-specific
+ `default_params`. Because it goes through the wrapper, it also copies them into `params`
+ (via `dt_iop_load_default_params()`). Temperature additionally populates
+ `dev->chroma.as_shot[]` and `D65coeffs[]` as a side effect.
+- **`_dev_add_default_modules()`** prepends the workflow-mandated modules into the in-memory
+ history.
+- **`_dev_auto_apply_presets()`** applies auto-presets. It is gated on the
+ `DT_IMAGE_AUTO_PRESETS_APPLIED` image flag, **not** on `change_timestamp == -1` (that test
+ guards only the separate legacy pre-3.0 WB recovery branch). It is a no-op for an image
+ whose presets were already applied, so only a first import actually gains preset entries
+ here.
+- **`_dev_merge_history()`** merges the in-memory default and preset history into
+ `main.history`, so the database read below picks it up.
+
+**Then, for all images,** the same database-read loop runs. For each `main.history` row it
+allocates a `hist` item, copies the params (falling back to the module's `default_params`
+when the row has none), sets `hist->enabled`, appends to `dev->history`, and increments
+`history_end`:
+
+```c
+memcpy(hist->params, hist->module->default_params, hist->module->params_size);
+hist->enabled = TRUE;
+...
+dev->history = g_list_append(dev->history, hist);
+dev->history_end++;
+```
+
+Modules with no database row keep the image-specific `default_params` set earlier by
+`_dt_dev_load_pipeline_defaults()`. The `enabled` flag is carried per entry, separately from
+params.
+
+After the loop, if both `temperature` and `channelmixerrgb` are present, the function calls
+`temperature->reload_defaults(temperature)` **directly**, to resync the WB/CAT shared state:
+
+```c
+if(temperature && channelmixerrgb)
+ temperature->reload_defaults(temperature);
+```
+
+This direct call is a deliberate wrapper bypass: it skips the framework's wholesale
+`default_params → params` copy (`dt_iop_load_default_params()`). It still refreshes
+`default_params` and the `dev->chroma` reference data, and temperature's own body also writes
+a few `self->params` fields directly (`preset`, `late_correction`). So `params` is partially
+updated, not fully resynced — see the `reload_defaults` caveat in
+[IOP_Module_API.md](IOP_Module_API.md#reload_defaults---per-image-defaults).
+
+The function then re-reads `history_end` from `main.images`, and — when the GUI is attached —
+calls `dt_dev_pipe_synch_all()` and `dt_dev_invalidate_all()` so the pipes will commit and
+reprocess. The pipes stay `DIRTY`, and the pipeline threads run on the next redraw
+([section 6](#6-pipeline-execution-detail)).
+
+---
+
+## 3. Editing operations
+
+### 3a. Adjust a slider — the common `TOP_CHANGED` path
+
+1. The bauhaus slider fires its value-change handler (`bauhaus.c`), which calls
+ `dt_iop_gui_changed(module, widget, &prev)` directly.
+2. The module's `gui_changed()` runs (module-specific; it may update dependent widgets).
+3. `dt_dev_add_history_item_target()` reaches `_dev_add_history_item_ext()` (develop.c), which
+ chooses one of two branches:
+ - **`TOP_CHANGED`**: the same module is already at the top of the history,
+ `dev->focus_hash == hist->focus_hash`, and only the params differ. The params are
+ updated in place and `pipe->changed |= DT_DEV_PIPE_TOP_CHANGED`.
+ - **`SYNCH`**: anything else (a different module, or focus changed since the last commit).
+ A new history entry is pushed and `pipe->changed |= DT_DEV_PIPE_SYNCH`.
+4. `dt_dev_invalidate_all()` marks all pipes `DIRTY` and increments `dev->timestamp`.
+5. `DT_SIGNAL_DEVELOP_HISTORY_CHANGE` is emitted **only when `need_end_record` is true**, not
+ on every slider tick. It is tied to the undo-record start/end pairing, so one logical edit
+ emits the signal once.
+6. On the pipeline thread, `dt_dev_pixelpipe_change()` dispatches on the flag:
+ - `TOP_CHANGED` → `dt_dev_pixelpipe_synch_top()`: runs `commit_params()` for that one
+ module only; upstream piece hashes are not recomputed.
+ - `SYNCH` → `dt_dev_pixelpipe_synch_all()`: runs `commit_params()` for every history item
+ in order. Upstream modules re-commit, but an unchanged hash still hits the cache.
+7. The pipeline runs: modules with unchanged hashes reuse their cached output; the changed
+ module and everything downstream of it reprocess.
+
+### 3b. Enable / disable a module
+
+1. The toggle button fires its `toggled` signal, which reaches `_gui_off_callback()`
+ (imageop.c).
+2. The callback sets `module->enabled` and calls `dt_dev_add_history_item()`. The
+ `hist->enabled` flag is recorded **separately from params**.
+3. `pipe->changed |= DT_DEV_PIPE_SYNCH`, because an enable/disable propagates downstream.
+4. A full `synch_all` runs on the next pipeline pass.
+
+### 3c. Reset to defaults
+
+1. The reset button fires `button-press-event`, which reaches `_gui_reset_callback()`
+ (imageop.c).
+2. `dt_iop_reload_defaults(module)` recomputes the image-specific `default_params` and copies
+ them into `module->params` (this is the wrapper path, so the copy does happen).
+3. `dt_iop_gui_update(module)` syncs the widgets to the new params.
+4. `dt_dev_add_history_item(module->dev, module, TRUE)` adds or updates a **single** history
+ entry. It does not rebuild the whole stack.
+5. `pipe->changed |= DT_DEV_PIPE_SYNCH`.
+
+### 3d. Add a module instance (+ button)
+
+1. `dt_iop_gui_duplicate()` (imageop.c) starts the duplication.
+2. It records the base module's state: `dt_dev_add_history_item(base->dev, base, FALSE)`.
+3. `dt_dev_module_duplicate()` creates the new instance in `dev->iop`.
+4. `dt_iop_reload_defaults(new_module)` sets the image-specific defaults for the new instance.
+5. `dt_dev_add_history_item(new_module->dev, new_module, TRUE)` adds a new history entry.
+6. `dt_dev_pixelpipe_rebuild()` sets `pipe->changed |= DT_DEV_PIPE_REMOVE`, which forces a
+ full topology rebuild — the node graph changed, so it is rebuilt from scratch.
+
+### 3e. Remove a module instance (− button)
+
+`dt_dev_module_remove()` (develop.c) does the teardown. Inside its `if(dev->gui_attached)`
+block — true in the interactive darkroom — it **prunes the history**. It walks `dev->history`
+and, for each entry whose module is the one being removed, frees the item, unlinks it, and
+decrements `history_end`:
+
+```c
+if(module == hist->module)
+{
+ dt_dev_free_history_item(hist);
+ dev->history = g_list_delete_link(dev->history, elem);
+ dev->history_end--;
+}
+```
+
+It then removes the module from `dev->iop`. A `REMOVE` flag triggers a full topology rebuild
+(`cleanup_nodes` + `create_nodes`).
+
+So a removed instance's history entries are **actively deleted**, not merely skipped. In a
+headless context, where `gui_attached` is false, this pruning block does not run — but
+interactive removal is the case this document covers.
+
+### 3f. Undo, redo, and history-panel navigation
+
+This is a **distinct path** from the initial image load. During an active editing session it
+is the main way `module->params` are rewritten from saved entries.
+
+1. The user clicks a history entry, or presses Ctrl+Z / Ctrl+Y.
+2. `dt_dev_reload_history_items()` (develop.c) sets `dev->history_end` to the target position.
+3. `dt_dev_pop_history_items(dev, 0)` resets all modules to their `default_params`.
+4. The history is re-read (from memory or the database).
+5. `dt_dev_pop_history_items(dev, history_end)` replays up to the target position.
+6. `dt_dev_invalidate_all()` marks the pipes dirty, which leads to a full pipeline rerun.
+
+The two `pop_history_items` calls live **here**, in `dt_dev_reload_history_items()`, not in
+`dt_dev_read_history_ext()`. Apart from the initial image load, this is the only path that
+rewrites `module->params` from saved history. The `SYNCH` flag ensures every module
+re-commits after the replay.
+
+---
+
+## 4. Cross-module interactions
+
+### 4a. Temperature (WB) → ChannelMixerRGB (through `dev->chroma`)
+
+There is no signal; synchronization is implicit and ordered by iop_order. Because temperature is
+upstream of channelmixerrgb, a full `synch_all` runs `commit_params()` in pipe order:
+`temperature.commit_params()` writes `dev->chroma.wb_coeffs[]` first and
+`channelmixerrgb.commit_params()` reads it afterward to build its chromatic-adaptation matrix.
+(`synch_top` re-commits only the topmost history item, so it is not evidence that an upstream
+producer has just run.) The full producer/consumer field map, the once-per-load `reload_defaults`
+side effects, and why reverse-order default loading stays correct are covered in
+[wb_and_colorcalibration](iop/wb_and_colorcalibration/README.md).
+
+### 4b. Color-profile change (colorin / colorout)
+
+This follows the same flow as a slider adjust: `TOP_CHANGED` or `SYNCH`, depending on
+`focus_hash` and on whether it is an in-place update. There is no `REMOVE`, because the
+topology does not change. `commit_params()` rebuilds the color transforms; no cross-module
+signal is needed. Because the transform changes the pixels, all downstream cache hashes change
+and everything downstream reprocesses.
+
+---
+
+## 5. Pipeline ordering asymmetry
+
+Two operations walk the module list in **opposite** directions, which is a common source of
+confusion. (See also the
+[ordering-asymmetry note](pixelpipe_architecture.md#pipeline-ordering-asymmetry) in
+pixelpipe_architecture.md.)
+
+- **`commit_params()` (through `synch_all`)** iterates the history in history-list order, which
+ is effectively pipe order: an upstream module commits before a downstream one, so any state it
+ writes into shared structures is in place when the downstream module reads it.
+- **`_dt_dev_load_pipeline_defaults()`** iterates in **reverse** pipe order (last module first):
+
+ ```c
+ for(const GList *modules = g_list_last(dev->iop);
+ modules;
+ modules = g_list_previous(modules))
+ {
+ dt_iop_reload_defaults(modules->data);
+ }
+ ```
+
+ So a downstream module's `reload_defaults()` runs **before** an upstream one's. The source
+ documents no rationale for the direction.
+
+### Developer rule
+
+A module's `reload_defaults()` **cannot** assume that earlier-in-pipe modules have already
+populated shared state (such as `dev->chroma`), because under reverse iteration they have not run
+yet. Shared state that a `reload_defaults()` depends on must therefore be **reset to a neutral
+value before the reverse default-load** — which the framework does via `dt_dev_reset_chroma()`
+just before `_dt_dev_load_pipeline_defaults()`.
+
+The worked example — how white balance and color calibration share `dev->chroma`, and why this
+reset keeps color calibration's defaults image-local despite the reverse order — is in
+[wb_and_colorcalibration](iop/wb_and_colorcalibration/README.md).
+
+---
+
+## 6. Pipeline execution detail
+
+How pixels actually flow once the pipes are marked dirty:
+
+- `dt_dev_process_image_job()` (develop.c) is the entry point. It locks the pipe and captures
+ `dev->timestamp`.
+- `dt_dev_pixelpipe_process()` calls `_dev_pixelpipe_process_rec()` (pixelpipe_hb.c), which
+ recurses from the last module back toward the input, pulling each module's input on demand.
+- For each module, the cache key is the **cumulative hash** of all upstream `piece->hash`
+ values (each set in
+ [`dt_iop_commit_params()`](IOP_Module_API.md#commit_params---transform-ui-parameters-into-processing-data)
+ from op, instance, params, and blend_params),
+ plus the color-profile information. On a key match, the cached output is reused and
+ `process()` is skipped. On a miss, `process()` (or `process_cl()` on the GPU) runs and the
+ result is stored.
+- `modify_roi_in()` and `modify_roi_out()` run on every module during the ROI-propagation
+ phase, before processing, translating between the output-side and input-side regions. Under
+ the `ZOOMED` flag only the ROIs change — the module hashes do not — so cache hits are
+ possible for any module whose cached output region still covers the requested region.
+- The cache is a fixed-size LRU, and eviction is automatic. There is no explicit invalidation
+ on a param change; a changed hash simply misses.
+- After all modules finish, `pipe->status` becomes `VALID` and a finished signal (for example
+ `DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED`) triggers the GTK redraw.
+- The full and preview pipes run identical logic at different input resolutions and can run in
+ parallel on separate threads.
+
+---
+
+## Key symbols
+
+Grouped by file. Look these up by name; the surrounding block is described in the relevant
+section above.
+
+**develop.c**
+
+| Symbol | Role |
+|---|---|
+| `dt_dev_load_image` | Top-level image load |
+| `dt_dev_read_history_ext` | Loads defaults/presets and builds `dev->history` from the DB |
+| `_dt_dev_load_pipeline_defaults` | Calls `reload_defaults` on all modules, in reverse pipe order |
+| `dt_dev_reset_chroma` | Resets shared WB state to neutral before reverse default-load |
+| `dt_dev_add_history_item` / `_dev_add_history_item_ext` | Add/update a history entry; choose `TOP_CHANGED` vs `SYNCH` (the `focus_hash` test lives here) |
+| `dt_dev_pop_history_items` / `_ext` | Reset to defaults, then replay history (used by undo/redo) |
+| `dt_dev_reload_history_items` | Undo/redo and history-panel navigation |
+| `dt_dev_module_remove` | Removes a module instance and prunes its history entries |
+| `dt_dev_invalidate_all` | Marks all pipes dirty, bumps `dev->timestamp` |
+| `_dev_auto_apply_presets` | Auto-presets; gated on `DT_IMAGE_AUTO_PRESETS_APPLIED` |
+| `dt_dev_process_image_job` | Pixel-processing entry point |
+
+**imageop.c**
+
+| Symbol | Role |
+|---|---|
+| `dt_iop_reload_defaults` | Wrapper: calls the module's `reload_defaults()` then `dt_iop_load_default_params()` |
+| `dt_iop_load_default_params` | Copies `default_params` into `params` |
+| `dt_iop_commit_params` | Translates params into `piece->data`; sets `piece->hash` |
+| `dt_iop_gui_changed` | Slider/widget change entry point |
+| `_gui_off_callback` | Enable/disable toggle handler |
+| `_gui_reset_callback` | Reset-to-defaults handler |
+| `dt_iop_gui_duplicate` | Add a module instance |
+
+**pixelpipe_hb.c / .h**
+
+| Symbol | Role |
+|---|---|
+| `DT_DEV_PIPE_*` flags | Pipe change flags (defined in pixelpipe_hb.h) |
+| `dt_dev_pixelpipe_change` | Dispatches on the change flag |
+| `dt_dev_pixelpipe_synch_top` | Re-commits the top module only |
+| `dt_dev_pixelpipe_synch_all` | Re-commits all history items in order |
+| `_dev_pixelpipe_process_rec` | Recursive per-module processing |
+
+**Other**
+
+| Symbol | File | Role |
+|---|---|---|
+| Basic-hash helper (cumulative cache hash) | pixelpipe_cache.c | Builds the cache key from image id, profiles, and all modules up to a position |
+
+For the white-balance ↔ color-calibration symbols (`temperature.*` / `channelmixerrgb.*` and the
+`dev->chroma` helpers), see
+[wb_and_colorcalibration](iop/wb_and_colorcalibration/README.md#key-symbols).
+
+## See also
+
+- [IOP_Module_API.md](IOP_Module_API.md) — the callback API reference (signatures, the two
+ [`reload_defaults`](IOP_Module_API.md#reload_defaults---per-image-defaults) jobs).
+- [pixelpipe_architecture.md](pixelpipe_architecture.md) — pipeline architecture, including the
+ [ordering-asymmetry note](pixelpipe_architecture.md#pipeline-ordering-asymmetry).
+- [wb_and_colorcalibration](iop/wb_and_colorcalibration/README.md) — the white-balance ↔
+ color-calibration interaction through `dev->chroma` (worked example of the ordering principle).
diff --git a/dev-doc/README.md b/dev-doc/README.md
index 7c071ce43a7c..08acb26fa396 100644
--- a/dev-doc/README.md
+++ b/dev-doc/README.md
@@ -9,6 +9,7 @@ This guide covers building Image Operation (IOP) modules for darktable's darkroo
|------|-------------|
| **[IOP_Module_API.md](IOP_Module_API.md)** | Module API reference: params_t vs data_t, processing, commit_params, lifecycle |
| **[pixelpipe_architecture.md](pixelpipe_architecture.md)** | Pixelpipe data flow, caching, ROI, ordering asymmetry |
+| **[Module_Lifecycle.md](Module_Lifecycle.md)** | When callbacks fire: image load, edits, history replay, pipe flags, cross-module data flow |
| **[introspection.md](introspection.md)** | Introspection system for parameters and GUI |
| **[Shortcuts.md](Shortcuts.md)** | The Action/Shortcut system and `dt_action_def_t` |
| **[Module_Groups.md](Module_Groups.md)** | Module grouping explanation and `default_group()` |
diff --git a/dev-doc/pixelpipe_architecture.md b/dev-doc/pixelpipe_architecture.md
index d7f2728ebd0b..69085d7ae138 100644
--- a/dev-doc/pixelpipe_architecture.md
+++ b/dev-doc/pixelpipe_architecture.md
@@ -4,6 +4,10 @@ The **pixelpipe** is the core image processing engine of darktable. It is respon
## Core Structures
+### `dt_develop_t`
+Defined in `src/develop/develop.h`.
+This is the main darkroom session state — one per open image. Besides the pixelpipes (below) it holds the module list (`dev->iop`, a `GList` of `dt_iop_module_t`), the history stack (`dev->history` plus the `dev->history_end` cursor), and shared inter-module state such as `dev->chroma` (white-balance / chromatic-adaptation data passed from `temperature.c` to `channelmixerrgb.c`). Most lifecycle operations — image load, history replay, parameter commits — act on this structure. For the event-by-event walkthrough see [Module_Lifecycle.md](Module_Lifecycle.md).
+
### `dt_dev_pixelpipe_t`
Defined in `src/develop/pixelpipe_hb.h`.
This structure represents a single instance of a processing pipeline. A `dt_develop_t` (the main development state) holds several pipes:
@@ -105,9 +109,9 @@ Two key pipeline operations iterate modules in **different** orders:
- **`commit_params()`** runs in **forward** pipe order (e.g., temperature before channelmixerrgb). This is the normal processing direction.
- **`_dt_dev_load_pipeline_defaults()`** runs in **reverse** pipe order (e.g., channelmixerrgb before temperature). This happens during history reset and default loading.
-This asymmetry matters for modules that communicate via shared state. For example, `temperature.c` writes white balance coefficients into `dev->chroma.wb_coeffs`, and `channelmixerrgb.c` reads them during `commit_params()`. During forward processing, temperature commits first and the data is available. But during reverse-order default loading, channelmixerrgb runs first — before temperature has written its values. This caused a bug where stale values from a previous image or history state influenced the defaults.
+This asymmetry matters for modules that communicate via shared state. For example, `temperature.c` writes white balance coefficients into `dev->chroma.wb_coeffs`, and `channelmixerrgb.c` reads them during `commit_params()`. During forward processing, temperature commits first and the data is available. During reverse-order default loading, channelmixerrgb runs first — before temperature has refreshed its values — so any shared state a `reload_defaults()` depends on must be reset to a neutral value beforehand.
-**Consequence:** Shared state (like `dev->chroma`) must be properly reset before reverse-order iteration to prevent this class of ordering-dependent bug.
+**Consequence:** Shared state (like `dev->chroma`) must be reset before reverse-order iteration. The framework does this via `dt_dev_reset_chroma()` immediately before `_dt_dev_load_pipeline_defaults()`; the neutral `wb_coeffs` it writes is what keeps the dependent defaults image-local. For the full white-balance ↔ color-calibration interaction, see [iop/wb_and_colorcalibration](iop/wb_and_colorcalibration/README.md).
## Introspection Connection
diff --git a/src/common/exif.cc b/src/common/exif.cc
index ccc867df64a4..64f1cbebc3aa 100644
--- a/src/common/exif.cc
+++ b/src/common/exif.cc
@@ -2061,7 +2061,7 @@ static bool _exif_decode_exif_data(dt_image_t *img, Exiv2::ExifData &exifData)
dt_dng_illuminant_t illu[3] = { DT_LS_Unknown, DT_LS_Unknown, DT_LS_Unknown };
// make sure for later testing fallback later via
- // `find_temperature_from_raw_coeffs` if there is no valid illuminant
+ // `find_illuminant_xy_from_as_shot_coeffs` if there is no valid illuminant
dt_mark_colormatrix_invalid(&img->d65_color_matrix[0]);
#if EXIV2_TEST_VERSION(0,27,4)
diff --git a/src/common/illuminants.h b/src/common/illuminants.h
index 5f9530677c86..42f27f19d04e 100644
--- a/src/common/illuminants.h
+++ b/src/common/illuminants.h
@@ -34,6 +34,7 @@ typedef enum dt_illuminant_t
DT_ILLUMINANT_BB = 6, // $DESCRIPTION: "Planckian (black body)" general black body radiator - not CIE standard
DT_ILLUMINANT_CUSTOM = 7, // $DESCRIPTION: "custom" input x and y directly - bypass search
DT_ILLUMINANT_CAMERA = 10,// $DESCRIPTION: "as shot in camera" read RAW EXIF for WB
+ DT_ILLUMINANT_FROM_WB = 11,// $DESCRIPTION: "as set in white balance module" read coefficients from the white balance module
DT_ILLUMINANT_LAST,
DT_ILLUMINANT_DETECT_SURFACES = 8,
DT_ILLUMINANT_DETECT_EDGES = 9,
@@ -215,19 +216,23 @@ static inline void illuminant_CCT_to_RGB(const float t, dt_aligned_pixel_t RGB)
illuminant_xy_to_RGB(x, y, RGB);
}
+// Derive illuminant chromaticity from WB module coefficients
+static inline gboolean find_illuminant_xy_from_wb_coeffs(const dt_image_t *img, const dt_aligned_pixel_t wb_coeffs,
+ float *chroma_x, float *chroma_y);
// Fetch image from pipeline and read EXIF for camera RAW WB coeffs
-static inline gboolean find_temperature_from_raw_coeffs(const dt_image_t *img, const dt_aligned_pixel_t custom_wb,
+static inline gboolean find_illuminant_xy_from_as_shot_coeffs(const dt_image_t *img, const dt_aligned_pixel_t correction_ratios,
float *chroma_x, float *chroma_y);
-static inline int illuminant_to_xy(const dt_illuminant_t illuminant, // primary type of illuminant
- const dt_image_t *img, // image container
- const dt_aligned_pixel_t custom_wb, // optional user-set WB coeffs
- float *x_out, float *y_out, // chromaticity output
- const float t, // temperature in K, if needed
- const dt_illuminant_fluo_t fluo, // sub-type of fluorescent illuminant, if needed
- const dt_illuminant_led_t iled) // sub-type of led illuminant, if needed
+static inline int illuminant_to_xy(const dt_illuminant_t illuminant, // primary type of illuminant
+ const dt_image_t *img, // image container
+ const dt_aligned_pixel_t correction_ratios, // optional D65 correction ratios derived from user-set coefficients
+ const dt_aligned_pixel_t wb_coeffs, // optional user-set WB coeffs (absolute)
+ float *x_out, float *y_out, // chromaticity output
+ const float t, // temperature in K, if needed
+ const dt_illuminant_fluo_t fluo, // sub-type of fluorescent illuminant, if needed
+ const dt_illuminant_led_t iled) // sub-type of led illuminant, if needed
{
/**
* Compute the x and y chromaticity coordinates in Yxy spaces for standard illuminants
@@ -294,13 +299,26 @@ static inline int illuminant_to_xy(const dt_illuminant_t illuminant, // primary
// Model valid for T in [1667 ; 25000] K
CCT_to_xy_blackbody(t, &x, &y);
if(y != 0.f && x != 0.f) break;
- // else t is out of bounds -> use custom/original values (next case)
+ // else t is out of bounds -> use custom/original values
+ return FALSE;
}
case DT_ILLUMINANT_CAMERA:
{
- // Detect WB from RAW EXIF
- if(img)
- if(find_temperature_from_raw_coeffs(img, custom_wb, &x, &y)) break;
+ // Detect WB from RAW EXIF, correcting with D65/wb_coeff ratios
+ if(img && find_illuminant_xy_from_as_shot_coeffs(img, correction_ratios, &x, &y))
+ break;
+
+ // xy calculation failed
+ return FALSE;
+ }
+ case DT_ILLUMINANT_FROM_WB:
+ {
+ // Detect WB from user-provided coefficients
+ if(img && find_illuminant_xy_from_wb_coeffs(img, wb_coeffs, &x, &y))
+ break;
+
+ // xy calculation failed
+ return FALSE;
}
case DT_ILLUMINANT_CUSTOM: // leave x and y as-is
case DT_ILLUMINANT_DETECT_EDGES:
@@ -325,15 +343,15 @@ static inline int illuminant_to_xy(const dt_illuminant_t illuminant, // primary
static inline void WB_coeffs_to_illuminant_xy(const float CAM_to_XYZ[4][3], const dt_aligned_pixel_t WB,
float *x, float *y)
{
- // Find the illuminant chromaticity x y from RAW WB coeffs and camera input matrice
+ // Find the illuminant chromaticity x y from RAW WB coeffs and camera input matrix
dt_aligned_pixel_t XYZ, LMS;
// Simulate white point, aka convert (1, 1, 1) in camera space to XYZ
- // warning : we multiply the transpose of CAM_to_XYZ since the pseudoinverse transposes it
+ // warning: we multiply the transpose of CAM_to_XYZ since the pseudoinverse transposes it
XYZ[0] = CAM_to_XYZ[0][0] / WB[0] + CAM_to_XYZ[1][0] / WB[1] + CAM_to_XYZ[2][0] / WB[2];
XYZ[1] = CAM_to_XYZ[0][1] / WB[0] + CAM_to_XYZ[1][1] / WB[1] + CAM_to_XYZ[2][1] / WB[2];
XYZ[2] = CAM_to_XYZ[0][2] / WB[0] + CAM_to_XYZ[1][2] / WB[1] + CAM_to_XYZ[2][2] / WB[2];
- // Matrices white point is D65. We need to convert it for darktable's pipe (D50)
+ // Matrix's white point is D65. We need to convert it for darktable's pipe (D50)
static const dt_aligned_pixel_t D65 = { 0.941238f, 1.040633f, 1.088932f, 0.f };
const float p = powf(1.088932f / 0.818155f, 0.0834f);
@@ -385,31 +403,11 @@ static inline void matrice_pseudoinverse(float (*in)[3], float (*out)[3], int si
}
}
-
-static gboolean find_temperature_from_raw_coeffs(const dt_image_t *img, const dt_aligned_pixel_t custom_wb,
- float *chroma_x, float *chroma_y)
+// returns TRUE if OK, FALSE if failed
+static gboolean get_CAM_to_XYZ(const dt_image_t * img, float(* CAM_to_XYZ)[3])
{
- if(img == NULL) return FALSE;
- if(!dt_image_is_matrix_correction_supported(img)) return FALSE;
-
- gboolean has_valid_coeffs = TRUE;
- const int num_coeffs = (img->flags & DT_IMAGE_4BAYER) ? 4 : 3;
-
- // Check coeffs
- for(int k = 0; has_valid_coeffs && k < num_coeffs; k++)
- if(!dt_isnormal(img->wb_coeffs[k]) || img->wb_coeffs[k] == 0.0f) has_valid_coeffs = FALSE;
-
- if(!has_valid_coeffs) return FALSE;
-
- // Get white balance camera factors
- dt_aligned_pixel_t WB = { img->wb_coeffs[0], img->wb_coeffs[1], img->wb_coeffs[2], img->wb_coeffs[3] };
-
- // Adapt the camera coeffs with custom white balance if provided
- // this can deal with WB coeffs that don't use the input matrix reference
- if(custom_wb)
- for(size_t k = 0; k < 4; k++) WB[k] *= custom_wb[k];
-
- // Get the camera input profile (matrice of primaries)
+ if(img == NULL || CAM_to_XYZ == NULL) return FALSE;
+ // Get the camera input profile (matrice of primaries) - embedded or raw library DB
float XYZ_to_CAM[4][3];
dt_mark_colormatrix_invalid(&XYZ_to_CAM[0][0]);
@@ -438,21 +436,62 @@ static gboolean find_temperature_from_raw_coeffs(const dt_image_t *img, const dt
if(!dt_is_valid_colormatrix(XYZ_to_CAM[0][0])) return FALSE;
- // Bloody input matrices define XYZ -> CAM transform, as if we often needed camera profiles to output
- // So we need to invert them. Here go your CPU cycles again.
- float CAM_to_XYZ[4][3];
dt_mark_colormatrix_invalid(&CAM_to_XYZ[0][0]);
matrice_pseudoinverse(XYZ_to_CAM, CAM_to_XYZ, 3);
- if(!dt_is_valid_colormatrix(CAM_to_XYZ[0][0])) return FALSE;
+ return dt_is_valid_colormatrix(CAM_to_XYZ[0][0]);
+}
- float x, y;
- WB_coeffs_to_illuminant_xy(CAM_to_XYZ, WB, &x, &y);
- *chroma_x = x;
- *chroma_y = y;
+static inline gboolean _wb_coeffs_invalid(const dt_aligned_pixel_t wb_coeffs, const int num_coeffs)
+{
+ for(int k = 0; k < num_coeffs; k++)
+ if(!dt_isnormal(wb_coeffs[k])) return TRUE;
+
+ return FALSE;
+}
+
+// returns FALSE if failed; TRUE if successful
+static gboolean find_illuminant_xy_from_wb_coeffs(const dt_image_t *img, const dt_aligned_pixel_t wb_coeffs,
+ float *chroma_x, float *chroma_y)
+{
+ if(img == NULL || wb_coeffs == NULL) return FALSE;
+ if(!dt_image_is_matrix_correction_supported(img)) return FALSE;
+ // it's enough to check the first 3 here, as WB_coeffs_to_illuminant_xy only uses indices 0 to 2
+ if(_wb_coeffs_invalid(wb_coeffs, 3)) return FALSE;
+
+ float CAM_to_XYZ[4][3];
+ if(!get_CAM_to_XYZ(img, CAM_to_XYZ)) {
+ return FALSE;
+ }
+
+ WB_coeffs_to_illuminant_xy(CAM_to_XYZ, wb_coeffs, chroma_x, chroma_y);
return TRUE;
}
+// returns FALSE if failed; TRUE if successful
+static gboolean find_illuminant_xy_from_as_shot_coeffs(const dt_image_t *img, const dt_aligned_pixel_t correction_ratios,
+ float *chroma_x, float *chroma_y)
+{
+ if(img == NULL) return FALSE;
+ const int num_coeffs = (img->flags & DT_IMAGE_4BAYER) ? 4 : 3;
+
+ if(_wb_coeffs_invalid(img->wb_coeffs, num_coeffs)) return FALSE;
+
+ // Get as-shot white balance camera factors (from raw)
+ // component wise raw-RGB * wb_coeffs should provide R=G=B for a neutral patch under
+ // the scene illuminant
+ dt_aligned_pixel_t WB = { img->wb_coeffs[0], img->wb_coeffs[1], img->wb_coeffs[2], img->wb_coeffs[3] };
+
+ // Adapt the camera coeffs with custom D65 coefficients if provided ('caveats' workaround)
+ // this can deal with WB coeffs that don't use the input matrix reference
+ // correction_ratios[k] = chr->D65coeffs[k] / chr->wb_coeffs[k]
+ if(correction_ratios)
+ for(size_t k = 0; k < 4; k++) WB[k] *= correction_ratios[k];
+ // for a neutral surface, raw RGB * img->wb_coeffs would produce neutral R=G=B
+
+ return find_illuminant_xy_from_wb_coeffs(img, WB, chroma_x, chroma_y);
+}
+
DT_OMP_DECLARE_SIMD()
static inline float planckian_normal(const float x, const float t)
@@ -471,31 +510,6 @@ static inline float planckian_normal(const float x, const float t)
}
-DT_OMP_DECLARE_SIMD()
-static inline void blackbody_xy_to_tinted_xy(const float x, const float y, const float t, const float tint,
- float *x_out, float *y_out)
-{
- // Move further away from planckian locus in the orthogonal direction, by an amount of "tint"
- const float n = planckian_normal(x, t);
- const float norm = sqrtf(1.f + n * n);
- *x_out = x + tint * n / norm;
- *y_out = y - tint / norm;
-}
-
-
-DT_OMP_DECLARE_SIMD()
-static inline float get_tint_from_tinted_xy(const float x, const float y, const float t)
-{
- // Find the distance between planckian locus and arbitrary x y chromaticity in the orthogonal direction
- const float n = planckian_normal(x, t);
- const float norm = sqrtf(1.f + n * n);
- float x_bb, y_bb;
- CCT_to_xy_blackbody(t, &x_bb, &y_bb);
- const float tint = -(y - y_bb) * norm;
- return tint;
-}
-
-
DT_OMP_DECLARE_SIMD()
static inline void xy_to_uv(const float xy[2], float uv[2])
{
diff --git a/src/develop/develop.h b/src/develop/develop.h
index 99f5d610f634..8ef369fbf171 100644
--- a/src/develop/develop.h
+++ b/src/develop/develop.h
@@ -147,9 +147,9 @@ typedef struct dt_dev_viewport_t
- the currently used wb_coeffs in temperature module
- D65coeffs and as_shot are read from exif data
f) - late_correction set by temperature if we want to process data as following
- If we use the new DT_IOP_TEMP_D65_LATE mode in temperature.c and don#t have
- any temp parameters changes later we can calc correction coeffs to modify
- as_shot rgb data to D65
+ If we enable this for non-D65 modes of temperature.c and don't have any
+ temp parameters changes later we can calc correction ratios
+ to take white-balanced (neutralized) data to D65
*/
typedef struct dt_dev_chroma_t
{
diff --git a/src/iop/channelmixerrgb.c b/src/iop/channelmixerrgb.c
index 87e8c003fa05..da2b48b0cf1b 100644
--- a/src/iop/channelmixerrgb.c
+++ b/src/iop/channelmixerrgb.c
@@ -148,8 +148,8 @@ typedef struct dt_iop_channelmixer_rgb_gui_data_t
GtkWidget *scale_grey_R, *scale_grey_G, *scale_grey_B;
GtkWidget *normalize_R, *normalize_G, *normalize_B, *normalize_sat, *normalize_light, *normalize_grey;
GtkWidget *color_picker;
- float xy[2];
- float XYZ[4];
+ float colorchecker_xy[2];
+ float ai_wb_XYZ[4];
point_t box[4]; // the current coordinates, possibly non rectangle, of the bounding box for the color checker
point_t ideal_box[4]; // the desired coordinates of the perfect rectangle bounding box for the color checker
@@ -422,7 +422,7 @@ void init_presets(dt_iop_module_so_t *self)
p.illum_fluo = DT_ILLUMINANT_FLUO_F3;
p.illum_led = DT_ILLUMINANT_LED_B5;
p.temperature = 5003.f;
- illuminant_to_xy(DT_ILLUMINANT_PIPE, NULL, NULL, &p.x, &p.y, p.temperature,
+ illuminant_to_xy(DT_ILLUMINANT_PIPE, NULL, NULL, NULL, &p.x, &p.y, p.temperature,
DT_ILLUMINANT_FLUO_LAST, DT_ILLUMINANT_LED_LAST);
p.red[0] = 1.f;
@@ -593,9 +593,7 @@ void init_presets(dt_iop_module_so_t *self)
static gboolean _dev_is_D65_chroma(const dt_develop_t *dev)
{
const dt_dev_chroma_t *chr = &dev->chroma;
- return chr->late_correction
- ? dt_dev_equal_chroma(chr->wb_coeffs, chr->as_shot)
- : dt_dev_equal_chroma(chr->wb_coeffs, chr->D65coeffs);
+ return chr->late_correction || dt_dev_equal_chroma(chr->wb_coeffs, chr->D65coeffs);
}
static gboolean _area_mapping_active(const dt_iop_channelmixer_rgb_gui_data_t *g)
@@ -611,14 +609,14 @@ static const char *_area_mapping_section_text(const dt_iop_channelmixer_rgb_gui_
return _area_mapping_active(g) ? _("area color mapping (active)") : _("area color mapping");
}
-static gboolean _get_white_balance_coeff(const dt_iop_module_t *self,
- dt_aligned_pixel_t custom_wb)
+static gboolean _get_d65_correction_ratios(const dt_iop_module_t *self,
+ dt_aligned_pixel_t out_correction_ratios)
{
const dt_dev_chroma_t *chr = &self->dev->chroma;
// Init output with a no-op
for_four_channels(k)
- custom_wb[k] = 1.0f;
+ out_correction_ratios[k] = 1.0f;
if(!dt_image_is_matrix_correction_supported(&self->dev->image_storage))
return TRUE;
@@ -638,11 +636,33 @@ static gboolean _get_white_balance_coeff(const dt_iop_module_t *self,
if(valid_chroma && changed_chroma)
{
for_four_channels(k)
- custom_wb[k] = (float)chr->D65coeffs[k] / chr->wb_coeffs[k];
+ {
+ if(chr->wb_coeffs[k] > 1e-6f)
+ out_correction_ratios[k] = chr->D65coeffs[k] / chr->wb_coeffs[k];
+ else
+ out_correction_ratios[k] = 1.0f;
+ }
}
return FALSE;
}
+static void _get_corrected_illuminant_xy(const dt_iop_module_t *self,
+ const dt_iop_channelmixer_rgb_params_t *p,
+ float *x, float *y)
+{
+ dt_aligned_pixel_t correction_ratios;
+ _get_d65_correction_ratios(self, correction_ratios);
+ dt_aligned_pixel_t wb_coeffs = { 0.f };
+ if(p->illuminant == DT_ILLUMINANT_FROM_WB)
+ {
+ for(int k = 0; k < 4; k++)
+ wb_coeffs[k] = self->dev->chroma.wb_coeffs[k];
+ }
+ illuminant_to_xy(p->illuminant, &(self->dev->image_storage), correction_ratios,
+ wb_coeffs, x, y, p->temperature, p->illum_fluo,
+ p->illum_led);
+}
+
DT_OMP_DECLARE_SIMD(aligned(input, output:16) uniform(compression, clip))
static inline void _gamut_mapping(const dt_aligned_pixel_t input,
@@ -1246,7 +1266,7 @@ static void _check_if_close_to_daylight(const float x,
float uv_test[2];
// Compute the test chromaticity from the daylight model
- illuminant_to_xy(DT_ILLUMINANT_D, NULL, NULL, &xy_test[0], &xy_test[1], t,
+ illuminant_to_xy(DT_ILLUMINANT_D, NULL, NULL, NULL, &xy_test[0], &xy_test[1], t,
DT_ILLUMINANT_FLUO_LAST, DT_ILLUMINANT_LED_LAST);
xy_to_uv(xy_test, uv_test);
@@ -1255,7 +1275,7 @@ static void _check_if_close_to_daylight(const float x,
const float delta_daylight = dt_fast_hypotf(uv_test[0] - uv_ref[0], uv_test[1] - uv_ref[1]);
// Compute the test chromaticity from the blackbody model
- illuminant_to_xy(DT_ILLUMINANT_BB, NULL, NULL, &xy_test[0], &xy_test[1], t,
+ illuminant_to_xy(DT_ILLUMINANT_BB, NULL, NULL, NULL, &xy_test[0], &xy_test[1], t,
DT_ILLUMINANT_FLUO_LAST, DT_ILLUMINANT_LED_LAST);
xy_to_uv(xy_test, uv_test);
@@ -1704,8 +1724,8 @@ static void _extract_color_checker(const float *const restrict in,
dt_D50_XYZ_to_xyY(illuminant_XYZ, illuminant_xyY);
// save the illuminant in GUI struct for commit
- g->xy[0] = illuminant_xyY[0];
- g->xy[1] = illuminant_xyY[1];
+ g->colorchecker_xy[0] = illuminant_xyY[0];
+ g->colorchecker_xy[1] = illuminant_xyY[1];
// and recompute back the LMS to be sure we use the parameters that
// will be computed later
@@ -1985,20 +2005,27 @@ static void _set_trouble_messages(dt_iop_module_t *self)
const dt_develop_t *dev = self->dev;
const dt_dev_chroma_t *chr = &dev->chroma;
+ // module handle snapshots: dt_dev_reset_chroma() may NULL them
+ // from the pipe worker thread. single aligned pointer loads can't tear;
+ // the pointed-to modules are only freed on this (main) thread,
+ // so the snapshots stay valid for this call
+ dt_iop_module_t *const temperature = chr->temperature;
+ dt_iop_module_t *const adaptation = chr->adaptation;
+
dt_print(DT_DEBUG_PIPE | DT_DEBUG_VERBOSE, "trouble message for %s%s : temp=%p adapt=%p",
- self->op, dt_iop_get_instance_id(self), chr->temperature, chr->adaptation);
+ self->op, dt_iop_get_instance_id(self), temperature, adaptation);
// in temperature module we make sure this is only presented if temperature is enabled
- if(!chr->temperature)
+ if(!temperature)
{
- if(chr->adaptation)
- dt_iop_set_module_trouble_message(chr->adaptation, NULL, NULL, NULL);
+ if(adaptation)
+ dt_iop_set_module_trouble_message(adaptation, NULL, NULL, NULL);
return;
}
- if(!chr->adaptation)
+ if(!adaptation)
{
- dt_iop_set_module_trouble_message(chr->temperature, NULL, NULL, NULL);
+ dt_iop_set_module_trouble_message(temperature, NULL, NULL, NULL);
dt_iop_set_module_trouble_message(self, NULL, NULL, NULL);
return;
}
@@ -2008,46 +2035,46 @@ static void _set_trouble_messages(dt_iop_module_t *self)
&& !(p->illuminant == DT_ILLUMINANT_PIPE || p->adaptation == DT_ADAPTATION_RGB)
&& !dt_image_is_monochrome(&dev->image_storage);
- const gboolean temperature_enabled = chr->temperature && chr->temperature->enabled;
- const gboolean adaptation_enabled = chr->adaptation && chr->adaptation->enabled;
+ const gboolean temperature_enabled = temperature->enabled;
+ const gboolean adaptation_enabled = adaptation->enabled;
- // our first and biggest problem : white balance module is being
- // clever with WB coeffs
- const gboolean problem1 = valid
- && chr->adaptation == self
+ // this instance does CAT while WB uses an incompatible setup (neither camera reference
+ // D65, nor another mode with late_correction)
+ const gboolean wb_applied_twice = valid
+ && adaptation == self
&& temperature_enabled
&& !_dev_is_D65_chroma(dev);
- // our second biggest problem : another channelmixerrgb instance is doing CAT
- // earlier in the pipe and we don't use masking here.
- const gboolean problem2 = valid
+ // another channelmixerrgb instance is doing CAT
+ // earlier in the pipe, and we don't use masking here
+ const gboolean double_cat = valid
&& adaptation_enabled
&& temperature_enabled
- && chr->adaptation != self
+ && adaptation != self
&& !g->is_blending;
- // our third and minor problem: white balance module is not active but default_enabled
- // and we do chromatic adaptation in color calibration
- const gboolean problem3 = valid
+ // white balance is off on an image that needs it (default_enabled), but we rely on
+ // it to pre-process the data (neutralizing coeffs + late_correction, or D65 coeffs)
+ const gboolean wb_missing = valid
&& adaptation_enabled
&& !temperature_enabled
- && chr->temperature->default_enabled;
+ && temperature->default_enabled;
- const gboolean anyproblem = problem1 || problem2 || problem3;
+ const gboolean any_problem = wb_applied_twice || double_cat || wb_missing;
const dt_image_t *img = &dev->image_storage;
- dt_print_pipe(DT_DEBUG_PIPE, anyproblem ? "chroma trouble" : "chroma data",
+ dt_print_pipe(DT_DEBUG_PIPE, any_problem ? "chroma trouble" : "chroma data",
NULL, self, DT_DEVICE_NONE, NULL, NULL,
"%s%s%sD65=%s. D65 %.3f %.3f %.3f, AS-SHOT %.3f %.3f %.3f ID=%i",
- problem1 ? "white balance applied twice, " : "",
- problem2 ? "double CAT applied, " : "",
- problem3 ? "white balance missing, " : "",
+ wb_applied_twice ? "white balance applied twice, " : "",
+ double_cat ? "double CAT applied, " : "",
+ wb_missing ? "white balance missing, " : "",
STR_YESNO(_dev_is_D65_chroma(dev)),
chr->D65coeffs[0], chr->D65coeffs[1], chr->D65coeffs[2],
chr->as_shot[0], chr->as_shot[1], chr->as_shot[2],
img->id);
- if(problem2)
+ if(double_cat)
{
dt_iop_set_module_trouble_message
(self,
@@ -2060,10 +2087,10 @@ static void _set_trouble_messages(dt_iop_module_t *self)
return;
}
- if(problem1)
+ if(wb_applied_twice)
{
dt_iop_set_module_trouble_message
- (chr->temperature,
+ (temperature,
_("white balance applied twice (details)"),
_("the color calibration module is enabled and already provides\n"
"chromatic adaptation.\n"
@@ -2082,10 +2109,10 @@ static void _set_trouble_messages(dt_iop_module_t *self)
return;
}
- if(problem3)
+ if(wb_missing)
{
dt_iop_set_module_trouble_message
- (chr->temperature,
+ (temperature,
_("white balance missing (details)"),
_("this module is not providing a valid reference illuminant\n"
"causing chromatic adaptation issues in color calibration.\n"
@@ -2104,9 +2131,9 @@ static void _set_trouble_messages(dt_iop_module_t *self)
return;
}
- if(chr->adaptation && chr->adaptation == self)
+ if(adaptation == self)
{
- dt_iop_set_module_trouble_message(chr->temperature, NULL, NULL, NULL);
+ dt_iop_set_module_trouble_message(temperature, NULL, NULL, NULL);
dt_iop_set_module_trouble_message(self, NULL, NULL, NULL);
}
}
@@ -2180,7 +2207,7 @@ void process(dt_iop_module_t *self,
// as scratch space since we will be overwriting it afterwards
// anyway
_auto_detect_WB(in, out, data->illuminant_type, roi_in->width, roi_in->height,
- ch, RGB_to_XYZ, g->XYZ);
+ ch, RGB_to_XYZ, g->ai_wb_XYZ);
dt_dev_pixelpipe_cache_invalidate_later(piece->pipe, self->iop_order, "AI RGB: ");
dt_iop_gui_leave_critical_section(self);
}
@@ -2208,10 +2235,27 @@ void process(dt_iop_module_t *self,
// change here, so we can't update the defaults. So we need to
// re-run the detection at runtime…
float x, y;
- dt_aligned_pixel_t custom_wb;
- _get_white_balance_coeff(self, custom_wb);
+ dt_aligned_pixel_t correction_ratios;
+ _get_d65_correction_ratios(self, correction_ratios);
- if(find_temperature_from_raw_coeffs(&(self->dev->image_storage), custom_wb, &(x), &(y)))
+ if(find_illuminant_xy_from_as_shot_coeffs(&(self->dev->image_storage), correction_ratios, &x, &y))
+ {
+ // Convert illuminant from xyY to XYZ
+ dt_aligned_pixel_t XYZ;
+ illuminant_xy_to_XYZ(x, y, XYZ);
+
+ // Convert illuminant from XYZ to Bradford modified LMS
+ convert_any_XYZ_to_LMS(XYZ, data->illuminant, data->adaptation);
+ data->illuminant[3] = 0.f;
+ }
+ }
+ else if(data->illuminant_type == DT_ILLUMINANT_FROM_WB)
+ {
+ // Same logic as above (we depend on the coefficients from white balance,
+ // which don't come from the EXIF this time).
+ float x, y;
+
+ if(find_illuminant_xy_from_wb_coeffs(&(self->dev->image_storage), self->dev->chroma.wb_coeffs, &x, &y))
{
// Convert illuminant from xyY to XYZ
dt_aligned_pixel_t XYZ;
@@ -2319,10 +2363,27 @@ int process_cl(dt_iop_module_t *self,
// change here, so we can't update the defaults. So we need to
// re-run the detection at runtime…
float x, y;
- dt_aligned_pixel_t custom_wb;
- _get_white_balance_coeff(self, custom_wb);
+ dt_aligned_pixel_t correction_ratios;
+ _get_d65_correction_ratios(self, correction_ratios);
+
+ if(find_illuminant_xy_from_as_shot_coeffs(&(self->dev->image_storage), correction_ratios, &x, &y))
+ {
+ // Convert illuminant from xyY to XYZ
+ dt_aligned_pixel_t XYZ;
+ illuminant_xy_to_XYZ(x, y, XYZ);
+
+ // Convert illuminant from XYZ to Bradford modified LMS
+ convert_any_XYZ_to_LMS(XYZ, d->illuminant, d->adaptation);
+ d->illuminant[3] = 0.f;
+ }
+ }
+ else if(d->illuminant_type == DT_ILLUMINANT_FROM_WB)
+ {
+ // Same logic as above (we depend on the coefficients from white balance,
+ // which don't come from the EXIF this time).
+ float x, y;
- if(find_temperature_from_raw_coeffs(&(self->dev->image_storage), custom_wb, &(x), &(y)))
+ if(find_illuminant_xy_from_wb_coeffs(&(self->dev->image_storage), self->dev->chroma.wb_coeffs, &x, &y))
{
// Convert illuminant from xyY to XYZ
dt_aligned_pixel_t XYZ;
@@ -2941,8 +3002,8 @@ static void _commit_profile_callback(GtkWidget *widget,
dt_iop_gui_enter_critical_section(self);
- p->x = g->xy[0];
- p->y = g->xy[1];
+ p->x = g->colorchecker_xy[0];
+ p->y = g->colorchecker_xy[1];
p->illuminant = DT_ILLUMINANT_CUSTOM;
_check_if_close_to_daylight(p->x, p->y, &p->temperature, NULL, NULL);
@@ -3002,8 +3063,8 @@ static void _develop_ui_pipe_finished_callback(gpointer instance, dt_iop_module_
return;
dt_iop_gui_enter_critical_section(self);
- p->x = g->XYZ[0];
- p->y = g->XYZ[1];
+ p->x = g->ai_wb_XYZ[0];
+ p->y = g->ai_wb_XYZ[1];
dt_iop_gui_leave_critical_section(self);
_check_if_close_to_daylight(p->x, p->y,
@@ -3040,6 +3101,31 @@ static void _preview_pipe_finished_callback(gpointer instance, dt_iop_module_t *
dt_iop_gui_enter_critical_section(self);
gtk_label_set_markup(GTK_LABEL(g->label_delta_E), g->delta_E_label_text);
+
+ dt_iop_channelmixer_rgb_params_t *p = self->params;
+ if(p->illuminant == DT_ILLUMINANT_FROM_WB)
+ {
+ // WB coefficients might have changed temperature.c; recalculate xy and update the GUI
+ float x, y;
+
+ if(find_illuminant_xy_from_wb_coeffs(&(self->dev->image_storage), self->dev->chroma.wb_coeffs, &x, &y))
+ {
+ DT_ENTER_GUI_UPDATE();
+ _update_approx_cct(self);
+ _update_illuminant_color(self);
+
+ dt_aligned_pixel_t xyY = { x, y, 1.f };
+ dt_aligned_pixel_t Lch;
+ dt_xyY_to_Lch(xyY, Lch);
+
+ if(Lch[1] > 0)
+ dt_bauhaus_slider_set(g->illum_x, rad2degf(Lch[2]));
+ dt_bauhaus_slider_set(g->illum_y, Lch[1]);
+
+ DT_LEAVE_GUI_UPDATE();
+ }
+ }
+
dt_iop_gui_leave_critical_section(self);
_set_trouble_messages(self);
}
@@ -3110,16 +3196,12 @@ void commit_params(dt_iop_module_t *self,
// find x y coordinates of illuminant for CIE 1931 2° observer
float x = p->x;
float y = p->y;
- dt_aligned_pixel_t custom_wb;
- _get_white_balance_coeff(self, custom_wb);
- illuminant_to_xy(p->illuminant, &(self->dev->image_storage),
- custom_wb, &x, &y, p->temperature, p->illum_fluo, p->illum_led);
+ _get_corrected_illuminant_xy(self, p, &x, &y);
- // if illuminant is set as camera, x and y are set on-the-fly at
+ // if illuminant is from camera or user WB coefficients, x and y are set on-the-fly at
// commit time, so we need to set adaptation too
- if(p->illuminant == DT_ILLUMINANT_CAMERA)
+ if(p->illuminant == DT_ILLUMINANT_CAMERA || p->illuminant == DT_ILLUMINANT_FROM_WB)
_check_if_close_to_daylight(x, y, NULL, NULL, &(d->adaptation));
-
d->illuminant_type = p->illuminant;
// Convert illuminant from xyY to XYZ
@@ -3262,6 +3344,7 @@ static void _update_illuminants(const dt_iop_module_t *self)
break;
}
case DT_ILLUMINANT_CAMERA:
+ case DT_ILLUMINANT_FROM_WB:
{
gtk_widget_set_visible(g->adaptation, TRUE);
gtk_widget_set_visible(g->temperature, FALSE);
@@ -3554,11 +3637,8 @@ static gboolean _illuminant_color_draw(GtkWidget *widget,
// camera RAW is chosen
float x = p->x;
float y = p->y;
- dt_aligned_pixel_t RGB = { 0 };
- dt_aligned_pixel_t custom_wb;
- _get_white_balance_coeff(self, custom_wb);
- illuminant_to_xy(p->illuminant, &(self->dev->image_storage), custom_wb,
- &x, &y, p->temperature, p->illum_fluo, p->illum_led);
+ dt_aligned_pixel_t RGB = { 0.f };
+ _get_corrected_illuminant_xy(self, p, &x, &y);
illuminant_xy_to_RGB(x, y, RGB);
cairo_set_source_rgb(cr, RGB[0], RGB[1], RGB[2]);
cairo_rectangle(cr, INNER_PADDING, margin, width, height);
@@ -3658,10 +3738,7 @@ static void _update_approx_cct(const dt_iop_module_t *self)
float x = p->x;
float y = p->y;
- dt_aligned_pixel_t custom_wb;
- _get_white_balance_coeff(self, custom_wb);
- illuminant_to_xy(p->illuminant, &(self->dev->image_storage),
- custom_wb, &x, &y, p->temperature, p->illum_fluo, p->illum_led);
+ _get_corrected_illuminant_xy(self, p, &x, &y);
dt_illuminant_t test_illuminant;
float t = 5000.f;
@@ -3859,6 +3936,24 @@ void init(dt_iop_module_t *self)
d->red[0] = d->green[1] = d->blue[2] = 1.0;
}
+static void _add_or_remove_illuminant(const dt_iop_module_t *self,
+ const int illuminant,
+ const gboolean use_illuminant)
+{
+ // we only call this if g is not NULL
+ dt_iop_channelmixer_rgb_gui_data_t *g = self->gui_data;
+ const int pos = dt_bauhaus_combobox_get_from_value(g->illuminant, illuminant);
+ if(use_illuminant)
+ {
+ if(pos == -1)
+ dt_bauhaus_combobox_add_introspection(g->illuminant, NULL,
+ self->so->get_f("illuminant")->Enum.values,
+ illuminant, illuminant);
+ }
+ else if (pos != -1)
+ dt_bauhaus_combobox_remove_at(g->illuminant, pos);
+}
+
void reload_defaults(dt_iop_module_t *self)
{
dt_iop_channelmixer_rgb_params_t *d = self->default_params;
@@ -3895,10 +3990,10 @@ void reload_defaults(dt_iop_module_t *self)
{
d->adaptation = DT_ADAPTATION_CAT16;
- dt_aligned_pixel_t custom_wb;
- if(!_get_white_balance_coeff(self, custom_wb))
+ dt_aligned_pixel_t correction_ratios;
+ if(!_get_d65_correction_ratios(self, correction_ratios))
{
- if(find_temperature_from_raw_coeffs(img, custom_wb, &(d->x), &(d->y)))
+ if(find_illuminant_xy_from_as_shot_coeffs(img, correction_ratios, &(d->x), &(d->y)))
d->illuminant = DT_ILLUMINANT_CAMERA;
_check_if_close_to_daylight(d->x, d->y,
&(d->temperature), &(d->illuminant), &(d->adaptation));
@@ -3926,16 +4021,9 @@ void reload_defaults(dt_iop_module_t *self)
g->delta_E_label_text = NULL;
}
- const int pos = dt_bauhaus_combobox_get_from_value(g->illuminant, DT_ILLUMINANT_CAMERA);
- if(dt_image_is_matrix_correction_supported(img) && !dt_image_is_monochrome(img))
- {
- if(pos == -1)
- dt_bauhaus_combobox_add_introspection(g->illuminant, NULL,
- self->so->get_f("illuminant")->Enum.values,
- DT_ILLUMINANT_CAMERA, DT_ILLUMINANT_CAMERA);
- }
- else
- dt_bauhaus_combobox_remove_at(g->illuminant, pos);
+ const gboolean has_color_matrix = dt_image_is_matrix_correction_supported(img) && !dt_image_is_monochrome(img);
+ _add_or_remove_illuminant(self, DT_ILLUMINANT_CAMERA, has_color_matrix);
+ _add_or_remove_illuminant(self, DT_ILLUMINANT_FROM_WB, has_color_matrix);
gui_changed(self, NULL, NULL);
}
@@ -3999,9 +4087,19 @@ void gui_changed(dt_iop_module_t *self,
// illuminant = "as set in camera", temperature and
// chromaticity are inited with the preset content when
// illuminant is changed.
- dt_aligned_pixel_t custom_wb;
- _get_white_balance_coeff(self, custom_wb);
- find_temperature_from_raw_coeffs(&(self->dev->image_storage), custom_wb,
+ dt_aligned_pixel_t correction_ratios;
+ _get_d65_correction_ratios(self, correction_ratios);
+ find_illuminant_xy_from_as_shot_coeffs(&(self->dev->image_storage), correction_ratios,
+ &(p->x), &(p->y));
+ _check_if_close_to_daylight(p->x, p->y, &(p->temperature), NULL, &(p->adaptation));
+ }
+ if(*prev_illuminant == DT_ILLUMINANT_FROM_WB)
+ {
+ // If illuminant was previously "as set in white balance module",
+ // (x, y) were computed at runtime from the WB module's coefficients
+ // and p->x, p->y may hold stale values. Recompute them now so
+ // the new illuminant mode starts from the correct chromaticity.
+ find_illuminant_xy_from_wb_coeffs(&(self->dev->image_storage), self->dev->chroma.wb_coeffs,
&(p->x), &(p->y));
_check_if_close_to_daylight(p->x, p->y, &(p->temperature), NULL, &(p->adaptation));
}
@@ -4016,15 +4114,24 @@ void gui_changed(dt_iop_module_t *self,
if(p->illuminant == DT_ILLUMINANT_CAMERA)
{
// Get camera WB and update illuminant
- dt_aligned_pixel_t custom_wb;
- _get_white_balance_coeff(self, custom_wb);
- const gboolean found = find_temperature_from_raw_coeffs(&(self->dev->image_storage),
- custom_wb, &(p->x), &(p->y));
+ dt_aligned_pixel_t correction_ratios;
+ _get_d65_correction_ratios(self, correction_ratios);
+ const gboolean found = find_illuminant_xy_from_as_shot_coeffs(&(self->dev->image_storage),
+ correction_ratios, &(p->x), &(p->y));
_check_if_close_to_daylight(p->x, p->y, &(p->temperature), NULL, &(p->adaptation));
if(found)
dt_control_log(_("white balance successfully extracted from raw image"));
}
+ else if(p->illuminant == DT_ILLUMINANT_FROM_WB)
+ {
+ const gboolean found = find_illuminant_xy_from_wb_coeffs(&(self->dev->image_storage),
+ self->dev->chroma.wb_coeffs, &(p->x), &(p->y));
+ _check_if_close_to_daylight(p->x, p->y, &(p->temperature), NULL, &(p->adaptation));
+
+ if(found)
+ dt_control_log(_("white balance successfully extracted from white balance module"));
+ }
#ifdef AI_ACTIVATED
else if(p->illuminant == DT_ILLUMINANT_DETECT_EDGES
|| p->illuminant == DT_ILLUMINANT_DETECT_SURFACES)
@@ -4052,17 +4159,19 @@ void gui_changed(dt_iop_module_t *self,
// illuminant to allow swapping modes
if(p->illuminant != DT_ILLUMINANT_CUSTOM
- && p->illuminant != DT_ILLUMINANT_CAMERA)
+ && p->illuminant != DT_ILLUMINANT_CAMERA
+ && p->illuminant != DT_ILLUMINANT_FROM_WB)
{
// We are in any mode defining (x, y) indirectly from an
// interface, so commit (x, y) explicitly
- illuminant_to_xy(p->illuminant, NULL, NULL, &(p->x), &(p->y),
+ illuminant_to_xy(p->illuminant, NULL, NULL, NULL, &(p->x), &(p->y),
p->temperature, p->illum_fluo, p->illum_led);
}
if(p->illuminant != DT_ILLUMINANT_D
&& p->illuminant != DT_ILLUMINANT_BB
- && p->illuminant != DT_ILLUMINANT_CAMERA)
+ && p->illuminant != DT_ILLUMINANT_CAMERA
+ && p->illuminant != DT_ILLUMINANT_FROM_WB)
{
// We are in any mode not defining explicitly a temperature, so
// find the the closest CCT and commit it
@@ -4146,7 +4255,7 @@ void gui_changed(dt_iop_module_t *self,
// If "as shot in camera" illuminant is used, CAT space is forced automatically
// therefore, make the control insensitive
- gtk_widget_set_sensitive(g->adaptation, p->illuminant != DT_ILLUMINANT_CAMERA);
+ gtk_widget_set_sensitive(g->adaptation, p->illuminant != DT_ILLUMINANT_CAMERA && p->illuminant != DT_ILLUMINANT_FROM_WB);
_declare_cat_on_pipe(self, FALSE);
@@ -4223,15 +4332,13 @@ static void _auto_set_illuminant(dt_iop_module_t *self,
// find x y coordinates of illuminant for CIE 1931 2° observer
float x = p->x;
float y = p->y;
+ _get_corrected_illuminant_xy(self, p, &x, &y);
+
dt_adaptation_t adaptation = p->adaptation;
- dt_aligned_pixel_t custom_wb;
- _get_white_balance_coeff(self, custom_wb);
- illuminant_to_xy(p->illuminant, &(self->dev->image_storage),
- custom_wb, &x, &y, p->temperature, p->illum_fluo, p->illum_led);
// if illuminant is set as camera, x and y are set on-the-fly at
// commit time, so we need to set adaptation too
- if(p->illuminant == DT_ILLUMINANT_CAMERA)
+ if(p->illuminant == DT_ILLUMINANT_CAMERA || p->illuminant == DT_ILLUMINANT_FROM_WB)
_check_if_close_to_daylight(x, y, NULL, NULL, &adaptation);
// Convert illuminant from xyY to XYZ
@@ -4441,7 +4548,7 @@ void gui_init(dt_iop_module_t *self)
g->delta_E_in = NULL;
g->delta_E_label_text = NULL;
- g->XYZ[0] = NAN;
+ g->ai_wb_XYZ[0] = NAN;
#ifdef AI_ACTIVATED
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_UI_PIPE_FINISHED, _develop_ui_pipe_finished_callback);
diff --git a/src/iop/colorin.c b/src/iop/colorin.c
index b1214f67910c..302af4a0afe2 100644
--- a/src/iop/colorin.c
+++ b/src/iop/colorin.c
@@ -650,10 +650,15 @@ int process_cl(dt_iop_module_t *self,
const dt_dev_chroma_t *chr = &self->dev->chroma;
const gboolean corrected = chr->late_correction;
- dt_aligned_pixel_t coeffs = { corrected ? chr->D65coeffs[0] / chr->as_shot[0] : 1.0f,
- corrected ? chr->D65coeffs[1] / chr->as_shot[1] : 1.0f,
- corrected ? chr->D65coeffs[2] / chr->as_shot[2] : 1.0f,
- corrected ? chr->D65coeffs[3] / chr->as_shot[3] : 1.0f };
+ dt_aligned_pixel_t coeffs;
+ for_four_channels(k)
+ {
+ if(corrected && chr->wb_coeffs[k] > 1e-6f)
+ coeffs[k] = chr->D65coeffs[k] / chr->wb_coeffs[k];
+ else
+ coeffs[k] = 1.0f;
+ }
+
if(corrected)
{
for_four_channels(k)
@@ -1196,10 +1201,15 @@ void process(dt_iop_module_t *self,
const dt_dev_chroma_t *chr = &self->dev->chroma;
const dt_iop_colorin_data_t *const d = piece->data;
const gboolean corrected = chr->late_correction && d->type != DT_COLORSPACE_LAB;
- const dt_aligned_pixel_t coeffs = { corrected ? chr->D65coeffs[0] / chr->as_shot[0] : 1.0f,
- corrected ? chr->D65coeffs[1] / chr->as_shot[1] : 1.0f,
- corrected ? chr->D65coeffs[2] / chr->as_shot[2] : 1.0f,
- corrected ? chr->D65coeffs[3] / chr->as_shot[3] : 1.0f };
+ dt_aligned_pixel_t coeffs;
+ for_four_channels(k)
+ {
+ if(corrected && chr->wb_coeffs[k] > 1e-6f)
+ coeffs[k] = chr->D65coeffs[k] / chr->wb_coeffs[k];
+ else
+ coeffs[k] = 1.0f;
+ }
+
dt_dev_pixelpipe_t *pipe = piece->pipe;
if(corrected)
{
diff --git a/src/iop/highlights.c b/src/iop/highlights.c
index 01ab23ad8bf0..aabd112fc8ad 100644
--- a/src/iop/highlights.c
+++ b/src/iop/highlights.c
@@ -652,9 +652,9 @@ int process_cl(dt_iop_module_t *self,
dt_aligned_pixel_t clips = { clipper, clipper, clipper, clipper};
if(pipe->dsc.temperature.enabled && chr->late_correction)
{
- clips[0] *= chr->as_shot[0] / chr->D65coeffs[0];
- clips[1] *= chr->as_shot[1] / chr->D65coeffs[1];
- clips[2] *= chr->as_shot[2] / chr->D65coeffs[2];
+ clips[0] *= chr->wb_coeffs[0] / chr->D65coeffs[0];
+ clips[1] *= chr->wb_coeffs[1] / chr->D65coeffs[1];
+ clips[2] *= chr->wb_coeffs[2] / chr->D65coeffs[2];
}
dev_xtrans = dt_opencl_copy_host_to_device_constant(devid, sizeof(piece->xtrans), piece->xtrans);
@@ -731,9 +731,9 @@ static void process_clip(dt_iop_module_t *self,
dt_aligned_pixel_t clips = { clip, clip, clip, clip};
if(piece->pipe->dsc.temperature.enabled && chr->late_correction)
{
- clips[0] *= chr->as_shot[0] / chr->D65coeffs[0];
- clips[1] *= chr->as_shot[1] / chr->D65coeffs[1];
- clips[2] *= chr->as_shot[2] / chr->D65coeffs[2];
+ clips[0] *= chr->wb_coeffs[0] / chr->D65coeffs[0];
+ clips[1] *= chr->wb_coeffs[1] / chr->D65coeffs[1];
+ clips[2] *= chr->wb_coeffs[2] / chr->D65coeffs[2];
}
for(int row = 0; row < roi_out->height; row++)
{
diff --git a/src/iop/hlreconstruct/opposed.c b/src/iop/hlreconstruct/opposed.c
index 224f3246da66..89ad86555609 100644
--- a/src/iop/hlreconstruct/opposed.c
+++ b/src/iop/hlreconstruct/opposed.c
@@ -42,8 +42,7 @@ static dt_hash_t _opposed_hash(dt_dev_pixelpipe_iop_t *piece)
{
const dt_iop_highlights_data_t *d = piece->data;
dt_hash_t hash = dt_dev_pixelpipe_piece_hash(piece, NULL, FALSE);
- hash = dt_hash(hash, &d->clip, sizeof(d->clip));
- return dt_hash(hash, &piece->module->dev->chroma.late_correction, sizeof(int));
+ return dt_hash(hash, &d->clip, sizeof(d->clip));
}
static inline float _calc_linear_refavg(const float *in, const int color)
@@ -236,10 +235,14 @@ static float *_process_opposed(dt_iop_module_t *self,
const dt_dev_chroma_t *chr = &self->dev->chroma;
const gboolean late = chr->late_correction;
- const dt_aligned_pixel_t correction = { late ? (float)(chr->D65coeffs[0] / chr->as_shot[0]) : 1.0f,
- late ? (float)(chr->D65coeffs[1] / chr->as_shot[1]) : 1.0f,
- late ? (float)(chr->D65coeffs[2] / chr->as_shot[2]) : 1.0f,
- 1.0f };
+ dt_aligned_pixel_t correction;
+ for_four_channels(k)
+ {
+ if(late && chr->wb_coeffs[k] > 1e-6f)
+ correction[k] = chr->D65coeffs[k] / chr->wb_coeffs[k];
+ else
+ correction[k] = 1.0f;
+ }
const size_t mwidth = roi_in->width / 3;
const size_t mheight = roi_in->height / 3;
@@ -434,10 +437,14 @@ static cl_int process_opposed_cl(dt_iop_module_t *self,
const dt_dev_chroma_t *chr = &self->dev->chroma;
const gboolean late = chr->late_correction;
- dt_aligned_pixel_t correction = { late ? (float)(chr->D65coeffs[0] / chr->as_shot[0]) : 1.0f,
- late ? (float)(chr->D65coeffs[1] / chr->as_shot[1]) : 1.0f,
- late ? (float)(chr->D65coeffs[2] / chr->as_shot[2]) : 1.0f,
- 1.0f };
+ dt_aligned_pixel_t correction;
+ for_four_channels(k)
+ {
+ if(late && chr->wb_coeffs[k] > 1e-6f)
+ correction[k] = chr->D65coeffs[k] / chr->wb_coeffs[k];
+ else
+ correction[k] = 1.0f;
+ }
cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
cl_mem dev_chrominance = NULL;
diff --git a/src/iop/hlreconstruct/segbased.c b/src/iop/hlreconstruct/segbased.c
index 62545f631cb7..20d33e117d3b 100644
--- a/src/iop/hlreconstruct/segbased.c
+++ b/src/iop/hlreconstruct/segbased.c
@@ -466,10 +466,14 @@ static void _process_segmentation(dt_dev_pixelpipe_iop_t *piece,
const dt_dev_chroma_t *chr = &piece->module->dev->chroma;
const gboolean late = chr->late_correction;
- const dt_aligned_pixel_t correction = { late ? (float)(chr->D65coeffs[0] / chr->as_shot[0]) : 1.0f,
- late ? (float)(chr->D65coeffs[1] / chr->as_shot[1]) : 1.0f,
- late ? (float)(chr->D65coeffs[2] / chr->as_shot[2]) : 1.0f,
- 1.0f };
+ dt_aligned_pixel_t correction;
+ for_four_channels(k)
+ {
+ if(late && chr->wb_coeffs[k] > 1e-6f)
+ correction[k] = chr->D65coeffs[k] / chr->wb_coeffs[k];
+ else
+ correction[k] = 1.0f;
+ }
const int recovery_mode = d->recovery;
const float strength = d->strength;
diff --git a/src/iop/temperature.c b/src/iop/temperature.c
index eee5d23ab7d8..befcbd3a70f6 100644
--- a/src/iop/temperature.c
+++ b/src/iop/temperature.c
@@ -43,7 +43,7 @@
#include "common/colorspaces.h"
#include "external/cie_colorimetric_tables.c"
-DT_MODULE_INTROSPECTION(4, dt_iop_temperature_params_t)
+DT_MODULE_INTROSPECTION(5, dt_iop_temperature_params_t)
#define INITIALBLACKBODYTEMPERATURE 4000
@@ -53,7 +53,7 @@ DT_MODULE_INTROSPECTION(4, dt_iop_temperature_params_t)
#define DT_IOP_LOWEST_TINT 0.135
#define DT_IOP_HIGHEST_TINT 2.326
-#define DT_IOP_NUM_OF_STD_TEMP_PRESETS 5
+#define DT_IOP_NUM_OF_STD_TEMP_PRESETS 4
// If you reorder presets combo, change this consts
#define DT_IOP_TEMP_UNKNOWN -1
@@ -61,7 +61,6 @@ DT_MODULE_INTROSPECTION(4, dt_iop_temperature_params_t)
#define DT_IOP_TEMP_SPOT 1
#define DT_IOP_TEMP_USER 2
#define DT_IOP_TEMP_D65 3
-#define DT_IOP_TEMP_D65_LATE 4
static void _gui_sliders_update(dt_iop_module_t *self);
@@ -72,6 +71,7 @@ typedef struct dt_iop_temperature_params_t
float blue; // $MIN: 0.0 $MAX: 8.0
float various; // $MIN: 0.0 $MAX: 8.0
int preset;
+ gboolean late_correction; // $DEFAULT: FALSE $DESCRIPTION: "prepare data for color calibration"
} dt_iop_temperature_params_t;
typedef struct dt_iop_temperature_gui_data_t
@@ -84,9 +84,8 @@ typedef struct dt_iop_temperature_gui_data_t
GtkWidget *btn_asshot; //As Shot
GtkWidget *btn_user;
GtkWidget *btn_d65;
- GtkWidget *btn_d65_late;
GtkWidget *temp_label;
- GtkWidget *balance_label;
+ GtkWidget *check_late_correction;
int preset_cnt;
int preset_num[54];
double mod_coeff[4];
@@ -102,6 +101,7 @@ typedef struct dt_iop_temperature_data_t
{
float coeffs[4];
int preset;
+ gboolean late_correction;
} dt_iop_temperature_data_t;
typedef struct dt_iop_temperature_global_data_t
@@ -142,6 +142,16 @@ int legacy_params(dt_iop_module_t *self,
int preset;
} dt_iop_temperature_params_v4_t;
+ typedef struct dt_iop_temperature_params_v5_t
+ {
+ float red;
+ float green;
+ float blue;
+ float various;
+ int preset;
+ gboolean late_correction;
+ } dt_iop_temperature_params_v5_t;
+
if(old_version == 2)
{
typedef struct dt_iop_temperature_params_v2_t
@@ -179,6 +189,28 @@ int legacy_params(dt_iop_module_t *self,
*new_version = 4;
return 0;
}
+ if(old_version == 4)
+ {
+ const int legacy_d65_late = 4;
+ const dt_iop_temperature_params_v4_t *o = (dt_iop_temperature_params_v4_t *)old_params;
+ dt_iop_temperature_params_v5_t *n = malloc(sizeof(dt_iop_temperature_params_v5_t));
+
+ memcpy(n, o, sizeof(dt_iop_temperature_params_v4_t));
+ if(o->preset == legacy_d65_late)
+ {
+ n->preset = DT_IOP_TEMP_AS_SHOT;
+ n->late_correction = TRUE;
+ }
+ else
+ {
+ n->preset = o->preset > legacy_d65_late ? o->preset - 1 : o->preset;
+ n->late_correction = FALSE;
+ }
+ *new_params = n;
+ *new_params_size = sizeof(dt_iop_temperature_params_v5_t);
+ *new_version = 5;
+ return 0;
+ }
return 1;
}
@@ -526,7 +558,7 @@ static inline void _publish_chroma(dt_dev_pixelpipe_iop_t *piece)
d->coeffs[k] * piece->pipe->dsc.processed_maximum[k];
chr->wb_coeffs[k] = d->coeffs[k];
}
- chr->late_correction = (d->preset == DT_IOP_TEMP_D65_LATE);
+ chr->late_correction = d->late_correction;
}
void process(dt_iop_module_t *self,
@@ -702,6 +734,8 @@ void commit_params(dt_iop_module_t *self,
{
for_four_channels(k)
chr->wb_coeffs[k] = 1.0f;
+ // keep the module handle available for GUI reports (see below)
+ chr->temperature = self;
return;
}
@@ -717,14 +751,26 @@ void commit_params(dt_iop_module_t *self,
d->preset = p->preset;
- /* Make sure the chroma information stuff is valid
- If piece is disabled we always clear the trouble message and
- make sure chroma does know there is no temperature module.
+ const gboolean effective_late_correction =
+ p->preset == DT_IOP_TEMP_D65 ? FALSE : p->late_correction;
+
+ d->late_correction = effective_late_correction;
+ // When the WB module is disabled it applies no coefficients (wb_coeffs are
+ // published as 1.0 above), so it makes no promise that the data is white
+ // balanced. Do not ask colorin (or any other consumer) to "finish" a
+ // correction that never started, otherwise disabling WB would still pull the
+ // image to D65 via D65coeffs/1.0.
+ chr->late_correction = piece->enabled ? effective_late_correction : FALSE;
+
+ /* Always publish the module handle for GUI reports, regardless of the
+ enabled state. The enabled state is tracked separately via
+ chr->temperature->enabled (module flag) and pipe->dsc.temperature.enabled
+ (pipe flag). This lets color calibration warn about a disabled white
+ balance module while it is still doing chromatic adaptation. Trouble
+ message state is owned entirely by _set_trouble_messages() in
+ channelmixerrgb, so we no longer clear it here.
*/
- chr->late_correction = p->preset == DT_IOP_TEMP_D65_LATE;
- chr->temperature = piece->enabled ? self : NULL;
- if(dt_pipe_is_preview(pipe) && !piece->enabled)
- dt_iop_set_module_trouble_message(self, NULL, NULL, NULL);
+ chr->temperature = self;
}
void init_pipe(dt_iop_module_t *self,
@@ -1154,7 +1200,6 @@ static inline const char *_preset_to_str(const int preset)
case DT_IOP_TEMP_SPOT: return "by spot";
case DT_IOP_TEMP_USER: return "user defined";
case DT_IOP_TEMP_D65: return "camera reference";
- case DT_IOP_TEMP_D65_LATE: return "as shot to reference";
default: return "other";
}
}
@@ -1163,18 +1208,43 @@ static void _update_preset(dt_iop_module_t *self, int mode)
{
dt_iop_temperature_params_t *p = self->params;
dt_dev_chroma_t *chr = &self->dev->chroma;
+ dt_iop_temperature_gui_data_t *g = self->gui_data;
+
+ const gboolean is_current_reference = p->preset == DT_IOP_TEMP_D65;
+ const gboolean is_new_mode_manual = mode != DT_IOP_TEMP_D65;
+
+ if(is_current_reference && is_new_mode_manual)
+ {
+ // set iff color calibration active in adaptation mode
+ p->late_correction = (chr->adaptation != NULL);
+ }
p->preset = mode;
- chr->late_correction = mode == DT_IOP_TEMP_D65_LATE;
+
+ if(mode == DT_IOP_TEMP_D65)
+ {
+ chr->late_correction = FALSE;
+ }
+ else
+ {
+ // For non-D65 modes, use the parameter
+ chr->late_correction = p->late_correction;
+ }
+
+ if(g && g->check_late_correction)
+ {
+ // show the checkbox only in modes where user can adjust multipliers
+ gtk_widget_set_visible(g->check_late_correction, is_new_mode_manual);
+
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->check_late_correction),
+ p->late_correction);
+ }
}
void gui_update(dt_iop_module_t *self)
{
dt_iop_temperature_gui_data_t *g = self->gui_data;
dt_iop_temperature_params_t *p = self->params;
- dt_iop_temperature_params_t *d = self->default_params;
-
- d->preset = dt_is_scene_referred() ? DT_IOP_TEMP_D65_LATE : DT_IOP_TEMP_AS_SHOT;
const gboolean true_monochrome =
dt_image_monochrome_flags(&self->dev->image_storage) & DT_IMAGE_MONOCHROME;
@@ -1207,13 +1277,7 @@ void gui_update(dt_iop_module_t *self)
gboolean found = FALSE;
const dt_dev_chroma_t *chr = &self->dev->chroma;
// is this a "as shot" white balance?
- if(dt_dev_equal_chroma((float *)p, chr->as_shot) && (p->preset == DT_IOP_TEMP_D65_LATE))
- {
- dt_bauhaus_combobox_set(g->presets, DT_IOP_TEMP_D65_LATE);
- found = TRUE;
- }
-
- else if(dt_dev_equal_chroma((float *)p, chr->as_shot))
+ if(dt_dev_equal_chroma((float *)p, chr->as_shot))
{
dt_bauhaus_combobox_set(g->presets, DT_IOP_TEMP_AS_SHOT);
p->preset = DT_IOP_TEMP_AS_SHOT;
@@ -1361,8 +1425,6 @@ void gui_update(dt_iop_module_t *self)
p->preset == DT_IOP_TEMP_USER);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->btn_d65),
p->preset == DT_IOP_TEMP_D65);
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->btn_d65_late),
- p->preset == DT_IOP_TEMP_D65_LATE);
_color_temptint_sliders(self);
_color_rgb_sliders(self);
@@ -1515,7 +1577,8 @@ void reload_defaults(dt_iop_module_t *self)
dt_iop_temperature_params_t *d = self->default_params;
dt_iop_temperature_params_t *p = self->params;
- d->preset = dt_is_scene_referred() ? DT_IOP_TEMP_D65_LATE : DT_IOP_TEMP_AS_SHOT;
+ d->preset = DT_IOP_TEMP_AS_SHOT;
+ d->late_correction = p->late_correction = dt_is_scene_referred();
float *dcoeffs = (float *)d;
for_four_channels(k)
@@ -1600,7 +1663,7 @@ void reload_defaults(dt_iop_module_t *self)
STR_YESNO(another_cat_defined),
daylights[0], daylights[1], daylights[2], as_shot[0], as_shot[1], as_shot[2]);
- d->preset = p->preset = DT_IOP_TEMP_AS_SHOT;
+ d->preset = DT_IOP_TEMP_AS_SHOT;
// White balance module doesn't need to be enabled for true_monochrome raws (like
// for leica monochrom cameras). prepare_matrices is a noop as well, as there
@@ -1624,7 +1687,8 @@ void reload_defaults(dt_iop_module_t *self)
{
for_four_channels(k)
dcoeffs[k] = as_shot[k];
- d->preset = p->preset = DT_IOP_TEMP_D65_LATE;
+ d->preset = p->preset = DT_IOP_TEMP_AS_SHOT;
+ d->late_correction = p->late_correction = TRUE;
}
else
{
@@ -1634,6 +1698,7 @@ void reload_defaults(dt_iop_module_t *self)
dcoeffs[2] = coeffs[2]/coeffs[1];
dcoeffs[3] = coeffs[3]/coeffs[1];
dcoeffs[1] = 1.0f;
+ d->late_correction = p->late_correction = FALSE;
}
}
}
@@ -1671,7 +1736,6 @@ void reload_defaults(dt_iop_module_t *self)
dt_bauhaus_combobox_add(g->presets, C_("white balance", "user modified"));
// old "camera neutral", reason: better matches intent
dt_bauhaus_combobox_add(g->presets, C_("white balance", "camera reference"));
- dt_bauhaus_combobox_add(g->presets, C_("white balance", "as shot to reference"));
g->preset_cnt = DT_IOP_NUM_OF_STD_TEMP_PRESETS;
memset(g->preset_num, 0, sizeof(g->preset_num));
@@ -1681,7 +1745,6 @@ void reload_defaults(dt_iop_module_t *self)
_gui_sliders_update(self);
dt_bauhaus_combobox_set(g->presets, p->preset);
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->btn_d65_late), p->preset == DT_IOP_TEMP_D65_LATE);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->btn_asshot), p->preset == DT_IOP_TEMP_AS_SHOT);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->btn_user), FALSE);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->btn_d65), FALSE);
@@ -1741,8 +1804,10 @@ void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous)
_mul2temp(self, p, &g->mod_temp, &g->mod_tint);
- dt_bauhaus_combobox_set(g->presets, DT_IOP_TEMP_USER);
- _update_preset(self, DT_IOP_TEMP_USER);
+ if(w != g->check_late_correction) {
+ dt_bauhaus_combobox_set(g->presets, DT_IOP_TEMP_USER);
+ _update_preset(self, DT_IOP_TEMP_USER);
+ }
}
static gboolean _btn_toggled(GtkWidget *togglebutton,
@@ -1755,7 +1820,6 @@ static gboolean _btn_toggled(GtkWidget *togglebutton,
const int preset = togglebutton == g->btn_asshot ? DT_IOP_TEMP_AS_SHOT :
togglebutton == g->btn_d65 ? DT_IOP_TEMP_D65 :
- togglebutton == g->btn_d65_late ? DT_IOP_TEMP_D65_LATE :
togglebutton == g->btn_user ? DT_IOP_TEMP_USER : 0;
if(!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(togglebutton)))
@@ -1798,8 +1862,6 @@ static void _preset_tune_callback(GtkWidget *widget, dt_iop_module_t *self)
pos == DT_IOP_TEMP_USER);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->btn_d65),
pos == DT_IOP_TEMP_D65);
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->btn_d65_late),
- pos == DT_IOP_TEMP_D65_LATE);
gboolean show_finetune = FALSE;
dt_dev_chroma_t *chr = &self->dev->chroma;
@@ -1829,9 +1891,6 @@ static void _preset_tune_callback(GtkWidget *widget, dt_iop_module_t *self)
case DT_IOP_TEMP_D65: // camera reference d65
_temp_params_from_array(p, chr->D65coeffs);
break;
- case DT_IOP_TEMP_D65_LATE: // as shot wb just for now
- _temp_params_from_array(p, chr->as_shot);
- break;
default: // camera WB presets
{
gboolean found = FALSE;
@@ -2107,21 +2166,11 @@ void gui_init(dt_iop_module_t *self)
(g->btn_d65,
_("set white balance to camera reference point\nin most cases it should be D65"));
- g->btn_d65_late = dt_iop_togglebutton_new(self,
- N_("settings"),
- N_("as shot to reference"), NULL,
- G_CALLBACK(_btn_toggled), FALSE, 0, 0,
- dtgtk_cairo_paint_bulb_mod, NULL);
- gtk_widget_set_tooltip_text
- (g->btn_d65_late,
- _("set white balance to as shot and later correct to camera reference point,\nin most cases it should be D65"));
-
// put buttons at top. fill later.
g->buttonbar = dt_gui_hbox(dt_gui_expand(g->btn_asshot),
dt_gui_expand(g->colorpicker),
dt_gui_expand(g->btn_user),
- dt_gui_expand(g->btn_d65),
- dt_gui_expand(g->btn_d65_late));
+ dt_gui_expand(g->btn_d65));
dt_gui_add_class(g->buttonbar, "dt_iop_toggle");
g->presets = dt_bauhaus_combobox_new(self);
@@ -2164,12 +2213,26 @@ void gui_init(dt_iop_module_t *self)
(g->scale_tint,
_("color tint of the image, from magenta (value < 1) to green (value > 1)"));
+ g->check_late_correction = dt_bauhaus_toggle_from_params(self, "late_correction");
+ g_object_ref(g->check_late_correction); // prevent destruction
+ GtkWidget *temp_parent = gtk_widget_get_parent(g->check_late_correction);
+ if(temp_parent)
+ gtk_container_remove(GTK_CONTAINER(temp_parent), g->check_late_correction);
+
+ gtk_widget_set_tooltip_text(g->check_late_correction,
+ _("ensures the white balance coefficients are treated as a reference for the color calibration module.\n"
+ "keep this checked for non-reference white balance in the modern scene-referred workflow."));
+
GtkWidget *box_enabled = dt_gui_vbox(g->buttonbar,
+ g->check_late_correction,
g->presets,
g->finetune,
temp_label_box,
g->scale_k,
g->scale_tint);
+
+ g_object_unref(g->check_late_correction);
+
dt_gui_new_collapsible_section
(&g->cs,
"plugins/darkroom/temperature/expand_coefficients",
@@ -2223,9 +2286,9 @@ void gui_cleanup(dt_iop_module_t *self)
void gui_reset(dt_iop_module_t *self)
{
dt_iop_temperature_gui_data_t *g = self->gui_data;
- dt_iop_temperature_params_t *d = self->default_params;
+ dt_iop_temperature_params_t *p = self->params;
- const int preset = d->preset = dt_is_scene_referred() ? DT_IOP_TEMP_D65_LATE : DT_IOP_TEMP_AS_SHOT;
+ const int preset = p->preset;
dt_iop_color_picker_reset(self, TRUE);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->btn_asshot),
@@ -2234,8 +2297,6 @@ void gui_reset(dt_iop_module_t *self)
preset == DT_IOP_TEMP_USER);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->btn_d65),
preset == DT_IOP_TEMP_D65);
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->btn_d65_late),
- preset == DT_IOP_TEMP_D65_LATE);
_color_finetuning_slider(self);
_color_rgb_sliders(self);