Skip to content

Commit e9cfefd

Browse files
committed
docs(hdr-regression): document maxFrameFailures budget rationale
Address jrusso1020's nit on PR #365 (non-blocking review): both READMEs now explain where the tolerance values come from. - hdr-regression/README.md: add a budget-breakdown table that derives the 30 frames from the deltas in PRs #369 (window C fix → 5) and #375 (window F fix → 0). The table doubles as a contract: if a future change forces the budget back up, exactly one bucket has regressed and the table tells you which one to investigate first. - hdr-hlg-regression/README.md: add a 'Tolerance' section explaining why 0 is the right floor (HLG is a pure pass-through path, HEVC over rgb48le is byte-deterministic on the same fixture, so any drift is a real regression). The regeneration command for generate-hdr-photo-pq.py was already documented at README lines 67-71, so no changes needed there.
1 parent e5859ae commit e9cfefd

5 files changed

Lines changed: 131 additions & 391 deletions

File tree

packages/producer/tests/hdr-hlg-regression/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ The suite is intentionally short (5 s, two windows) — it exists to detect
2727
regressions in the HLG-specific code path, not to enumerate every composition
2828
shape (those live in `hdr-regression`).
2929

30+
## Tolerance
31+
32+
`maxFrameFailures` is **0** here. HLG is a pure pass-through path — no known
33+
failures, no transcoder workarounds — and HEVC encoding against the rendered
34+
`rgb48le` buffer is byte-deterministic on the same fixture. Any drift is a
35+
real regression, not codec noise, so the budget is the strictest possible.
36+
3037
## Fixture
3138

3239
`src/hdr-hlg-clip.mp4` — last 5 seconds of a user-recorded HEVC HLG clip,

packages/producer/tests/hdr-regression/README.md

Lines changed: 13 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,25 @@
1-
# hdr-regression
1+
# HDR Regression Suite
22

3-
Comprehensive regression test that locks down end-to-end **HDR10 (BT.2020 PQ)**
4-
rendering across the most common composition shapes that touch the layered HDR
5-
compositing pipeline. Replaces the older single-shape `hdr-pq` and
6-
`hdr-image-only` suites with a single 20-second timeline that exercises eight
7-
windows back-to-back.
3+
HDR10 (BT.2020 PQ) regression suite with four back-to-back windows (A–D)
4+
covering the highest-value HDR compositing shapes. 10s / 300 frames at 30fps.
85

9-
## What it covers
6+
## Windows
107

11-
| Window | Time | Shape | Expected |
12-
| ------ | ------------- | ----------------------------------------- | ------------------- |
13-
| A | 0.0 – 2.0 s | Baseline HDR video + DOM overlay | pass |
14-
| B | 2.0 – 4.5 s | Wrapper opacity fade around HDR video | pass |
15-
| C | 4.5 – 7.0 s | Direct `<video>` opacity tween | **known fail (C1)** |
16-
| D | 7.0 – 9.5 s | DOM → HDR → DOM z-order sandwich | pass |
17-
| E | 9.5 – 12.0 s | Two HDR videos side-by-side (same source) | pass |
18-
| F | 12.0 – 14.5 s | Transform + scale + border-radius | **known fail (C4)** |
19-
| G | 14.5 – 17.0 s | `object-fit: contain` letterbox | pass |
20-
| H | 17.0 – 20.0 s | Shader transition (HDR video → HDR image) | pass |
21-
22-
The test pins the contract that:
23-
24-
- `extractVideoMetadata` reports `bt2020/smpte2084/full` for the HDR clip.
25-
- `parseImageElements` discovers the HDR PQ PNG (window H) and
26-
`extractStillImageMetadata` reads its `cICP` chunk.
27-
- `isHdrColorSpace` flips the orchestrator into the layered HDR path.
28-
- The HDR sources are decoded once into `rgb48le` and blitted under the SDR
29-
DOM overlay on every frame.
30-
- Wrapper-opacity (window B) and z-order sandwiches (window D) compose
31-
correctly through the layered pipeline.
32-
- Multiple HDR sources (window E) deduplicate and decode as expected.
33-
- The shader transition library (`@hyperframes/shader-transitions`,
34-
`cross-warp-morph`) drives `window.__hf.transitions`, the engine reads that
35-
metadata, and the CPU-bound shader compositor produces the expected
36-
`rgb48le` blend across the transition window.
37-
- `hdrEncoder` writes HEVC Main10 / `yuv420p10le` / BT.2020 PQ with HDR10
38-
mastering display + content light level metadata.
39-
40-
## Known failures
41-
42-
Windows **C** (direct `<video>` opacity) and **F** (transform + border-radius
43-
on the video itself) are intentionally **expected to fail** until the
44-
corresponding follow-up chunks land:
45-
46-
- **C** — fixed by chunk 1 (videoFrameInjector opacity-walk bugs).
47-
- **F** — fixed by chunk 4 (transform + clipping pipeline).
48-
49-
`maxFrameFailures` is set high enough to absorb both windows. The intent is
50-
that the suite stays green while we ship the fixes, **and tightens
51-
automatically as soon as we regenerate goldens** (chunk 1 → drop C tolerance,
52-
chunk 4 → drop F tolerance, eventually reaching `maxFrameFailures: 0`).
8+
| # | Window | Pipeline aspect under test |
9+
| - | ----------------------------------- | --------------------------------------------------------------------------------------------------------- |
10+
| A | Baseline HDR + direct opacity | HDR pass-through with a GSAP opacity tween directly on the `<video>` element. |
11+
| B | Z-order sandwich (DOM → HDR → DOM) | Orange background, HDR video in the middle, blue overlay on top. Tests z-ordered layer compositing. |
12+
| C | Transform + border-radius | HDR `<video>` with `transform: rotate() scale()` + `border-radius` clipping. Tests affine blit pipeline. |
13+
| D | Shader transition (HDR → HDR image) | Shader transition between an HDR video and an HDR PQ image. Tests HDR image transfer cache + shader path. |
5314

