Add dehaze as a member of the compositional detail family.
Why
Named four times across core/dr-pipeline/src/detail.rs, core/dr-gpu/src/detail.rs, ops/README.md and ops/capture_sharpen.rs as a member of the family clarity and texture belong to — and absent from the tree. The documentation already implies it exists.
It also belongs before the colour block in the chain, because dehaze shifts colour and the colour work should be correcting a near-final state. That is a placement decision worth recording now rather than discovering later.
Acceptance
A DetailStage node with its radius expressed in RenderScale::frame_fraction, alongside clarity and texture — never in raw pixels.
Ordered before the colour block, not after it.
Declared in ops/ with rust: and a why_rust: line, like the other neighbourhood nodes.
Priority
Low. Listed for completeness.
Part of the Develop Ergonomics spec (FR-DEV series proposal), 2026-09-05.
Add **dehaze** as a member of the compositional detail family.
## Why
Named four times across `core/dr-pipeline/src/detail.rs`, `core/dr-gpu/src/detail.rs`, `ops/README.md` and `ops/capture_sharpen.rs` as a member of the family clarity and texture belong to — and absent from the tree. The documentation already implies it exists.
It also belongs **before** the colour block in the chain, because dehaze shifts colour and the colour work should be correcting a near-final state. That is a placement decision worth recording now rather than discovering later.
## Acceptance
- [ ] A `DetailStage` node with its radius expressed in `RenderScale::frame_fraction`, alongside clarity and texture — never in raw pixels.
- [ ] Ordered before the colour block, not after it.
- [ ] Declared in `ops/` with `rust:` and a `why_rust:` line, like the other neighbourhood nodes.
## Priority
Low. Listed for completeness.
---
Part of the Develop Ergonomics spec (FR-DEV series proposal), 2026-09-05.
Implemented on dev/dehaze (cce90dc). One acceptance criterion in this ticket was wrong and could not be met.
Ordered before the colour block, not after it.
That is architecturally impossible without splitting the fused pass, and the right call was not to force it.
The detail stage runs as a group after every point operation, because a neighbourhood pass is a separate dispatch reading a texture the fused pass has already finished writing. So an order: placing dehaze ahead of vibrance or colour_mixer would be, in ops/README.mds own phrase, a lie the chain cannot tell. Honouring it would mean splitting the fused pass in half around this one node — a second full-frame dispatch and intermediate on every edit in the catalogue, whether or not it uses dehaze.
The underlying observation stands: dehaze subtracts a grey term and rescales, so it does shift saturation wherever the veil is thick, and the colour controls would ideally be correcting the picture that leaves it. They cannot be. That cost is now written into the placement: block of ops/dehaze.yaml so it is not rediscovered as a bug.
Landed at order 125 instead — first among the compositional detail nodes: after noise reduction (dividing by a transmission below one amplifies noise by exactly the recovery factor) and before clarity and texture (coarse before fine).
Revised criterion: ordered first among the compositional detail nodes, with the colour interaction recorded rather than corrected.
Also noted for a separate ticket: core/dr-preset-xmp maps crs:Clarity and crs:Sharpness; Adobe has a crs:Dehaze and the mapping was deliberately left out of scope.
**Implemented on `dev/dehaze` (cce90dc). One acceptance criterion in this ticket was wrong and could not be met.**
> - [ ] Ordered before the colour block, not after it.
**That is architecturally impossible without splitting the fused pass**, and the right call was not to force it.
The detail stage runs as a *group* after every point operation, because a neighbourhood pass is a separate dispatch reading a texture the fused pass has already finished writing. So an `order:` placing dehaze ahead of `vibrance` or `colour_mixer` would be, in `ops/README.md`s own phrase, a lie the chain cannot tell. Honouring it would mean splitting the fused pass in half around this one node — a second full-frame dispatch and intermediate **on every edit in the catalogue**, whether or not it uses dehaze.
The underlying observation stands: dehaze subtracts a grey term and rescales, so it *does* shift saturation wherever the veil is thick, and the colour controls would ideally be correcting the picture that leaves it. They cannot be. That cost is now written into the `placement:` block of `ops/dehaze.yaml` so it is not rediscovered as a bug.
**Landed at order 125** instead — first among the compositional detail nodes: after noise reduction (dividing by a transmission below one amplifies noise by exactly the recovery factor) and before clarity and texture (coarse before fine).
Revised criterion: *ordered first among the compositional detail nodes, with the colour interaction recorded rather than corrected.*
Also noted for a separate ticket: `core/dr-preset-xmp` maps `crs:Clarity` and `crs:Sharpness`; Adobe has a `crs:Dehaze` and the mapping was deliberately left out of scope.
Built and rendered. It works, and there is one visible defect at full strength.
dev/dehaze compiles clean (cargo build -p dr-gpu --release, exit 0) — 742 lines of hand-written node and five WGSL passes, first pass through the compiler. The node registers in the chain and sweeps correctly: dehaze / amount, -100 .. 100, default 0.
Rendered a −100 → +100 sweep over a real backlit frame. Negative adds haze, zero is neutral, positive recovers the distance. The effect is real and asymmetric, which is right for a frame that already has some veil in it.
The defect: halos at high amount. At +100 there are visible light fringes around the subjects where they meet the sky, and the sky itself goes flat grey. Measured against the neutral frame, the maximum local difference grows faster than the mean:
amount
max diff
mean diff
ratio
40
0.081
0.013
6.2
60
0.143
0.021
6.9
80
0.239
0.030
8.0
100
0.446
0.096
4.7
A halo is exactly that signature — a large excursion confined to a boundary while the frame as a whole moves much less.
Likely cause, and it is a known one. The transmission map comes from a dark-channel minimum filter (four erosions) and is used unrefined. The dark channel is constant across a patch, so at an object boundary the estimate is wrong on the near side by roughly the patch width, and dividing by it lifts a band around the subject. The standard answer in the literature is to refine the transmission before dividing — soft matting in He et al., a guided filter in the usual fast version — and that step is not present.
Not a blocker for the branch. It is correct at moderate amounts, which is where the control will live. Suggested follow-ups, either here or as a new ticket:
Refine the transmission with a guided filter before the recovery pass.
Until then, consider whether the slider should reach a strength that visibly halos on an ordinary frame, or whether the useful range is narrower than −100..100.
Also worth noting from the build: the pipeline refused to render this node through the fused path at all, with the error naming the fix — this shader was composed with a detail stage and writes linear working values; render it with render_detailed. The architecture caught a caller doing the wrong thing rather than producing a wrong picture.
**Built and rendered. It works, and there is one visible defect at full strength.**
`dev/dehaze` compiles clean (`cargo build -p dr-gpu --release`, exit 0) — 742 lines of hand-written node and five WGSL passes, first pass through the compiler. The node registers in the chain and sweeps correctly: `dehaze / amount, -100 .. 100, default 0`.
Rendered a −100 → +100 sweep over a real backlit frame. Negative adds haze, zero is neutral, positive recovers the distance. The effect is real and asymmetric, which is right for a frame that already has some veil in it.
**The defect: halos at high amount.** At +100 there are visible light fringes around the subjects where they meet the sky, and the sky itself goes flat grey. Measured against the neutral frame, the maximum local difference grows faster than the mean:
| amount | max diff | mean diff | ratio |
|---|---|---|---|
| 40 | 0.081 | 0.013 | 6.2 |
| 60 | 0.143 | 0.021 | 6.9 |
| 80 | 0.239 | 0.030 | 8.0 |
| 100 | 0.446 | 0.096 | 4.7 |
A halo is exactly that signature — a large excursion confined to a boundary while the frame as a whole moves much less.
**Likely cause, and it is a known one.** The transmission map comes from a dark-channel minimum filter (four erosions) and is used **unrefined**. The dark channel is constant across a patch, so at an object boundary the estimate is wrong on the near side by roughly the patch width, and dividing by it lifts a band around the subject. The standard answer in the literature is to refine the transmission before dividing — soft matting in He et al., a guided filter in the usual fast version — and that step is not present.
**Not a blocker for the branch.** It is correct at moderate amounts, which is where the control will live. Suggested follow-ups, either here or as a new ticket:
- Refine the transmission with a guided filter before the recovery pass.
- Until then, consider whether the slider should reach a strength that visibly halos on an ordinary frame, or whether the useful range is narrower than −100..100.
Also worth noting from the build: the pipeline refused to render this node through the fused path at all, with the error naming the fix — *this shader was composed with a detail stage and writes linear working values; render it with `render_detailed`*. The architecture caught a caller doing the wrong thing rather than producing a wrong picture.
Re-rendered on a frame with real atmospheric haze (an alpine valley, RAW). Much clearer than the first test, and it narrows the finding usefully.
−100 adds veil convincingly.
0 neutral.
+60 is the good result: the distance clears, the far ridge gains contrast, nothing looks processed. This is where the control earns its place.
+100 breaks down. The sky collapses to a flat grey mass with a hard edge along the mountain ridge — not a soft halo but a visible boundary, with a patch of residual blue stranded inside it.
That is the unrefined transmission map showing itself directly. The dark channel is constant across its patch, so at a high-contrast silhouette the estimate steps rather than varies, and dividing by it reproduces the step in the output.
Revised suggestion, sharper than my earlier one: this is not a subtle quality issue to fix later, it is a visible artefact at the top of the declared range. Either
refine the transmission (guided filter) before the recovery pass, or
narrow what the slider can reach, so full deflection stays inside the range that behaves.
The second is cheap and honest; the first is the real fix. Worth deciding which before this branch merges.
**Re-rendered on a frame with real atmospheric haze** (an alpine valley, RAW). Much clearer than the first test, and it narrows the finding usefully.
- **−100** adds veil convincingly.
- **0** neutral.
- **+60** is the good result: the distance clears, the far ridge gains contrast, nothing looks processed. This is where the control earns its place.
- **+100** breaks down. The sky collapses to a flat grey mass with a **hard edge along the mountain ridge** — not a soft halo but a visible boundary, with a patch of residual blue stranded inside it.
That is the unrefined transmission map showing itself directly. The dark channel is constant across its patch, so at a high-contrast silhouette the estimate steps rather than varies, and dividing by it reproduces the step in the output.
**Revised suggestion**, sharper than my earlier one: this is not a subtle quality issue to fix later, it is a visible artefact at the top of the declared range. Either
- refine the transmission (guided filter) before the recovery pass, or
- narrow what the slider can reach, so full deflection stays inside the range that behaves.
The second is cheap and honest; the first is the real fix. Worth deciding which before this branch merges.
Landed in 81b1ae8: ops/dehaze.yaml with rust: and why_rust:, patch radius through RenderScale::frame_fraction, order 125 after noise reduction and before clarity and texture. The one acceptance item not met is placement before the colour block — the detail stage is a separate dispatch after every point operation, so an order ahead of vibrance would be a lie the chain cannot tell. The declaration records that instead.
Landed in 81b1ae8: ops/dehaze.yaml with rust: and why_rust:, patch radius through RenderScale::frame_fraction, order 125 after noise reduction and before clarity and texture. The one acceptance item not met is placement before the colour block — the detail stage is a separate dispatch after every point operation, so an order ahead of vibrance would be a lie the chain cannot tell. The declaration records that instead.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Add dehaze as a member of the compositional detail family.
Why
Named four times across
core/dr-pipeline/src/detail.rs,core/dr-gpu/src/detail.rs,ops/README.mdandops/capture_sharpen.rsas a member of the family clarity and texture belong to — and absent from the tree. The documentation already implies it exists.It also belongs before the colour block in the chain, because dehaze shifts colour and the colour work should be correcting a near-final state. That is a placement decision worth recording now rather than discovering later.
Acceptance
DetailStagenode with its radius expressed inRenderScale::frame_fraction, alongside clarity and texture — never in raw pixels.ops/withrust:and awhy_rust:line, like the other neighbourhood nodes.Priority
Low. Listed for completeness.
Part of the Develop Ergonomics spec (FR-DEV series proposal), 2026-09-05.
Implemented on
dev/dehaze(cce90dc). One acceptance criterion in this ticket was wrong and could not be met.That is architecturally impossible without splitting the fused pass, and the right call was not to force it.
The detail stage runs as a group after every point operation, because a neighbourhood pass is a separate dispatch reading a texture the fused pass has already finished writing. So an
order:placing dehaze ahead ofvibranceorcolour_mixerwould be, inops/README.mds own phrase, a lie the chain cannot tell. Honouring it would mean splitting the fused pass in half around this one node — a second full-frame dispatch and intermediate on every edit in the catalogue, whether or not it uses dehaze.The underlying observation stands: dehaze subtracts a grey term and rescales, so it does shift saturation wherever the veil is thick, and the colour controls would ideally be correcting the picture that leaves it. They cannot be. That cost is now written into the
placement:block ofops/dehaze.yamlso it is not rediscovered as a bug.Landed at order 125 instead — first among the compositional detail nodes: after noise reduction (dividing by a transmission below one amplifies noise by exactly the recovery factor) and before clarity and texture (coarse before fine).
Revised criterion: ordered first among the compositional detail nodes, with the colour interaction recorded rather than corrected.
Also noted for a separate ticket:
core/dr-preset-xmpmapscrs:Clarityandcrs:Sharpness; Adobe has acrs:Dehazeand the mapping was deliberately left out of scope.Built and rendered. It works, and there is one visible defect at full strength.
dev/dehazecompiles clean (cargo build -p dr-gpu --release, exit 0) — 742 lines of hand-written node and five WGSL passes, first pass through the compiler. The node registers in the chain and sweeps correctly:dehaze / amount, -100 .. 100, default 0.Rendered a −100 → +100 sweep over a real backlit frame. Negative adds haze, zero is neutral, positive recovers the distance. The effect is real and asymmetric, which is right for a frame that already has some veil in it.
The defect: halos at high amount. At +100 there are visible light fringes around the subjects where they meet the sky, and the sky itself goes flat grey. Measured against the neutral frame, the maximum local difference grows faster than the mean:
A halo is exactly that signature — a large excursion confined to a boundary while the frame as a whole moves much less.
Likely cause, and it is a known one. The transmission map comes from a dark-channel minimum filter (four erosions) and is used unrefined. The dark channel is constant across a patch, so at an object boundary the estimate is wrong on the near side by roughly the patch width, and dividing by it lifts a band around the subject. The standard answer in the literature is to refine the transmission before dividing — soft matting in He et al., a guided filter in the usual fast version — and that step is not present.
Not a blocker for the branch. It is correct at moderate amounts, which is where the control will live. Suggested follow-ups, either here or as a new ticket:
Also worth noting from the build: the pipeline refused to render this node through the fused path at all, with the error naming the fix — this shader was composed with a detail stage and writes linear working values; render it with
render_detailed. The architecture caught a caller doing the wrong thing rather than producing a wrong picture.Re-rendered on a frame with real atmospheric haze (an alpine valley, RAW). Much clearer than the first test, and it narrows the finding usefully.
That is the unrefined transmission map showing itself directly. The dark channel is constant across its patch, so at a high-contrast silhouette the estimate steps rather than varies, and dividing by it reproduces the step in the output.
Revised suggestion, sharper than my earlier one: this is not a subtle quality issue to fix later, it is a visible artefact at the top of the declared range. Either
The second is cheap and honest; the first is the real fix. Worth deciding which before this branch merges.
Landed in
81b1ae8: ops/dehaze.yaml with rust: and why_rust:, patch radius through RenderScale::frame_fraction, order 125 after noise reduction and before clarity and texture. The one acceptance item not met is placement before the colour block — the detail stage is a separate dispatch after every point operation, so an order ahead of vibrance would be a lie the chain cannot tell. The declaration records that instead.