Skip to content

agx: target white above 100% for HDR output - #22661

Closed
nicknezis wants to merge 7 commits into
darktable-org:masterfrom
nicknezis:agx-hdr-target-white
Closed

nicknezis wants to merge 7 commits into
darktable-org:masterfrom
nicknezis:agx-hdr-target-white

Conversation

@nicknezis

Copy link
Copy Markdown

agx: target white above 100% for HDR output

Branch: nicknezis:agx-hdr-target-white

Summary

AgX capped target white at 100%, which is the 203 nit reference white of a PQ or HLG export, so HDR exports could not hold highlights above SDR white without a negative exposure hack. Target white can now go up to 5000% (about 10000 nits), and the curve graph rescales with guides at 1x to 32x. An optional "show target white in nits" toggle displays and accepts the value in nits, for example 1000 nits instead of 493%. The tooltip explains that target white is the peak output brightness. A warning icon appears on the slider when the export profile is HLG and the value is above about 493%, because HLG clips at its 1000 nit nominal peak.

Referenced issue

Follows #22516, which fixed #20562 by putting PQ reference white at 203 nits. That left AgX's 100% cap at 203 nits, so it could no longer reach HDR highlights.

Related: #20562
Related: #17710

Checklist

  • I have read CONTRIBUTING.md and the coding style.
  • I have not merged master into the topic branch.
  • The pull request is one logical change, and every commit compiles on its own.
  • I ran the relevant tests: unit tests, src/tests/integration/ where the pixelpipe is touched, or darktable-cli as a headless smoke test.
  • New user-visible strings use _(), new preferences are registered in data/darktableconfig.xml.in.
  • A RELEASE_NOTES.md entry was added (only needed if fixing an issue in a release). Do not reference GitHub issues.

Test instructions

  1. Open a raw, enable AgX, and set the export profile to PQ Rec2020 or HLG Rec2020.
  2. In AgX, open "advanced curve parameters" and set target white to 493% (about 1000 nits). Export to AVIF or JXL. Highlights should land near 1000 nits with no exposure change.
  3. Tick "show target white in nits". The slider should read 1001 nits and accept typed nit values.
  4. With the HLG export profile, raise target white to 800%. A warning icon should appear on the slider, and it should disappear at 493% or with a PQ profile.

Done so far:

  • every commit builds (git rebase --exec 'cmake --build build' master)
  • a darktable-cli PQ export of a synthetic -10 to +8 EV ramp: 203 nits lands at PQ code 148 and 1000 nits at code 192
  • integration tests 0174 and 0175 pass on CPU
  • the GUI was checked under Xvfb on Linux

Not verified: OpenCL, real raws, macOS and Windows. The default params and existing edits are unchanged, since only the upper limit moved.

AI assistance

The code, commit messages and this description were written with Claude Code. I reviewed the diff.

The shoulder of the AgX curve was capped at linear 1.0, so a PQ or HLG
export could never place highlights above the 203 nit reference white
that colorout maps 1.0 to. Users had to pull exposure down and lose the
mid-tone placement AgX was tuned for.

Raise the target white limit to 50 (5000%, about 10000 nits) while
keeping the 50-100% soft range, so SDR edits are unchanged. The curve
math already handles a shoulder ending above 1; the graph now scales
its y axis to fit and adds guides above 100%.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Hf5bYk6AYsiHXNmKFEr15
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Hf5bYk6AYsiHXNmKFEr15
With HDR headroom, target white is easier to reason about as display
luminance than as a percentage of reference white. Add a "show target
white in nits" toggle under the slider. It only changes the slider's
display factor and unit (203 nits per 100%), so typed values are taken
in nits too and the stored params are unchanged. The choice is kept in
conf, and the curve graph's guide labels follow it.

Labels are now placed top-down and skipped when they would overlap, so
the guides squeezed together below a raised shoulder no longer print
on top of each other.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Hf5bYk6AYsiHXNmKFEr15
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Hf5bYk6AYsiHXNmKFEr15
With HDR headroom, "target white" is the curve's peak output rather
than the reference white, which stays at 203 nits. The label is kept
so existing shortcuts, translations and the manual still match; the
tooltip now says what the slider controls and how it maps to nits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Hf5bYk6AYsiHXNmKFEr15
HLG is encoded through the BT.2100 1000 nit OOTF, so a target white
above about 493% is not rolled off by the curve but clipped flat in
dt_hdr_hlg_encode(). Nothing told the user this was happening.

