Split out from #90 — A.1 was deferred during the v1.8.0 Bucket A pass because it has a meaningfully different cost shape from the other A items.
Why this needs its own issue
The other Bucket A items (A.2 dehaze, A.3 tint, A.5 Color Grading, A.6 Texture) all decode short flat structs (16–60 bytes, scalar fields). The tonecurve mv5 struct is qualitatively different:
```c
typedef struct dt_iop_tonecurve_params_t
{
dt_iop_tonecurve_node_t tonecurve[3][20]; // 3 channels x 20 nodes x (x,y) = 480 bytes
int tonecurve_nodes[3]; // active node count per channel = 12 bytes
int tonecurve_type[3]; // curve interpolation type per channel = 12 bytes
dt_iop_tonecurve_autoscale_t tonecurve_autoscale_ab; // 4 bytes
int tonecurve_preset; // 4 bytes
int tonecurve_unbound_ab; // 4 bytes
dt_iop_rgb_norms_t preserve_colors; // 4 bytes
} dt_iop_tonecurve_params_t;
```
Total: 520 bytes, with the dominant payload being 60 (x,y) float pairs.
Why darktable session is needed
Constructing a 520-byte tonecurve op_params from struct knowledge alone is risky:
- The 60 node slots are mostly inactive —
tonecurve_nodes[i] says how many of the 20 are real. The unused-slot bytes might be zero, might be uninitialized garbage, might be set by darktable on save. No empirical baseline = no way to verify.
- The
tonecurve_type[i] enum + tonecurve_autoscale_ab enum + preserve_colors enum each have several legal values. The safe-to-ship default isn't obvious from the source.
- Curve nodes need to be photographically meaningful. A decoder that pins arbitrary node positions wouldn't pass visual review; we want presets a photographer would recognize (S-curve, lifted shadows, filmic etc).
The clean path is: open darktable's GUI, draw 3-4 known curves (identity, gentle S-curve, lifted shadows, filmic), export each as .dtstyle, capture the bytes. Those become both the empirical baseline for the decoder AND the discrete vocabulary entries. Then the parameterized version can synthesize curves between them.
Scope
- Capture baselines from darktable GUI — author 4 reference dtstyles in darktable, export, drop into
vocabulary/packs/expressive-baseline/layers/L3/tonecurve/:
tone_identity.dtstyle — straight-line baseline (bytes for an unmodified curve)
curve_filmic_subtle.dtstyle — mild S-curve
curve_filmic_strong.dtstyle — stronger S-curve
curve_lifted_shadows.dtstyle — lifted blacks, rolled highlights
- Reverse-engineer the 520-byte struct against these baselines — diff the bytes between identity and each preset to confirm offsets for the L curve's first few nodes.
- Build the decoder at
src/chemigram/core/parameterize/tonecurve.py with at minimum a "node-pull" parameterization (e.g. --param shadow_pull=V moves the low-end node up/down by V).
- Register in
_PATCH_REGISTRY, _modversion_drift, and _AXIS_FIELD_INDICES-style table if the patch function would otherwise hit C901.
- Ship the parameterized
tonecurve entry with at least one axis (likely s_curve_strength or shadow_pull).
- 5-layer coverage per ADR-080.
What to expect on ship
- Brings Lightroom daily-use parity from 19/23 to 20/23.
- 14th Path C decoder.
- 31st parameterized entry.
- This issue's complexity is real but bounded — once the empirical baselines exist, the decoder shape is well-understood from prior bytes-diff exercises (saw it work cleanly on toneequalizer and colorbalancergb, both larger structs than tonecurve).
Reference
Split out from #90 — A.1 was deferred during the v1.8.0 Bucket A pass because it has a meaningfully different cost shape from the other A items.
Why this needs its own issue
The other Bucket A items (A.2 dehaze, A.3 tint, A.5 Color Grading, A.6 Texture) all decode short flat structs (16–60 bytes, scalar fields). The tonecurve mv5 struct is qualitatively different:
```c
typedef struct dt_iop_tonecurve_params_t
{
dt_iop_tonecurve_node_t tonecurve[3][20]; // 3 channels x 20 nodes x (x,y) = 480 bytes
int tonecurve_nodes[3]; // active node count per channel = 12 bytes
int tonecurve_type[3]; // curve interpolation type per channel = 12 bytes
dt_iop_tonecurve_autoscale_t tonecurve_autoscale_ab; // 4 bytes
int tonecurve_preset; // 4 bytes
int tonecurve_unbound_ab; // 4 bytes
dt_iop_rgb_norms_t preserve_colors; // 4 bytes
} dt_iop_tonecurve_params_t;
```
Total: 520 bytes, with the dominant payload being 60 (x,y) float pairs.
Why darktable session is needed
Constructing a 520-byte tonecurve op_params from struct knowledge alone is risky:
tonecurve_nodes[i]says how many of the 20 are real. The unused-slot bytes might be zero, might be uninitialized garbage, might be set by darktable on save. No empirical baseline = no way to verify.tonecurve_type[i]enum +tonecurve_autoscale_abenum +preserve_colorsenum each have several legal values. The safe-to-ship default isn't obvious from the source.The clean path is: open darktable's GUI, draw 3-4 known curves (identity, gentle S-curve, lifted shadows, filmic), export each as
.dtstyle, capture the bytes. Those become both the empirical baseline for the decoder AND the discrete vocabulary entries. Then the parameterized version can synthesize curves between them.Scope
vocabulary/packs/expressive-baseline/layers/L3/tonecurve/:tone_identity.dtstyle— straight-line baseline (bytes for an unmodified curve)curve_filmic_subtle.dtstyle— mild S-curvecurve_filmic_strong.dtstyle— stronger S-curvecurve_lifted_shadows.dtstyle— lifted blacks, rolled highlightssrc/chemigram/core/parameterize/tonecurve.pywith at minimum a "node-pull" parameterization (e.g.--param shadow_pull=Vmoves the low-end node up/down by V)._PATCH_REGISTRY,_modversion_drift, and_AXIS_FIELD_INDICES-style table if the patch function would otherwise hit C901.tonecurveentry with at least one axis (likelys_curve_strengthorshadow_pull).What to expect on ship
Reference
docs/capability-survey.md§ 13.src/iop/tonecurve.cdt_iop_tonecurve_params_tv5.