fix(engine,shader): handle matrix3d transforms and hide non-first scenes#374
fix(engine,shader): handle matrix3d transforms and hide non-first scenes#374vanceingalls wants to merge 1 commit intovance/refactor-helpersfrom
Conversation
f328731 to
79b029f
Compare
4f4682b to
78a6af9
Compare
79b029f to
e78f981
Compare
3838031 to
95df8c6
Compare
45009c4 to
cb840e7
Compare
jrusso1020
left a comment
There was a problem hiding this comment.
Two real fixes, both structural:
matrix3d in parseTransformMatrix — previously returned null for any 3D matrix, which meant every GSAP composition with force3D: true (GSAP's default for transform tweens) fell off the deterministic engine path and rendered with identity transforms on the HDR compositor. The spec-correct approach is: parse the 16-element column-major matrix, extract the 2D affine components (a=m[0], b=m[1], c=m[4], d=m[5], tx=m[12], ty=m[13]), drop Z-translation, and validate all six extracted components are finite. Tests pin the three common GSAP emission shapes (identity, translate3d, scale + translate3d, rotateZ) plus malformed rejection (wrong arg count, NaN values). Exactly the right coverage.
Worth documenting in code (or the plans doc) that this is a 2D projection of the 3D transform — any composition that actually uses Z-depth (perspective, 3D rotation around X/Y axes) will silently lose those components in the engine path. For force3D: true with a flat Z this is invisible; for a genuine 3D scene the engine just doesn't support it, which was already the case before this PR but is now hidden behind a "parses successfully" result instead of a null. If a Z-significant transform ever shows up in a regression test, the engine will render it flat without flagging that it threw away the Z info. Non-blocking, but worth a log warning when m[8..11] are non-identity.
Non-first scenes start at opacity: 0 in hyper-shader.ts — the fix for the Window F corruption in #365. Without this, non-first scenes were visible during scene-A capture and bled into the HDR compositor's first frame. Cleaner than the alternative (hiding scenes via DOM manipulation) because it stays inside the shader-transitions package's opacity contract.
CI failures here are the stack-wide Format and Render on windows-latest — not specific to this change. The Windows failure across the whole upper stack is worth running down separately (could be LFS-check issue after #376, or could be unrelated).
Approved.
— Rames Jusso
cb840e7 to
c37d8a5
Compare
34e108c to
51aaf25
Compare
c37d8a5 to
a6f91b0
Compare
51aaf25 to
60ba3c7
Compare
a6f91b0 to
43ed55b
Compare
Review feedback follow-up@jrusso1020's non-blocking suggestion is addressed:
function warnIfZSignificant(parts: number[]): void {
if (warnedZSignificant) return;
// For a flat 2D transform — the only thing this engine path can render
// faithfully — we expect:
// a3 = b3 = c1 = c2 = 0 (no XZ/YZ rotation coupling)
// c3 = 1 (no Z scaling)
// d1 = d2 = d3 = 0 (no perspective)
// d4 = 1 (no homogeneous scaling)
// Z translation (c4 = parts[14]) is explicitly dropped by the 2D affine
// extraction below — that's the whole point of supporting GSAP's
// `force3D: true` translate3d(x, y, 0) emission — so it is NOT flagged.
// ...component checks against Z_EPSILON...
if (...non-trivial 3D components detected...) {
warnedZSignificant = true;
console.warn(
`[alphaBlit] parseTransformMatrix received a matrix3d with non-trivial 3D components ` +
`(a3=${a3}, b3=${b3}, c1=${c1}, c2=${c2}, c3=${c3}, d1=${d1}, d2=${d2}, d3=${d3}, d4=${d4}). ` +
`The engine projects 3D transforms to 2D (m11, m12, m21, m22, m41, m42) and silently ` +
`discards perspective and out-of-plane rotation. ...`,
);
}
}Key design points (addressing the staff-eng nod):
Test coverage in
Stack-wide CI: ✅ green now (the |
43ed55b to
2060489
Compare
16713e6 to
a8546d5
Compare
2060489 to
30f0fc9
Compare
a8546d5 to
d3f12e6
Compare
44be838 to
e5271b7
Compare
67bb918 to
a6a68e9
Compare
558326d to
d5c4402
Compare
46e66c1 to
b505a8f
Compare
d5c4402 to
6d3cc3f
Compare
b505a8f to
f99cd4b
Compare
6d3cc3f to
0d0a913
Compare
f99cd4b to
fe8806d
Compare
0d0a913 to
c0cfdcf
Compare
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.
c0cfdcf to
7f0411c
Compare
fe8806d to
d22e419
Compare

Summary
Two correctness fixes in the HDR transform & clipping pipeline:
parseTransformMatrixnow handlesmatrix3d(...)(GSAP's defaultforce3D: true), and shader-transitions sets every non-first scene toopacity: 0att=0so the engine doesn't over-composite at the start.Why
Chunk 4ofplans/hdr-followups.md. Transform extraction and border-radius computation existed but were dead — an HDR video withrotation: 45rendered un-rotated, and 3-scene compositions ghosted att=0because every scene defaulted to CSSopacity: 1and contributed to the first frame.What changed
Matrix3d support in
parseTransformMatrix.DOMMatrix.toString()emitsmatrix3dwhenever any ancestor in the chain has used a 3D transform — most importantly GSAP's defaultforce3D: true, which convertstranslate(...)intotranslate3d(..., 0). Without this, every GSAP-driven transform was silently dropped during HDR compositing becausevideoFrameInjector.getViewportMatrix()would returnmatrix3d(...)and the blit path would parse it asnulland fall back to identity. The 16-value column-major form is converted to its 2D affine projection (indices 0, 1, 4, 5, 12, 13 → m11, m12, m21, m22, m41, m42); Z, perspective, and out-of-plane rotation components are dropped.Initial-state opacity in
initEngineMode. The browser preview branch uses a GL canvas overlay during transitions, so scene opacity att=0doesn't matter visually. The engine branch reads scene opacity directly viaqueryElementStacking()to decide which layers to composite. Without an explicit initial-state tween, every scene defaulted to CSSopacity: 1and contributed to the very first frame, causing ghosting/overlap until the first transition fired.tl.set()at position 0 anchors the initial state in the timeline graph so reverse seeks from inside a later transition restore it correctly.These two fixes together make
el.transformandel.borderRadius(already wired in Chunk 7A'scompositeHdrFrame) actually flow through the GSAP-animated case, and keep the engine's per-frame compositing aligned with what the user sees in browser preview.Test plan
alphaBlit.test.tscases (identity matrix3d, translate3d, scale + translate3d, rotateZ, malformed arg count, non-finite values).hdr-regressionWindow H already CSS-sets#scene-b { opacity: 0 }as a fallback; the newtl.setis redundant for that case but harmless and removes the need for compositions to remember the CSS workaround.rotation: 45) appears rotated;border-radius: 50%clips to circle; 3-scene composition has no overlap att=0.Stack
Chunk 4 of
plans/hdr-followups.md. Window F of the regression suite documents the bug; the next PR in the stack tightens themaxFrameFailuresbudget to 0.