Show the warning icon on the target white slider when the export
profile is HLG and the target white exceeds the HLG peak, refresh it
when the export profile changes, and mention the limit in the tooltip.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Hf5bYk6AYsiHXNmKFEr15
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Hf5bYk6AYsiHXNmKFEr15
@TurboGit

Copy link
Copy Markdown
Member

@nicknezis : You have created a set of PR with the same goal, supporting HDR. In each PR you have a description and I think I understand them. But can you please give me an explanation of the full picture, what is your goal at the end? What's are you trying to achieve/fix on the HDR side? This would help me for the review. TIA.

@nicknezis

Copy link
Copy Markdown
Author

@nicknezis : You have created a set of PR with the same goal, supporting HDR. In each PR you have a description and I think I understand them. But can you please give me an explanation of the full picture, what is your goal at the end? What's are you trying to achieve/fix on the HDR side? This would help me for the review. TIA.

Thanks for asking, and sorry for landing six PRs at once without a map. Here is the whole picture.

The goal

Take a raw, edit it once, and export it both as a normal SDR file and as an HDR file (PQ or HLG) that uses the extra brightness of an HDR screen, with no exposure hacks and without changing the history per target. Later, the same mechanism should allow gain-map exports (one file that looks right on SDR and HDR screens) and HDR display in the darkroom.

What is broken today

#22516 fixed the PQ/HLG encoding so that 1.0 in the pipe is the 203 nit reference white (BT.2408). That is correct, but it means anything above reference white has to come out of the tone mapper as values above 1.0, and none of our tone mappers really do that:

  • AgX caps target white at 100%, so an AgX edit exported to PQ peaks at 203 nits. It is an SDR image in an HDR container.
  • sigmoid caps at 1600% (about 3250 nits), while PQ goes to 10000.
  • filmic accepts up to 1600%, but its graph clips above 100%, so you cannot see what it does.
  • Even where a value above 100% is possible, the user has to type the right number for each target (100% for SDR, about 493% for a 1000 nit HLG export) and edit the history to export the same image for another target.
  • There is no way to see in the darkroom where highlights will land in nits, and the JPEG XL export claimed every image peaked at 10000 nits.

Before #22516 people worked around this by pulling exposure down by several EV, which is the hack I wanted to get rid of.

The PRs, grouped by what they fix

  1. Let the tone mappers reach above reference white (manual, per edit):
  2. Take the peak from the output instead of the history:
    • agx, sigmoid: follow the output's peak for HDR export #22669 (fixes Output headroom: let tone mappers take their peak from the output #22664): a per-pipe "output headroom" (output peak / 203 nits), set with the output profile: 1 for SDR, 1000/203 for HLG, and a preference for PQ (default 1000 nits). AgX and sigmoid get a "target white from output" option, so one history exports correctly to SDR, HLG and PQ. After kofa73's review, the AgX part now follows the HDR design of Blender 5's AgX: shadows and midtones keep their SDR brightness, and only the highlights extend to the peak. This is the PR I care most about, because everything further down the road depends on it.
  3. Let users see HDR highlights on an SDR screen:
    • Scopes hdr scale #22662: histogram, waveform and parade can switch between SDR, PQ (in nits) and HLG scales, like the scope scales in Resolve.
  4. Describe the result correctly to viewers:

Each one is based on master and can be reviewed and merged on its own. #22661 and #22669 both touch AgX, so whichever goes in second will need a rebase.

How #22661 relates to #22669

#22661 was the first, simpler step: a manual slider. kofa73's review of #22669 showed that AgX's curve maths was built for a white of at most 1.0, so pushing target white well above 100% (above about 600 nits) makes the shoulder fall back to a shape that steepens near the top. The Blender-style rework in #22669 avoids that by keeping the curve on 0..1 and adding the HDR step after it. The large values #22661 allows would run into the same problem. So I think #22669 is the better route for AgX, and #22661 is worth keeping only if a manual peak (without the output option) is wanted. I'm happy to close #22661, or cut it down to what still makes sense on top of #22669, whichever you prefer.

