Skip to content

feat(core): bake a colour grade into a 3D LUT that matches the shader - #6679

Merged
vanceingalls merged 1 commit into
mainfrom
feat/color-bake
Oct 28, 2026
Merged

vanceingalls merged 1 commit into
mainfrom
feat/color-bake

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

What

bakeHfColorGradingCube(grading, { size = 33, lut }) turns a data-color-grading value into the text of a 3D LUT .cube file. It uses the same stages as the runtime shader: primary, wheels, RGB curves, hue curves, secondaries, then the LUT and overall intensity. Every one of these is pointwise, so a colour-only grade is exactly a cube.

Why: a host that grades live in preview and bakes only at render (the Desktop podcast edit: grade a camera's lane, flatten for export) needs ffmpeg's lut3d to give the same picture as the shader.

Behaviour

  • enabled: false returns the identity cube, matching the shader, which doesn't draw it either.
  • If the grading names a LUT with intensity above 0 and options.lut is missing, the bake throws. The preview would skip that LUT silently and the export would be wrong.
  • Spatial stages throw an error that names them. These are grain, vignette, active effects, and presets that carry grain.
  • size must be a whole number from 2 to 256.
  • Curve and secondary values are stored in 32-bit floats before being rounded to 8 bits, as the runtime does. This matters for how exact halves round: luma max 0.9 becomes 229, not 230.
  • The cube maps the 8-bit full-range RGB the shader sees. Apply lut3d after converting to full-range RGB, never on video-range or linear data.

Tests

  • core: 19 unit tests. One pins the 32-bit rounding case, and it fails when that rounding is broken on purpose.
  • cli Chromium parity test against the real runtime, on 4096 lattice colours: 0 codes max, 0.000 mean at the cube's own nodes, for a primary-only grade and for wheels + RGB curve + hue curve + one secondary. The test also fails if the grade leaves the picture unchanged.
  • Typecheck, oxfmt, oxlint, package cycles and fallow audit all pass.
  • Full core and cli suites had timeouts under heavy machine load. Rerun alone, core passes 239/239. Five cli files still failed alone (whisper, fileWatcher, checkSweep with no WebGL). None of them touch grading, and they were not checked against main.

Not covered

Interpolation error between nodes is not bounded yet: Genesis measured up to 11 codes at size 33 for a grade with a secondary (3 for primary-only). A follow-up adds an off-node test and states the bound.

The LUT stage is unit-tested only, against an inverting LUT. The runtime loads LUTs by URL, so the browser parity test does not include one.

🤖 Generated with Claude Code

bakeHfColorGradingCube evaluates the runtime shader's colour path on the
CPU (primary, tonal wheels, RGB and hue curves, secondaries, output LUT,
intensity mix) through the same 8-bit texture quantisation, and writes a
red-fastest .cube for ffmpeg's lut3d. Grades with spatial stages (grain,
vignette, effects) throw, as does a grade whose LUT was not passed in.

A Chromium test draws a 4,096-colour lattice through the real runtime
and compares every tile with the bake.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@github-actions

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 142 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (2)

  • seqplainkeys-none-px-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true
  • seqplainkeys-none-pct-r0-nested-z100: tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo false, teleport true / tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE · 0 blocker · 1 important · 1 nit · head 75fbb10

Required checks are green. Nothing consumes this yet: hyperframes-internal#6729 bakes with its own primary-only port.

  • nit: the PR body says the bake returns a CubeLut3D, but it returns the .cube text.
Scope

Compared the bake stage by stage with the shader in runtime/colorGrading.ts (applyPrimaryGrade, applyTonalWheels, curves, hue curves, applySecondary, applyLut, the clamps, the intensity mix) and found no divergence. Probe for the finding: bun, bakeHfColorGradingCube at sizes 33/65 against size 256, trilinear interpolation, 200k random 8-bit colours.

— Claude (Genesis)

}

function compareWithBake(pixels: readonly number[], grading: HfColorGrading): Difference {
const rows = bakeHfColorGradingCube(grading, { size: STEPS })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

important: this parity test only checks colours that sit exactly on cube nodes (size 16 = the lattice), so it can't see interpolation error. ffmpeg's lut3d interpolates every other colour. I baked at size 256 (exact at every 8-bit code) and compared 200k random colours against trilinear lookups in smaller cubes. The ADVANCED grade misses by up to 11 codes at the default size 33 (mean 0.33, worst at 48,44,32, a dark skin tone near the secondary's key) and by up to 7 codes at 65. PRIMARY misses by up to 3 codes at 33. So "matches the shader exactly" only holds at the nodes. Please add an off-node case (e.g. bake at 33 and probe the 16-step lattice through tetrahedral interpolation) and state the bound in the docblock. A larger default size for grades with secondaries or hue curves would also help.

@vanceingalls
vanceingalls added this pull request to the merge queue Oct 28, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 28, 2026
@vanceingalls
vanceingalls added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit c9017f4 Oct 28, 2026
130 checks passed
@vanceingalls
vanceingalls deleted the feat/color-bake branch October 28, 2026 17:07
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.

1 participant