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.
These are the moving parts the later sections rely on. Each one gets a short definition, not a full reference. Core_Concepts.md introduces the objects they act on: dev, module instance, pipe, and piece.
An ordered list of per-module parameter snapshots on the dev
(dt_develop_t). 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.
The darkroom dev has three pixelpipes. They run the same module chain independently, at different resolutions:
- full: the main darkroom image
- preview: the low-resolution image used for the navigation thumbnail and the histogram
- preview2: the optional second window
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 dev.
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 changed | synch_top: re-commit that one module; the other pieces are not touched |
DT_DEV_PIPE_SYNCH |
All items need a param resync, topology unchanged | synch_all: commit every piece's defaults, then every active history item in history 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 | no param sync; only the ROIs change |
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).
pipe->status is one of DT_DEV_PIXELPIPE_DIRTY (needs reprocessing), RUNNING (a thread is
processing it now), VALID (finished with a valid result) or INVALID (finished with an
unusable result).
Despite its name, dev->focus_hash is a flag. dt_iop_request_focus() sets it to TRUE when
the user moves focus to a different module (opens its panel or clicks into one of its widgets),
and dt_dev_reload_history_items() sets it back to FALSE. Each new history item stores the
value it had when the item was created.
_dev_add_history_item_ext() (develop.c) compares the two. Editing the module that is already
at the top of the history normally updates that item in place, but when dev->focus_hash
differs from the item's stored value and the params differ, it pushes a new item instead:
|| ((dev->focus_hash != hist->focus_hash) // or if focused out and in
// but only add item if there is a difference at all for the same module
&& (... memcmp(hist->params, module->params, module->params_size)))A new item 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.
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, blend params and mask), combined
with the image id, the color-profile information and the requested ROI. It is not a hash of
one module's data on its own. _dev_pixelpipe_cache_basichash() in pixelpipe_cache.c walks all
enabled 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.
default_params is image-specific: it is set by reload_defaults() (see
IOP_Module_API.md). 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.
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.
_dt_dev_load_raw()triggers the raw decode through the mipmap cache (this blocks), then reads the decoded image struct intodev->image_storage. The large mipmap buffer is released right after the decode;image_storagekeeps the metadata. (The mipmap cache holds pre-scaled copies of each image at several resolutions, used for fast thumbnail and preview generation.)- Mark all pipes
DIRTYand setloading = TRUE. dt_iop_load_modules()builds the IOP module list (dev->iop).dt_dev_read_history_ext()loads defaults and presets, and buildsdev->history. See below.
dt_dev_load_image() itself does not load defaults. That happens inside
dt_dev_read_history_ext(). Neither function copies the history into module->params: the
caller does that afterwards (see After loading).
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.)
For every image (the if(!no_image) block, which runs whether or not the image has saved
history):
- Clear the scratch
memory.historytable. dt_dev_reset_chroma()resets part of the shared white balance state,dev->chroma(see section 5 for why)._dt_dev_load_pipeline_defaults()callsdt_iop_reload_defaults()on every module instance already indev->iop, in reverse pipe order (section 5 explains the consequences). This sets each module's image-specificdefault_params. Because it goes through the wrapper, it also copies them intoparams(viadt_iop_load_default_params()). Some modules also write shared state here: white balance stores the camera's reference coefficients indev->chroma._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 theDT_IMAGE_AUTO_PRESETS_APPLIEDimage flag, not onchange_timestamp == -1(that test guards only the separate legacy pre-3.0 white balance 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 intomain.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, which is how auto-applied presets are stored), sets hist->enabled,
appends to dev->history, and increments history_end:
if(param_length == 0)
memcpy(hist->params, hist->module->default_params, hist->module->params_size);
...
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().
A row can name an instance that is not in dev->iop yet, for example a second exposure
instance (multi_priority > 0). The loop then creates it with dt_iop_load_module()
(imageop.c), which calls init() but not reload_defaults(). The new instance has the
default_params set by init(), not image-specific ones, and a row without params falls
back to them. Whether reload_defaults() runs for it later depends on the caller: see
After loading.
After the loop, in the none workflow, if both white balance (temperature) and color
calibration (channelmixerrgb) are enabled in the history, the function calls
temperature->reload_defaults(temperature) directly. This bypasses the wrapper, so
params is only partially updated; see the reload_defaults() caveat in
IOP_Module_API.md.
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).
dt_dev_read_history_ext() fills dev->history, but it does not copy the history params into
module->params, and it does not update any widget. In darkroom, the caller does this with
dt_dev_pop_history_items(dev, dev->history_end), after the module GUIs exist. That call
resets every module to default_params, replays the active history entries into
module->params, calls gui_update() on every module, and schedules pipe synchronization
(see section 3f). Only after this call are
module->params and the widgets current for the new image.
The two darkroom paths differ in what runs before that call (both in views/darkroom.c):
- Entering darkroom (
enter()): callsdt_dev_load_image(), then, for every visible module instance,dt_iop_gui_init()followed bydt_iop_reload_defaults(), thendt_dev_pop_history_items(). Instances created from the history get their image-specific defaults here. - Switching image (
_dev_load_requested_image(), queued by_dev_change_image()): does not calldt_dev_load_image(). It keeps each module's base instance and callsdt_iop_reload_defaults()on it, destroys the other instances, then callsdt_dev_read_history(). For each instance that the history creates, it callsdt_iop_gui_init()but notdt_iop_reload_defaults(); for each base instance it callschange_image(). Then it callsdt_dev_pop_history_items().
So a module cannot assume that reload_defaults(), or any side effect in it, has run for an
instance created from the history.
Headless callers call dt_dev_load_image(), and not all of them replay. Export in
imageio.c, for example, calls dt_dev_pop_history_items_ext() only when it needs a history
end different from the stored one. The pipe does not read module->params:
dt_dev_pixelpipe_synch_all() commits each module's default_params and then the
hist->params of the history entries up to history_end, so the pipe output is correct
without a replay into the modules.
The edits in 3a-3d end in _dev_add_history_item_ext() (develop.c), which chooses between two
branches:
- update in place (
TOP_CHANGED): the module is already at the top of the history, and thefocus_hashtest above does not ask for a new item. The item's params andenabledflag are overwritten andpipe->changed |= DT_DEV_PIPE_TOP_CHANGED. - new item (
SYNCH): anything else, such as a different module or instance. A new history entry is pushed andpipe->changed |= DT_DEV_PIPE_SYNCH.
Removing an instance (3e) and undo, redo or history navigation (3f) change the history without this function.
- For a widget created with a
_from_paramshelper, the bauhaus value-change handler (bauhaus.c) writes the new value intoself->paramsand callsdt_iop_gui_changed(module, widget, &prev). A manually connected widget callsdt_dev_add_history_item()from its own callback instead. dt_iop_gui_changed()runs the module'sgui_changed()(module-specific; it may update dependent widgets), thendt_dev_add_history_item_target()._dev_add_history_item_ext()chooses one of the two branches above.dt_dev_invalidate_all()marks all pipesDIRTYand incrementsdev->timestamp.DT_SIGNAL_DEVELOP_HISTORY_CHANGEis emitted only whenneed_end_recordis true, not on every slider tick. It is tied to the undo-record start/end pairing, so one logical edit emits the signal once.- On the pipeline thread,
dt_dev_pixelpipe_change()dispatches on the flag:TOP_CHANGED->dt_dev_pixelpipe_synch_top(): runscommit_params()for that one module only.SYNCH->dt_dev_pixelpipe_synch_all(): commits every piece's defaults, then every active history item in history order. Unchanged hashes still hit the cache.
- The pipeline runs: modules with unchanged hashes reuse their cached output; the changed module and everything downstream of it reprocess.
- The toggle button fires its
toggledsignal, which reaches_gui_off_callback()(imageop.c). - The callback sets
module->enabledand callsdt_dev_add_history_item(). Thehist->enabledflag is recorded separately from params. - If the module is already at the top of the history, its item is updated in place
(
TOP_CHANGED); otherwise a new item is pushed (SYNCH).
A click on the reset button in the module header reaches _gui_reset_clicked() (imageop.c).
A shortcut reaches _gui_reset_callback(), which has the same logic. A plain reset does this:
- If the module has a drawn mask,
dt_masks_form_remove()(masks.c) removes it. This changes the history before the reset itself: it clears the module'smask_idand callsdt_dev_add_history_item(), then callsdt_dev_add_masks_history_item(), which stores the new mask list in the history and setsSYNCHon all pipes. This can change other modules too. The "add existing shape" menu of a drawn mask also lists the mask groups of other modules, so the group of module B can contain the group of module A. A reset of A removes A's group from B's group. If B's group becomes empty, it is removed as well: B'smask_idis cleared and B gets its own history item. dt_iop_reload_defaults(module)recomputes the image-specificdefault_paramsand copies them intomodule->params(this is the wrapper path, so the copy does happen). The blend params are reset todefault_blendop_params.dt_iop_gui_reset(module)calls the module'sgui_reset(), if it has one, anddt_iop_gui_update(module)syncs the widgets to the new params.dt_dev_add_history_item(module->dev, module, TRUE)adds or updates a single history entry, with the sameTOP_CHANGED/SYNCHchoice as any other edit. It does not rebuild the whole stack. Without a drawn mask, this is the only history change of the reset. With a drawn mask, step 1 has already setSYNCH, so the pipe runsdt_dev_pixelpipe_synch_all()even if this step updates the item in place.
Ctrl+reset first calls dt_gui_presets_autoapply_for_module() (gui/presets.c). If an
auto-apply preset matches the image, or the module has a built-in preset for the current
workflow (such as "scene-referred default"), that preset is applied instead of steps 1-4:
dt_gui_presets_apply_preset() copies the preset's params, enabled flag and blend params into
the module, updates the GUI, and calls dt_dev_add_history_item(..., FALSE). If no preset
matches, a plain reset is done.
dt_iop_gui_duplicate()(imageop.c) starts the duplication.- It records the base module's state:
dt_dev_add_history_item(base->dev, base, FALSE). dt_dev_module_duplicate()creates the new instance indev->iop.dt_iop_reload_defaults(new_module)sets the image-specific defaults for the new instance.dt_dev_add_history_item(new_module->dev, new_module, TRUE)adds a new history entry.dt_dev_pixelpipe_rebuild()setspipe->changed |= DT_DEV_PIPE_REMOVE, which forces a full topology rebuild: the node graph changed, so it is rebuilt from scratch.
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:
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, and the pipes are marked for a topology rebuild
(REMOVE: 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.
During an active editing session these are the main way module->params are rewritten
from saved entries. Both end in dt_dev_pop_history_items(), the same call that finishes a
darkroom image load (After loading). It calls
dt_dev_pop_history_items_ext() to reset all modules to their default_params and replay the
history up to the given position, updates every module's GUI, then calls
dt_dev_pipe_synch_all() (or dt_dev_pixelpipe_rebuild() when the module order changed) and
dt_dev_invalidate_all().
Clicking an entry in the history panel (libs/history.c) calls
dt_dev_pop_history_items(dev, num) directly, with the clicked position.
Undo and redo (_pop_undo() in libs/history.c) swap in the saved history list and
history_end, mark the pipes for a rebuild, write the history to the database, and call
dt_dev_reload_history_items() (develop.c), which:
- clears
dev->focus_hash, - calls
dt_dev_pop_history_items(dev, 0)to reset all modules to theirdefault_params, - drops the history items beyond
history_end, and re-reads the history from the database, - calls
dt_dev_pop_history_items(dev, history_end)to replay up to the target position.
White balance (temperature.c) and color calibration (channelmixerrgb.c) share state in
dev->chroma; there is no signal. White balance writes the camera's reference coefficients in
reload_defaults() and the coefficients it applies in commit_params(); color calibration
reads them in reload_defaults(), commit_params() and process().
Commit order does not guarantee that white balance commits first: synch_all replays the
history in history order, and synch_top re-commits only the top item. Color calibration
therefore recomputes its camera illuminant (DT_ILLUMINANT_CAMERA) in process(), which runs
after all the pipe's commits are done.
This follows the same flow as a slider adjust: TOP_CHANGED or SYNCH, depending on the
history and focus_hash. 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.
Three operations walk the modules in different orders, which is a common source of confusion. (See also the ordering-asymmetry note in pixelpipe_architecture.md.)
-
Processing runs in pipe order: an upstream module's
process()finishes before a downstream one starts. -
commit_params()(throughsynch_all) first commits every piece's defaults in pipe order, then replays the history items in history order. History order is the order of the user's edits, not pipe order, so a downstream module can commit before an upstream one. -
_dt_dev_load_pipeline_defaults()iterates in reverse pipe order (last module first):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.
Neither reload_defaults() nor commit_params() can assume that earlier-in-pipe modules have
already written shared state for the current image or edit. Only process() runs in pipe
order.
Shared state that a reload_defaults() depends on must therefore be reset to a neutral
value before the reverse default-load. The framework does this for white balance: it calls
dt_dev_reset_chroma() just before _dt_dev_load_pipeline_defaults(). The neutral white
balance coefficients make color calibration compute its default illuminant from the image's own
as-shot coefficients, instead of from values left by the previous image.
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 recordsdev->timestampas the pipe's input 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 described in
section 1. On a key match, the cached output is reused and
process()is skipped. On a miss,process()(orprocess_cl()on an OpenCL device) runs and the result is stored. modify_roi_in()andmodify_roi_out()run on every module during the ROI-propagation phase, before processing, translating between the output-side and input-side regions. Under theZOOMEDflag only the ROIs change, not the module hashes. The ROI is part of the cache key, so a module gets a cache hit only for a region it has already produced, for example after zooming back.- 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->statusbecomesVALIDand a finished signal (for exampleDT_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.
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 the instances already in dev->iop, in reverse pipe order |
dt_dev_reset_chroma |
Resets shared white balance state before the 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 up to a position |
dt_dev_reload_history_items |
Undo/redo: reset, re-read the history, replay |
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_load_module |
Creates a module instance; calls init(), not reload_defaults() |
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 |
Entry point for _from_params widget changes |
dt_iop_request_focus |
Changes the focused module; sets dev->focus_hash |
_gui_off_callback |
Enable/disable toggle handler |
_gui_reset_clicked |
Reset button handler (_gui_reset_callback for shortcuts) |
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 history item only |
dt_dev_pixelpipe_synch_all |
Commits all defaults, then all active history items |
_dev_pixelpipe_process_rec |
Recursive per-module processing |
Other
| Symbol | File | Role |
|---|---|---|
_dev_pixelpipe_cache_basichash |
pixelpipe_cache.c | Builds the cache key from image id, profiles, and all modules up to a position |
_pop_undo |
libs/history.c | Undo/redo handler |
enter |
views/darkroom.c | Darkroom entry: load image, create GUIs, reload defaults, replay history |
_dev_load_requested_image |
views/darkroom.c | Darkroom image switch: keep base instances, read history, replay |
- IOP_Module_API.md: the callback API reference (signatures, the two
reload_defaultsjobs). - pixelpipe_architecture.md: pipeline architecture, including the ordering-asymmetry note.