Where it goes next (not in these PRs)

Suggested review order

#22669 first, since it carries the design. Then #22662 (independent). Then #22668 and #22667, which are small. #22661 last, or closed, depending on what you think of the above.

@TurboGit

Copy link
Copy Markdown
Member

@nicknezis : Thanks, I'm starting to understand, still some question. You said :

Take a raw, edit it once, and export it both as a normal SDR file and as an HDR file (PQ or HLG) that uses the extra brightness of an HDR screen, with no exposure hacks and without changing the history per target.

But if I understand well taking the description on Sigmoid PR #22668 you still need to tweak the "target white" to get a proper export in "Rec2020 PQ". So you don't really ensure the "edit it once" and export SDR & HDR ?

If this is true maybe we can design an automatic way of supporting that ?

@nicknezis

Copy link
Copy Markdown
Author

@nicknezis : Thanks, I'm starting to understand, still some question. You said :

Take a raw, edit it once, and export it both as a normal SDR file and as an HDR file (PQ or HLG) that uses the extra brightness of an HDR screen, with no exposure hacks and without changing the history per target.

But if I understand well taking the description on Sigmoid PR #22668 you still need to tweak the "target white" to get a proper export in "Rec2020 PQ". So you don't really ensure the "edit it once" and export SDR & HDR ?

If this is true maybe we can design an automatic way of supporting that ?

@TurboGit You're right about #22668: on its own it only raises sigmoid's limit, so you would still set target white by hand for each target. The same is true of #22661 for AgX. Those two are the manual part.

The automatic way is #22669, and it is what makes "edit once" work:

  • The pipe gets an output headroom, which is the output's peak divided by the 203 nit reference white. It is set from the output profile when the pipe is built: 1 for any SDR profile, 1000/203 for HLG, and for PQ it comes from a new "peak luminance of PQ output" preference (default 1000 nits). Export, the darkroom and darktable-cli all get it the same way. It is also part of the pipe cache hash.
  • AgX and sigmoid get a "target white from output" option. When it is on, the module ignores its target white slider and uses the pipe's headroom instead. In SDR the headroom is 1, so the result is the same as today's 100%.
  • So you tick that option once in the edit, and the same history exports to sRGB, HLG Rec2020 and PQ Rec2020 with the right peak for each, with nothing to change between exports. The output profile is the only thing you pick.

The option is off by default, and existing edits are migrated with it off, so nobody's images change. If you would rather have it on by default for new edits, or set by a preference, I'm happy to do that. That is the main design choice I'd like your view on.

Given that, I don't think #22661 and #22668 are needed for the HDR workflow any more. They only matter for someone who wants a fixed peak typed in by hand, and for AgX that path also has the shoulder problem kofa73 found above about 600 nits. I'd suggest focusing review on #22669 and closing #22661 and #22668, unless you see a use for the manual sliders.

@TurboGit

Copy link
Copy Markdown
Member

Ok, the fog is starting to dissipate :)

  • I have not yet looked at the PR, so that's mostly design at this point.
  • I don't see the HDR option "target white from output" on AgX & Sigmoid good because you need to tick it to allow HDR and when you tick it it is still possible to to export SDR & HDR and this is anyway an export option. Ticking it will never change the darkroom render.
  • For a end-user wanting to export HDR the option should be in the export module actually.
  • Let's say that if the export is set to HDR then an hidden setting is set to TRUE. It is then the responsibility to the tone mappers to look for this setting and to act accordingly.
  • We need to support all tone mapper, AgX, Sigmoid, Filmic. Not sure about Spektrafilm.

Do you think this makes sense?

Let's continue the design before changing code at the moment, I really need to get a clear view of all this which is probably not yet the case at the moment. Thanks for your patience.

@wpferguson

Copy link
Copy Markdown
Member

