This guide documents the functions that darktable Image Operation (IOP) modules can implement. The API is defined in src/iop/iop_api.h and used via src/develop/imageop.h. See src/iop/useless.c for a fully documented example module.
See also:
- Pixelpipe Architecture for pipeline data flow and caching.
- Introspection System for parameter management.
- GUI Architecture for GUI events, callbacks, thread safety, and widget reparenting.
Every IOP module needs:
- Parameter struct (
dt_iop_modulename_params_t) - the user-facing parameters, serialized to the database, controlled via UI widgets inself->params - Processing data struct (
dt_iop_modulename_data_t) - optional but common; a processing-optimized version of the parameters, stored inpiece->dataand used byprocess(). When not provided,piece->datais a plain copy ofparams_t(see params_t vs data_t below). - GUI data struct (
dt_iop_modulename_gui_data_t) - widget references, only present in darkroom mode - Required functions -
name(),default_colorspace(),process() - Optional functions - GUI, lifecycle, geometry, etc.
This struct defines the user-facing parameters. It is the contract between the module and the outside world:
- Database: Serialized as a binary blob and stored in the history stack. This is how edits persist across sessions.
- UI widgets:
dt_bauhaus_*_from_params()functions read and write fields inself->paramsvia introspection. - Presets/styles: Params are what gets exported and imported.
// Version number - increment when struct changes
DT_MODULE_INTROSPECTION(1, dt_iop_mymodule_params_t)
typedef struct dt_iop_mymodule_params_t
{
// Introspection tags configure widgets automatically:
// $MIN, $MAX, $DEFAULT set slider range/default
// $DESCRIPTION sets the widget label
float exposure; // $MIN: -10.0 $MAX: 10.0 $DEFAULT: 0.0 $DESCRIPTION: "exposure"
float gamma; // $MIN: 0.1 $MAX: 4.0 $DEFAULT: 1.0
gboolean enabled; // $DEFAULT: TRUE $DESCRIPTION: "enable correction"
// Enum fields auto-populate comboboxes
dt_mymodule_method_t method; // $DEFAULT: METHOD_AUTO
} dt_iop_mymodule_params_t;Rules:
- Use
gbooleannotbool(4-byte alignment) - Changes require version bump and
legacy_params()migration - Values are serialized to database - no pointers
Introspection tags (parsed from comments at compile time, used by dt_bauhaus_*_from_params()):
| Tag | Applies to | Purpose |
|---|---|---|
$MIN: value |
float, int |
Hard minimum for the widget |
$MAX: value |
float, int |
Hard maximum for the widget |
$DEFAULT: value |
all types | Default value (also used by dt_iop_default_init()) |
$DESCRIPTION: "text" |
all types | Widget label in the GUI (translatable) |
For enums, $DESCRIPTION on each enum member becomes the combobox entry text. For gboolean, $DEFAULT accepts TRUE / FALSE.
The DT_MODULE_INTROSPECTION(version, struct_type) macro at the top of the file activates this system and sets the parameter version number.
typedef enum dt_mymodule_method_t
{
METHOD_AUTO = 0, // $DESCRIPTION: "automatic"
METHOD_MANUAL = 1, // $DESCRIPTION: "manual"
METHOD_CUSTOM = 2, // $DESCRIPTION: "custom"
} dt_mymodule_method_t;The dt_iop_module_t structure (defined in src/iop/iop_api.h) is the main handle for your module instance. Key members:
params: Pointer to the current parameter struct.default_params: Pointer to the default parameters.gui_data: Pointer to your GUI data struct.dev: Pointer to thedt_develop_tsession.widget: The main widget container for the module.multi_priority(orinstance): The integer instance number (0 for the first instance).iop_order: The module's execution order position in the pixelpipe.enabled: boolean flag for the module's enabled state.
A common source of confusion is the relationship between the parameter struct (params_t) and the processing data struct (data_t). They serve different purposes:
dt_iop_modulename_params_t |
dt_iop_modulename_data_t |
|
|---|---|---|
| Where it lives | self->params |
piece->data |
| Source | Database / UI widgets | Built by commit_params() |
| Purpose | Record user intent in a stable, serializable format | Provide processing-ready values to process() |
| May contain | Raw user values (e.g. EV, percentages, enum choices) | Precomputed LUTs, splines, normalized/transformed values, expensive one-time calculations |
| Constraints | No pointers; must be serializable; changing it requires a version bump | No constraints — can contain pointers, LUTs, runtime-only data |
When you don't need a data_t: If process() can work directly from the raw user parameters without any transformation, you don't need a separate data_t. The default init_pipe() allocates piece->data as a params_t-sized buffer, and the default commit_params() does memcpy(piece->data, params, self->params_size).
When you do need a data_t: Most non-trivial modules define a separate data_t struct because:
- Some user parameters need transformation before they are useful for processing (e.g. converting percentages to linear factors, degrees to radians, EV values to exposure multipliers).
- Expensive calculations should happen once in
commit_params(), not per-pixel inprocess()(e.g. building interpolation splines, computing lookup tables, solving matrices). - Additional runtime state is needed that doesn't belong in the database (e.g. pointers to the current working color profile, gamut boundary LUTs).
Concrete examples from the codebase:
// exposure.c: data_t embeds params_t and adds precomputed fields
typedef struct dt_iop_exposure_data_t
{
dt_iop_exposure_params_t params; // raw user params
int deflicker; // computed: is deflicker mode active?
float black; // computed: adjusted black point
float scale; // computed: exposure multiplier
} dt_iop_exposure_data_t;
// filmicrgb.c: data_t is entirely different from params_t
typedef struct dt_iop_filmicrgb_data_t
{
float dynamic_range; // derived from white_point - black_point
float grey_source; // computed from user grey_point / 100
float contrast; // clamped/adjusted contrast
float saturation; // normalized from user percentage
float sigma_toe, sigma_shoulder; // computed from spline
struct dt_iop_filmic_rgb_spline_t spline; // fully solved spline LUT
// ... no direct copy of params fields
} dt_iop_filmicrgb_data_t;The data flow:
Database ──load──→ self->params ──UI widgets──→ self->params
│
commit_params()
│
▼
piece->data ──→ process()
(params_t or data_t)
When a data_t is used, you must also implement init_pipe() and cleanup_pipe() to allocate/free it. See Pipe Lifecycle Functions below.
const char *name() { return _("my module"); }
dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self,
dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
{
return IOP_CS_RGB; // or IOP_CS_LAB, IOP_CS_RAW
}void process(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
const void *const ivoid,
void *const ovoid,
const dt_iop_roi_t *const roi_in,
const dt_iop_roi_t *const roi_out)
{
// Get processing data — this is piece->data as prepared by commit_params().
const dt_iop_mymodule_params_t *d = piece->data;
const size_t ch = piece->colors; // Usually 4 (RGBA)
// Validate input format
if(!dt_iop_have_required_input_format(4, self, piece->colors,
ivoid, ovoid, roi_in, roi_out))
return;
// Process pixels
DT_OMP_FOR()
for(int j = 0; j < roi_out->height; j++)
{
const float *in = ((float *)ivoid) + (size_t)ch * roi_in->width * j;
float *out = ((float *)ovoid) + (size_t)ch * roi_out->width * j;
for(int i = 0; i < roi_out->width; i++)
{
for_each_channel(c, aligned(in, out))
out[c] = in[c] * d->exposure;
in += ch;
out += ch;
}
}
}Important:
- Never use GTK+ API directly in
process()— see GUI.md for the correct approach - Use
piece->datafor parameters, notself->params - Use
DT_OMP_FOR()for parallelization - Use
for_each_channel()for vectorization
typedef struct dt_iop_roi_t
{
int x, y, width, height; // position and dimensions in pixels (at current scale)
float scale; // zoom factor relative to full image (0 < scale <= 1.0)
} dt_iop_roi_t;For most modules (those that don't change geometry), roi_in and roi_out are identical.
Multiple pipelines may process an image simultaneously. Check dt_pipe_is_full() and friends when behavior should differ:
DT_DEV_PIXELPIPE_FULL // Full-resolution center view
DT_DEV_PIXELPIPE_PREVIEW // Navigation preview
DT_DEV_PIXELPIPE_PREVIEW2 // Secondary preview (dual view)
DT_DEV_PIXELPIPE_EXPORT // Full export
DT_DEV_PIXELPIPE_THUMBNAIL // Thumbnail generation
DT_DEV_PIXELPIPE_SCREEN // PREVIEW | FULL | PREVIEW2
DT_DEV_PIXELPIPE_ANY // All typesThe ratio between the input buffer and the full image. Use to scale spatial parameters:
const float sigma = user_radius * roi_out->scale / piece->iscale;DT_OMP_FOR() // OpenMP parallel for with safe defaults
for_each_channel(c, aligned(in, out : 16)) // SIMD-vectorized 4-channel loop
copy_pixel(out, in); // 4-channel copy
copy_pixel_nontemporal(out, in); // bypass cache (sequential writes only)
dt_omploop_sfence(); // memory fence after nontemporal writesfloat *temp = NULL;
if(!dt_iop_alloc_image_buffers(module, roi_in, roi_out,
4 | DT_IMGSZ_OUTPUT | DT_IMGSZ_CLEARBUF,
&temp, NULL))
return; // allocation failed — trouble message already set
// ... use temp ...
dt_free_align(temp);dt_iop_set_module_trouble_message(module,
_("unsupported input"), _("expected 4-channel input"), NULL);
dt_iop_set_module_trouble_message(module, NULL, NULL, NULL); // clearTrigger reprocessing from GUI callbacks:
dt_iop_refresh_center(module); // invalidate full pipe, redraw center view
dt_iop_refresh_preview(module); // invalidate preview pipe
dt_iop_refresh_all(module); // invalidate all pipesThese functions provide metadata for the module. See src/iop/iop_api.h for signatures.
Common flags: IOP_FLAGS_INCLUDE_IN_STYLES, IOP_FLAGS_SUPPORTS_BLENDING, IOP_FLAGS_ALLOW_TILING, IOP_FLAGS_HIDDEN, IOP_FLAGS_DEPRECATED, IOP_FLAGS_ONE_INSTANCE.
For gui_init(), gui_update(), gui_changed(), gui_cleanup(), gui_focus(), color_picker_apply(), and mouse/drawing events, see GUI.md.
Called once per module type to register built-in presets:
void init_presets(dt_iop_module_so_t *self)
{
dt_iop_mymodule_params_t p = {
.exposure = 1.0f,
.gamma = 2.2f,
.method = METHOD_MANUAL,
.enabled = TRUE
};
dt_gui_presets_add_generic(_("my awesome preset"), self->op, self->version(), &p, sizeof(p));
}Called once per module instance. Usually not needed if using $DEFAULT tags.
void init(dt_iop_module_t *self)
{
dt_iop_default_init(self); // Use introspection defaults
self->hide_enable_button = TRUE; // Override specific settings
}Called once per module type (not per instance). Used primarily to load OpenCL kernels:
typedef struct dt_iop_mymodule_global_data_t
{
int kernel_process;
} dt_iop_mymodule_global_data_t;
void init_global(dt_iop_module_so_t *self)
{
dt_iop_mymodule_global_data_t *gd = calloc(1, sizeof(*gd));
self->data = gd;
gd->kernel_process = dt_opencl_create_kernel(42, "mymodule_process");
}
void cleanup_global(dt_iop_module_so_t *self)
{
dt_iop_mymodule_global_data_t *gd = self->data;
dt_opencl_free_kernel(gd->kernel_process);
free(gd);
}Access in process_cl() via self->global_data.
Called when switching images. Update defaults based on image properties:
void reload_defaults(dt_iop_module_t *self)
{
dt_iop_mymodule_params_t *d = self->default_params;
const dt_image_t *img = &self->dev->image_storage;
if(!dt_image_is_raw(img))
self->default_enabled = FALSE;
}Common checks: dt_image_is_raw(), dt_image_is_hdr(), dt_image_is_ldr(), dt_image_is_monochrome(), dt_image_is_bayerRGB().
Each pixelpipe has its own copy of every module's data via dt_dev_pixelpipe_iop_t ("piece").
Default behavior (if not implemented): allocates/frees self->params_size bytes.
Custom implementation (required when using a separate data_t):
void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe,
dt_dev_pixelpipe_iop_t *piece)
{
piece->data = calloc(1, sizeof(dt_iop_mymodule_data_t));
}
void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe,
dt_dev_pixelpipe_iop_t *piece)
{
dt_iop_mymodule_data_t *d = piece->data;
dt_free_align(d->gamut_LUT); // free any sub-allocations
dt_free_align(piece->data);
piece->data = NULL;
}Called by the framework whenever parameters are synced to the pixelpipe. Its job is to translate self->params into processing-ready piece->data.
Caching note: The framework hashes piece->data after this function returns. If the hash changes, the cache for this module and all subsequent ones is invalidated. Ensure deterministic output for identical inputs.
Simple case — no transformation needed, default memcpy suffices. Don't implement this function.
Common case — precompute expensive values:
void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1,
dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
{
const dt_iop_mymodule_params_t *p = (dt_iop_mymodule_params_t *)p1;
dt_iop_mymodule_data_t *d = piece->data;
d->exposure_scale = exp2f(p->exposure); // EV → linear multiplier
d->blending = p->blending / 100.0f; // percentage → 0-1 range
dt_iop_compute_spline(p, &d->spline); // solve spline
}int legacy_params(dt_iop_module_t *self,
const void *const old_params, const int old_version,
void **new_params, int32_t *new_params_size, int *new_version)
{
if(old_version == 1)
{
typedef struct { float exposure; } v1_params_t;
const v1_params_t *o = old_params;
dt_iop_mymodule_params_t *n = malloc(sizeof(dt_iop_mymodule_params_t));
n->exposure = o->exposure;
n->gamma = 1.0f; // default for new field
n->method = METHOD_AUTO;
*new_params = n;
*new_params_size = sizeof(dt_iop_mymodule_params_t);
*new_version = 2;
return 0;
}
return 1; // Unknown version
}If the module can use the GPU, implement process_cl() wrapped in #ifdef HAVE_OPENCL. OpenCL kernels are loaded in init_global() and accessed via self->global_data. The pipeline falls back to CPU process() on failure. See existing modules (e.g., exposure.c, sharpen.c) for complete examples.
If IOP_FLAGS_ALLOW_TILING is set, the pixelpipe is allowed to process a piece in tiling mode, if some parameters don't allow tiling override this in commit_params().
For calculation of memory requirements and tile aligning we have tiling_callback(), if not provided defaults are used as in default_tiling_callback()
Whenever a module possibly exceeds requirements as defined in default_tiling_callback() or requires special aligning a specific tiling_callback() should be provided for three reasons:
a) the tiling process will not allocate more memory than granted
b) the OpenCL code path will not be tried if requirements are too high thus avoiding costly late fallbacks to CPU path.
c) tile stitching will be correct for alignment
| Field | Purpose |
|---|---|
factor / factor_cl |
Total CPU/GPU memory as a multiple of input buffer size |
maxbuf / maxbuf_cl |
Largest single temporary buffer as a multiple of input size |
overhead |
Fixed memory overhead in bytes |
overlap |
Pixels of overlap between adjacent tiles (for spatial filters) |
align |
Tile origin alignment (1 = none, other values only for special algorithms) |
An example
void tiling_callback(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
const dt_iop_roi_t *roi_in, const dt_iop_roi_t *roi_out,
dt_develop_tiling_t *tiling)
{
tiling->factor = 2.5f; // input + output + 2 single channel temp buffers
tiling->factor_cl = 3.75f; // as above but we need an additional rgb buffer plus a single channel buffer for a mask
tiling->maxbuf = 1.0f;
tiling->maxbuf_cl = 1.0f;
tiling->overhead = 0;
tiling->overlap = 4; // 4-pixel overlap for a 3×3 kernel
tiling->align = 1; // no special care for sensor patterns
}For modules that change image geometry (crop, rotate, lens correction), implement modify_roi_in(), modify_roi_out(), distort_transform(), distort_backtransform(), and optionally distort_mask(). See src/iop/iop_api.h for signatures and modules like ashift.c for examples.
Module Load:
init_global() → [once per module type]
Image Open:
init() → reload_defaults() → gui_init()
Pixelpipe Creation (per pipe):
init_pipe() [allocates piece->data]
Params Change:
gui_update() → gui_changed()
User Edits Widget:
[auto-callback] → gui_changed()
→ commit_params() [transforms self->params → piece->data]
→ process() [reads piece->data]
Image Switch:
reload_defaults() → gui_update() → gui_changed()
Darkroom Exit:
gui_cleanup() → cleanup_pipe() [per pipe] → cleanup()
Module Unload:
cleanup_global()