Repository navigation
fix(engine): SDR content in HLG exports sits at reference white, not peak - #5749
Merged
Merged
Conversation
…peak The sRGB to HLG conversion for DOM layers fed SDR-relative light straight into the HLG OETF, whose input is light relative to the HLG peak, so SDR white landed at 100% signal. Graded HLG footage (graded as SDR, then converted back) and every white overlay were pushed to peak and blew out. SDR white now sits at 75% HLG signal (BT.2408), the same model the PQ branch uses with 203 nits.
Contributor
Edit accuracy: accurate 271 (base branch 271), smooth 181 of thoseThe gate passes. Quarantined, measured but not gated (0) |
vanceingalls
approved these changes
Oct 19, 2026
vanceingalls
left a comment
Collaborator
There was a problem hiding this comment.
Review at 790a486f.
Strengths
alphaBlit.ts:308: the fix derives the scale from the HLG inverse OETF at 75% signal rather than hard-coding a constant. The scene light it gives is 0.26496, which is 203.15 nits on a 1000-nit display through the γ1.2 OOTF, matching BT.2408. HLG now follows the same reference-white model as the PQ branch's 203 nits.- The Python generator mirrors the change exactly. I ran
generate-lut-reference.py --probesfrom this head and its output is byte-identical to the regeneratedSRGB_TO_HDR_REFERENCE(sRGB 255 → 49151 = 0.75 × 65535, 128 → 27146).
Verification
- Math: sRGB 18% grey lands at 37.8% HLG signal, which matches BT.2408's 38% grey. Reference white at 75% is 721 as a 10-bit limited-range code, consistent with the measured 724 in the body.
- PQ and SDR paths unchanged: only the
transfer === "hlg"branch ofbuildSrgbToSignalLutchanged. Every PQ probe value in the table is identical, thesrgbbranch is untouched, and no encoder or service file is in the diff. - Colour metadata: unchanged and correct for HLG.
chunkEncoder.ts:436-446andstreamingEncoder.ts:404-412writebt2020nc/bt2020/arib-std-b67/tv. - Sibling sites: the other HLG OETF, in
shaderTransitions.ts:47, round-trips pixels that are already HLG signal (signal → linear → signal). It never takes SDR-relative light, so it doesn't need the scale. - SDR clips: these are never in
nativeHdrVideoIds(extractVideosStage.ts:437). They reach the canvas through the DOM capture andblitRgba8OverRgb48le, so the fix covers them too. - Tests: the byte-exact probe table plus the white, 50%-alpha, near-opaque and asymmetric-channel tests all pin 49151. The BT.2408 invariant test asserts both transfers. The goldens are unchanged, and the body explains why: the HLG fixture renders no SDR content.
Notes (non-blocking)
- A graded HLG clip still goes HDR → SDR (tone map) → grade → SDR → HLG, and those two conversions aren't inverses. The body's table shows
warm-daylightmoving the 75% patch from 720 to 627, so graded HLG clips now read darker than ungraded ones, where before they blew out. That's much better than before. A follow-up test pinning that an identity grade keeps an HLG clip within a few codes of its source would stop this drifting again. alphaBlit.ts:308andgenerate-lut-reference.py:26each compute the constant. The drift rule in the generator docstring covers that, so this is fine as it is.
CI at 790a486f: all 8 required checks are green (Build, Render on windows-latest, Semantic PR title, Test, Test: runtime contract, Tests on windows-latest, Typecheck, regression).
Verdict: APPROVE
Reasoning: The HLG mapping now matches BT.2408 reference white, which I checked numerically and confirmed with a byte-identical regenerated table. The PQ and SDR paths and the HLG colour tags are untouched, and required CI is green at this head.
— Via
2 of 4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What a user can do now
Grade iPhone HLG HDR footage and export it in HDR without the bright parts blowing out. White text and other page content in an HLG export now sit at HDR reference white instead of the display's peak.
Why it failed
In an HDR export, everything the browser draws (page content, SDR clips, and graded video, since grading runs in the browser as SDR Rec.709) is converted from sRGB into the output's transfer by a lookup table. The PQ branch puts SDR white at 203 nits (ITU-R BT.2408). The HLG branch fed SDR-relative light straight into the HLG OETF. That curve's input is light relative to the HLG peak, so SDR white landed at 100% signal, and everything below it moved up as well.
A graded HLG clip is tone-mapped to SDR, graded, then converted back through that table. So any grade lifted the whole clip toward peak.
The fix
The HLG branch scales SDR-relative light by the scene light the inverse OETF gives for 75% signal, which puts SDR white at BT.2408 reference white, the same model as PQ. The reference-value generator (
generate-lut-reference.py) mirrors the change, and the byte-exact probe table is regenerated from it.Before / After
Fixture: a 1280x720 HLG clip (BT.2020, arib-std-b67, 10-bit HEVC) with flat patches at known HLG signal levels and thin 3-pixel strokes at 95% signal standing in for small bright text. Values are mean 10-bit luma code values (legal range 64 to 940).
Before (current release), default auto mode (exports HLG):
warm-daylightgradeThe same grade in an SDR export moves the background by 3 eight-bit steps; in the HLG export it moves by about 37.
After (this branch, same fixture, rendered by the producer in the Docker test image):
warm-daylightgradeUngraded HLG footage is untouched. A graded clip still passes through SDR (grading is Rec.709), so its highlights stop at reference white instead of blowing past the peak.
Tests
alphaBlit.test.ts: the probe table is regenerated with the generator, and sRGB 255 now maps to 49151 (75% of 65535) in HLG. The invariant test now pins both transfers below peak. Removing the reference-white scaling turns 19 of its tests red.hdr-hlg-regressionpasses against its existing golden with this change (0 failed frames, lowest PSNR 31.2 against a floor of 28), andhdr-regressionis PQ. The HLG fixture's page label does not appear in its render, so that golden holds no SDR content for this change to move; that missing label is a separate bug, fixed separately.Not in this PR