@nicknezis what PR's do we need to pull so that we can test the whole thing. I have some HDR images, PQ and otherwise, that I can test with.

@nicknezis

Copy link
Copy Markdown
Author

Ok, the fog is starting to dissipate :)

  • I have not yet looked at the PR, so that's mostly design at this point.
  • I don't see the HDR option "target white from output" on AgX & Sigmoid good because you need to tick it to allow HDR and when you tick it it is still possible to to export SDR & HDR and this is anyway an export option. Ticking it will never change the darkroom render.
  • For a end-user wanting to export HDR the option should be in the export module actually.
  • Let's say that if the export is set to HDR then an hidden setting is set to TRUE. It is then the responsibility to the tone mappers to look for this setting and to act accordingly.
  • We need to support all tone mapper, AgX, Sigmoid, Filmic. Not sure about Spektrafilm.

Do you think this makes sense?

Let's continue the design before changing code at the moment, I really need to get a clear view of all this which is probably not yet the case at the moment. Thanks for your patience.

@TurboGit Yes, it makes sense, and I agree we should settle the design before more code. Here is how I'd refine your proposal, plus the points I think still need a decision.

1. Agreed: the export decides, not a toggle in each module

The user picks an HDR output profile (and a peak for PQ), and the tone mappers adapt on their own. No per-module tick box. That is a better UX than what #22669 has now.

2. The hidden setting should be a number, not a boolean

The tone mapper needs to know how much room there is above reference white, not just "HDR yes/no":

  • PQ: depends on the target display (1000 nits is typical, but 4000 is common for mastering).
  • HLG: fixed by the standard, 1000 nits.
  • Gain maps later: one export renders the same history twice, at 1 (SDR) and at N (HDR).

So I'd carry a "headroom" value on the pipe: output peak / 203 nits. It is 1 for every SDR output, so for SDR nothing changes. The pipe already has this in #22669 (pipe->output_headroom, set together with the output profile and included in the cache hash); only the module side would change.