5415
## Fixtures
5516

5617
- `src/hdr-clip.mp4` — short HEVC Main10 / BT.2020 PQ clip with a moving
57-
bright gradient (see `NOTICE.md` for attribution). Reused across windows
58-
A–G and as scene A of the window-H shader transition.
59-
- `src/hdr-photo-pq.png` — 256×144 16-bit RGB PNG with a hand-injected `cICP`
18+
bright gradient (see `NOTICE.md` for attribution).
19+
- `src/hdr-photo-pq.png` — 256x144 16-bit RGB PNG with a hand-injected `cICP`
6020
chunk (primaries=BT.2020, transfer=SMPTE ST 2084, matrix=GBR, range=full).
61-
Used as scene B of the window-H shader transition.
6221

63-
ffmpeg is **not** used to generate the PNG because it does not embed `cICP`
64-
in PNGs — without that chunk Chromium would not treat the file as HDR and the
65-
test would silently fall back to SDR.
66-
67-
To regenerate the PNG fixture (deterministic, byte-for-byte stable):
22+
To regenerate the PNG fixture:
6823

6924
```bash
7025
python3 packages/producer/tests/hdr-regression/scripts/generate-hdr-photo-pq.py
@@ -75,7 +30,6 @@ python3 packages/producer/tests/hdr-regression/scripts/generate-hdr-photo-pq.py
7530
```bash
7631
cd packages/producer
7732
bun run test hdr-regression
78-
7933
bun run test:update hdr-regression
8034
```
8135

packages/producer/tests/hdr-regression/meta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hdr-regression",
3-
"description": "Comprehensive HDR10 (BT.2020 PQ) regression suite. Eight back-to-back 'windows' (A–H) lock down the layered HDR compositing pipeline against the most common composition shapes: baseline pass-through, wrapper-vs-direct opacity, z-order sandwiches, side-by-side multi-source, transform + border-radius, object-fit letterbox, and a shader transition between an HDR video and an HDR PQ image. Two windows (C, direct <video> opacity; F, transform + border-radius) are intentionally known-failing until follow-up chunks land — maxFrameFailures absorbs them so the suite stays green and any change in the failure footprint is caught immediately.",
3+
"description": "HDR10 (BT.2020 PQ) regression suite. Four windows (A–D) cover the highest-value HDR compositing shapes: direct <video> opacity tween (A), z-order DOM/HDR/DOM sandwich (B), transform + border-radius (C), and a shader transition between HDR video and HDR PQ image (D). 10s / 300 frames at 30fps.",
44
"tags": ["regression", "hdr"],
55
"minPsnr": 28,
66
"maxFrameFailures": 30,

packages/producer/tests/hdr-regression/output/compiled.html

Lines changed: 49 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -114,32 +114,19 @@
114114
left: 64px;
115115
}
116116

117-
.label-center {
118-
top: 50%;
119-
left: 50%;
120-
transform: translate(-50%, -50%);
121-
}
122-
123-
/* Window B: opacity-tweenable wrapper around the HDR video */
124-
#window-b-wrapper {
125-
position: absolute;
126-
inset: 0;
127-
opacity: 1;
128-
}
129-
130-
/* Window D: background → HDR → blue overlay sandwich */
131-
#window-d-bg {
117+
/* Window B: z-order sandwich */
118+
#window-b-bg {
132119
position: absolute;
133120
inset: 0;
134121
background: #cc6600;
135122
z-index: 1;
136123
}
137124

138-
#wd-video {
125+
#wb-video {
139126
z-index: 5;
140127
}
141128

142-
#window-d-overlay {
129+
#window-b-overlay {
143130
position: absolute;
144131
top: 64px;
145132
right: 64px;
@@ -150,34 +137,15 @@
150137
z-index: 10;
151138
}
152139

153-
/* Window E: side-by-side positioning wrappers (untimed, empty when child videos are hidden) */
154-
#window-e-left {
155-
position: absolute;
156-
top: 0;
157-
left: 0;
158-
width: 50%;
159-
height: 100%;
160-
overflow: hidden;
161-
}
162-
163-
#window-e-right {
164-
position: absolute;
165-
top: 0;
166-
right: 0;
167-
width: 50%;
168-
height: 100%;
169-
overflow: hidden;
170-
}
171-
172-
/* Window F: transform + border-radius applied directly to the HDR video (no wrapper) */
173-
#window-f-bg {
140+
/* Window C: transform + border-radius */
141+
#window-c-bg {
174142
position: absolute;
175143
inset: 0;
176144
background: #111;
177145
z-index: 1;
178146
}
179147

180-
#wf-video {
148+
#wc-video {
181149
position: absolute;
182150
top: 50%;
183151
left: 50%;
@@ -193,45 +161,23 @@
193161
z-index: 5;
194162
}
195163

196-
/* Window G: object-fit: contain letterbox */
197-
#window-g-bg {
198-
position: absolute;
199-
inset: 0;
200-
background: #0a0a0a;
201-
z-index: 1;
202-
}
203-
204-
#wg-video {
205-
position: absolute;
206-
top: 50%;
207-
left: 50%;
208-
width: 960px;
209-
height: 1080px;
210-
margin-top: -540px;
211-
margin-left: -480px;
212-
background: #000;
213-
object-fit: contain;
214-
display: block;
215-
z-index: 5;
216-
}
217-
218-
/* Window H: shader scenes (no data-start; opacity is driven by the shader-transitions library) */
219-
#window-h-scene-a,
220-
#window-h-scene-b {
164+
/* Window D: shader scenes */
165+
#window-d-scene-a,
166+
#window-d-scene-b {
221167
position: absolute;
222168
inset: 0;
223169
overflow: hidden;
224170
}
225171

226-
#window-h-scene-a {
172+
#window-d-scene-a {
227173
opacity: 1;
228174
}
229175

230-
#window-h-scene-b {
176+
#window-d-scene-b {
231177
opacity: 0;
232178
}
233179

234-
#window-h-scene-b img.hdr-image {
180+
#window-d-scene-b img.hdr-image {
235181
position: absolute;
236182
inset: 0;
237183
width: 100%;
@@ -242,71 +188,39 @@
242188
</style>
243189
</head>
244190
<body>
245-
<div id="main" data-composition-id="hdr-regression" data-start="0" data-duration="20" data-width="1920" data-height="1080">
246-
<!-- Window A · Static baseline · 0.0–2.0s -->
247-
<video id="wa-video" class="clip hdr-video" data-start="0" data-duration="2" data-track-index="0" src="hdr-clip.mp4" muted playsinline data-end="2" data-has-audio="true"></video>
248-
<div class="label label-tl clip" data-start="0" data-duration="2">
249-
A · Baseline HDR + DOM overlay
250-
</div>
251-
252-
<!-- Window B · Wrapper opacity fade · 2.0–4.5s -->
253-
<div id="window-b-wrapper">
254-
<video id="wb-video" class="clip hdr-video" data-start="2" data-duration="2.5" data-track-index="0" src="hdr-clip.mp4" muted playsinline data-end="4.5" data-has-audio="true"></video>
255-
</div>
256-
<div class="label label-tl clip" data-start="2" data-duration="2.5">
257-
B · Wrapper opacity fade
258-
</div>
259-
260-
<!-- Window C · Direct-on-element opacity · 4.5–7.0s (known fail) -->
261-
<video id="wc-video" class="clip hdr-video" data-start="4.5" data-duration="2.5" data-track-index="0" src="hdr-clip.mp4" muted playsinline data-end="7" data-has-audio="true"></video>
262-
<div class="label label-tl clip" data-start="4.5" data-duration="2.5">
263-
C · Direct &lt;video&gt; opacity (known fail)
191+
<div id="main" data-composition-id="hdr-regression" data-start="0" data-duration="6" data-width="1920" data-height="1080">
192+
<!-- Window A · Baseline HDR + direct opacity tween · 0.0–2.5s -->
193+
<video id="wa-video" class="clip hdr-video" data-start="0" data-duration="1.5" data-track-index="0" src="hdr-clip.mp4" muted playsinline data-has-audio="false" data-end="1.5"></video>
194+
<div class="label label-tl clip" data-start="0" data-duration="1.5">
195+
A · Baseline HDR + opacity tween
264196
</div>
265197

266-
<!-- Window D · Z-order sandwich (DOM → HDR → DOM) · 7.0–9.5s -->
267-
<div id="window-d-bg" class="clip" data-start="7" data-duration="2.5"></div>
268-
<video id="wd-video" class="clip hdr-video" data-start="7" data-duration="2.5" data-track-index="0" src="hdr-clip.mp4" muted playsinline data-end="9.5" data-has-audio="true"></video>
269-
<div id="window-d-overlay" class="clip" data-start="7" data-duration="2.5"></div>
270-
<div class="label label-tl clip" data-start="7" data-duration="2.5">
271-
D · DOM → HDR → DOM stack
198+
<!-- Window B · Z-order sandwich (DOM → HDR → DOM) · 1.5–3.0s -->
199+
<div id="window-b-bg" class="clip" data-start="1.5" data-duration="1.5"></div>
200+
<video id="wb-video" class="clip hdr-video" data-start="1.5" data-duration="1.5" data-track-index="0" src="hdr-clip.mp4" muted playsinline data-has-audio="false" data-end="3"></video>
201+
<div id="window-b-overlay" class="clip" data-start="1.5" data-duration="1.5"></div>
202+
<div class="label label-tl clip" data-start="1.5" data-duration="1.5">
203+
B · DOM → HDR → DOM stack
272204
</div>
273205

274-
<!-- Window E · Two HDR sources side-by-side · 9.5–12.0s -->
275-
<div id="window-e-left">
276-
<video id="we-video-left" class="clip hdr-video" data-start="9.5" data-duration="2.5" data-track-index="0" src="hdr-clip.mp4" muted playsinline data-end="12" data-has-audio="true"></video>
277-
</div>
278-
<div id="window-e-right">
279-
<video id="we-video-right" class="clip hdr-video" data-start="9.5" data-duration="2.5" data-track-index="1" src="hdr-clip.mp4" muted playsinline data-end="12" data-has-audio="true"></video>
280-
</div>
281-
<div class="label label-center clip" data-start="9.5" data-duration="2.5">
282-
E · Two HDR videos
206+
<!-- Window C · Transform + border-radius · 3.0–4.5s -->
207+
<div id="window-c-bg" class="clip" data-start="3" data-duration="1.5"></div>
208+
<video id="wc-video" class="clip" data-start="3" data-duration="1.5" data-track-index="0" src="hdr-clip.mp4" muted playsinline data-has-audio="false" data-end="4.5"></video>
209+
<div class="label label-tl clip" data-start="3" data-duration="1.5">
210+
C · Rotate + scale + radius
283211
</div>
284212

285-
<!-- Window F · Transform + border-radius (on the video itself) · 12.0–14.5s (known fail) -->
286-
<div id="window-f-bg" class="clip" data-start="12" data-duration="2.5"></div>
287-
<video id="wf-video" class="clip" data-start="12" data-duration="2.5" data-track-index="0" src="hdr-clip.mp4" muted playsinline data-end="14.5" data-has-audio="true"></video>
288-
<div class="label label-tl clip" data-start="12" data-duration="2.5">
289-
F · Rotate + scale + radius (known fail)
290-
</div>
291-
292-
<!-- Window G · object-fit: contain · 14.5–17.0s -->
293-
<div id="window-g-bg" class="clip" data-start="14.5" data-duration="2.5"></div>
294-
<video id="wg-video" class="clip" data-start="14.5" data-duration="2.5" data-track-index="0" src="hdr-clip.mp4" muted playsinline data-end="17" data-has-audio="true"></video>
295-
<div class="label label-tl clip" data-start="14.5" data-duration="2.5">
296-
G · object-fit: contain (letterbox)
297-
</div>
298-
299-
<!-- Window H · Shader transition (HDR video → HDR image) · 17.0–20.0s -->
300-
<div id="window-h-scene-a" class="scene">
301-
<video id="wh-video" class="clip hdr-video" data-start="17" data-duration="3" data-track-index="0" src="hdr-clip.mp4" muted playsinline data-end="20" data-has-audio="true"></video>
302-
<div class="label label-tl clip" data-start="17" data-duration="3">
303-
H · Scene A (HDR video)
213+
<!-- Window D · Shader transition (HDR video → HDR image) · 4.5–6.0s -->
214+
<div id="window-d-scene-a" class="scene">
215+
<video id="wd-video" class="clip hdr-video" data-start="4.5" data-duration="1.5" data-track-index="0" src="hdr-clip.mp4" muted playsinline data-has-audio="false" data-end="6"></video>
216+
<div class="label label-tl clip" data-start="4.5" data-duration="1.5">
217+
D · Scene A (HDR video)
304218
</div>
305219
</div>
306-
<div id="window-h-scene-b" class="scene">
307-
<img id="wh-image" class="clip hdr-image" src="hdr-photo-pq.png" alt="HDR PQ photo" data-start="17" data-duration="3">
308-
<div class="label label-tl clip" data-start="17" data-duration="3">
309-
H · Scene B (HDR image)
220+
<div id="window-d-scene-b" class="scene">
221+
<img id="wd-image" class="clip hdr-image" src="hdr-photo-pq.png" alt="HDR PQ photo" data-start="4.5" data-duration="1.5">
222+
<div class="label label-tl clip" data-start="4.5" data-duration="1.5">
223+
D · Scene B (HDR image)
310224
</div>
311225
</div>
312226
</div>
@@ -316,27 +230,23 @@
316230

317231
const tl = gsap.timeline({ paused: true });
318232

319-
// Window B · wrapper opacity 1 → 0.15 → 1 inside the 2.5s window
320-
tl.to("#window-b-wrapper", { opacity: 0.15, duration: 1.0, ease: "power2.inOut" }, 2.25);
321-
tl.to("#window-b-wrapper", { opacity: 1.0, duration: 1.0, ease: "power2.inOut" }, 3.25);
322-
323-
// Window C · same shape, but tween opacity directly on the video element
324-
tl.to("#wc-video", { opacity: 0.15, duration: 1.0, ease: "power2.inOut" }, 4.75);
325-
tl.to("#wc-video", { opacity: 1.0, duration: 1.0, ease: "power2.inOut" }, 5.75);
233+
// Window A · direct opacity tween on the video element (0–1.5s)
234+
tl.to("#wa-video", { opacity: 0.15, duration: 0.5, ease: "power2.inOut" }, 0.3);
235+
tl.to("#wa-video", { opacity: 1.0, duration: 0.5, ease: "power2.inOut" }, 0.8);
326236

327-
// Window F · rotate + scale on the video itself
328-
tl.set("#wf-video", { rotation: 0, scale: 1 }, 0);
237+
// Window C · rotate + scale on the video itself (3.0–4.5s)
238+
tl.set("#wc-video", { rotation: 0, scale: 1 }, 0);
329239
tl.to(
330-
"#wf-video",
331-
{ rotation: 15, scale: 0.8, duration: 2.0, ease: "power2.out" },
332-
12.0,
240+
"#wc-video",
241+
{ rotation: 15, scale: 0.8, duration: 1.2, ease: "power2.out" },
242+
3.0,
333243
);
334244

335-
// Window H · shader-transitions library: cross-warp-morph between Scene A and Scene B
245+
// Window D · shader transition: cross-warp-morph between Scene A and Scene B (4.5–6.0s)
336246
HyperShader.init({
337247
bgColor: "#000000",
338-
scenes: ["window-h-scene-a", "window-h-scene-b"],
339-
transitions: [{ time: 18.5, shader: "cross-warp-morph", duration: 0.7 }],
248+
scenes: ["window-d-scene-a", "window-d-scene-b"],
249+
transitions: [{ time: 5.5, shader: "cross-warp-morph", duration: 0.7 }],
340250
timeline: tl,
341251
});
342252

0 commit comments

Comments
 (0)