3. Where it is set

  • Export module: when the output profile is PQ, show a "peak luminance" setting (default 1000 nits). HLG needs none. This replaces the preference agx, sigmoid: follow the output's peak for HDR export #22669 adds in the processing tab.
  • darktable-cli: an equivalent option.
  • Darkroom: from the display profile. One correction on "ticking it will never change the darkroom render": that is true with an SDR display profile, but if the display profile is PQ or HLG, the darkroom preview gets the same headroom as an export. That is also where native HDR display (Request: HDR viewers on macOS #17710) would plug in later. So I'd keep the value tied to "the output of this pipe" rather than to export only.

4. Existing edits (the main open question)

Some users already make HDR exports by hand: sigmoid or filmic with target white above 100%, or the old negative exposure trick. If tone mappers start following the headroom unconditionally, those edits would change on their next HDR export. Options:

  • (a) New module versions add a hidden "follow output" flag: on for new instances, off for migrated edits. No UI for it, or tucked away for the rare user who wants a fixed peak.
  • (b) Always follow the headroom and accept that old manual HDR edits change.

I lean towards (a), since it never changes an existing image, but I'd like your view.

5. What target white means afterwards

I'd keep target white as the SDR value the user edits (100% by default), and let the module scale it to the headroom for HDR output. The slider keeps working for SDR, and one edit gives both.

6. Which tone mappers

  • AgX: follows the HDR design of Blender 5's AgX (already done in agx, sigmoid: follow the output's peak for HDR export #22669): shadows and midtones keep their SDR brightness, highlights roll into the peak.
  • sigmoid: target white x headroom. kofa73 found the curve fit overflows at extreme settings above about 1600%, so it needs either that cap or a more robust fit.
  • filmic: possible for v6/v7, but it needs real design work. kofa73 found its spline raises midtone contrast to reach a higher white, and v3-v5 clip at 1.0. I'd do it as a separate step after AgX and sigmoid, and leave v5 and older as SDR.
  • spektrafilm: it simulates a print, and paper white is its maximum by construction, so there is no natural "above white". I'd leave it SDR unless its author sees a meaningful HDR mode (perhaps the "scan the film" path).
  • base curve and tone curve: display-referred, so they stay SDR.

Modules placed after the tone mapper would also need a check that nothing clips at 1.0 when the headroom is above 1.

7. Previewing HDR on an SDR screen

With an SDR display, the darkroom renders at headroom 1, so the HDR scopes in #22662 would show the SDR signal. To judge an HDR export on an SDR screen, the scopes would need the export's headroom. That could be a scope option later; it doesn't block the rest.

Proposed next step

If this matches what you have in mind, I'll rework #22669 to (1) drop the per-module toggles, (2) move the PQ peak into the export module, and (3) implement whichever of 4a/4b you prefer, for AgX and sigmoid first, with filmic as a follow-up. #22661 and #22668 (the manual sliders) would then no longer be needed.

@nicknezis

Copy link
Copy Markdown
Author

@nicknezis what PR's do we need to pull so that we can test the whole thing. I have some HDR images, PQ and otherwise, that I can test with.

#22666 (has been merged)
#22661
#22662
#22669
The others might be OBE based on the design discussion with TurboGit.

@TurboGit

Copy link
Copy Markdown
Member

I think all is clear now, and I'm happy to see that we have a good design coming.

  1. Perfect !
  2. Understood, a number is fine by me.
  3. Yes, makes sense.
  4. Indeed, I would also lean toward (a), we don't want to break existing edits.
  5. The one edit goal is what I was looking for. Good!
  6. Ok for AgX & Sigmoid first and Filmic later. "Modules placed after the tone mapper would also need a check that nothing clips at 1.0 when the headroom is above 1." indeed, 4 of them grain, vignetting, framing, watermark. Note that grain is going to have a scene-referred version (RFC: add scene-referred grain module #22311 not yet looked at it) and will then move below tone-mappers.
  7. Ok.

I think we are good now and your new proposal is sounds to me.

@nicknezis

Copy link
Copy Markdown
Author

I think all is clear now, and I'm happy to see that we have a good design coming.

  1. Perfect !
  2. Understood, a number is fine by me.
  3. Yes, makes sense.
  4. Indeed, I would also lean toward (a), we don't want to break existing edits.
  5. The one edit goal is what I was looking for. Good!
  6. Ok for AgX & Sigmoid first and Filmic later. "Modules placed after the tone mapper would also need a check that nothing clips at 1.0 when the headroom is above 1." indeed, 4 of them grain, vignetting, framing, watermark. Note that grain is going to have a scene-referred version (RFC: add scene-referred grain module #22311 not yet looked at it) and will then move below tone-mappers.
  7. Ok.

I think we are good now and your new proposal is sounds to me.

@TurboGit Thanks, that's very helpful. I've reworked #22669 to this design:

  • The PQ peak is now a "PQ peak" setting in the export module, shown only for PQ profiles (and "image settings"). The preference in the processing tab is gone.
  • AgX and sigmoid no longer have a per-module option. A hidden param makes them follow the output; it is on for new instances, the auto-applied defaults and the presets, and off for migrated edits, so existing images don't change (your 4a). SDR output is bit-identical either way.
  • Target white stays the SDR value you edit. AgX treats it as relative to the peak; sigmoid multiplies it by the headroom, up to 1600%.
  • For an old edit, the module shows a "follow output peak" toggle while it is off, so a user can opt that edit in. That's the only UI left for it. Tell me if you'd rather have no UI at all.
  • On the modules after the tone mapper: from reading the code, vignetting, framing and watermark don't clip above 1.0 (vignette only with its legacy "unbound" off). Grain doesn't clip either, but its LUT stops at L = 100, so grain fades out above reference white. Since grain is moving before the tone mappers with RFC: add scene-referred grain module #22311, I've left it alone.

Filmic will follow in its own PR. With this in, #22661 (AgX manual slider) and #22668 (sigmoid manual slider) aren't needed for HDR export, so I'll close them unless you see a use for them.

@nicknezis

Copy link
Copy Markdown
Author

Closing in favor of #22669, which takes the peak from the output instead of a manual target white, following the design agreed in #22661.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PQ and HLG transfer function problems (too dark import, too bright export)

4 participants