Repository navigation
Conversation
…t (PRD-492 Phase 1)
The starter's frame now ends in a colour grade and a film grain, both authored
stages it owns: `src/render/grade.ts` builds them, `quality.ts` decides per tier
and `public/grade.cube` is the table. Replacing that one file with a `.cube` from
any grading tool is the whole of "change the grade".
The filed design was a bijective log shaper inside the stage. It is measured and
dropped: an 8-bit table quantises in 1/255 steps, and sweeping the shaper family
over the frame's range found nothing under 2.0 8-bit steps per channel (the filed
W=4, g=1/2.2 costs 4.3), because ACES and sRGB together stretch a small change in
light into several codes. So the fallback this PRD already named ships instead: a
stage may declare `afterOutputTransform`, the chain then installs the graph with
the renderer's automatic transform off and the stage applies three's own
`renderOutput` first — what three's FXAA example does. An identity table there is
within 0.875 of a step over every value the frame can hold, which is the PRD's own
box, and a `.cube` now means what a grading tool means by it.
Engine change is mechanism only: `IRenderChainStage.afterOutputTransform`, a
third argument on `setOutputNode`, and the chain handing the renderer
`outputColorTransform: false` only when such a stage actually ran — a dropped one
would leave the frame untone-mapped with nothing to have applied it.
Ran: pnpm typecheck, pnpm lint, 259 vitest files across packages/core and
packages/create-threenative. Playtest `look.playtest.json` on a scaffolded starter
(webgpu, headed, adapterClass hardware): renderChain tier/includes/order/
contributions all pass with grade and grain applied; the tier-low leg on a
throwaway copy with the tier pinned reads `dropped: [{"name":"grain",
"reason":"grainIntensity:0"}]` with grade still applied.
Open, recorded in the PRD: AC-1 (gpuOther resolved in 1 of 17 windows graded, 0 of
16 ungraded — no delta to read) and the frame half of the identity round trip (the
starter's own animated content moves between runs by more than the claim; the same
build twice reads maxChannelDelta 114).
The pre-push drift gate refused the push on a formatter diff biome had already reported once; this is the same one line, formatted.
…hat could not run (PRD-492 Phase 2)
Phase 2's second box passes: `pnpm native:build` (409/409), then
`pnpm parity --target desktop --only-tests lut-grade` reports status pass with
`pixelMismatchRatio 0` and `perceptualDeltaE 0` over 1280x720 and no GPU
validation errors, against a web-lane reference capture of the same scene. The
table is a 17^3 per-channel gain rather than the identity, so a host that ignored
the upload or substituted a constant fails it, and the scene reads the parsed
Data3DTexture back in JS against that formula before the frame is compared. The
sweep exits 2 because a bounded --only-tests run blocks the other 95 rows.
Phase 2's first box stays open, and the reason is the packager rather than the
grade: the desktop run's marker reads `dropped: [{"name":"grade","reason":
"lut:pending"}]` and the console carries `TN_GRADE_TABLE grade.cube: File read
error`. package-desktop.mjs stages `selectManifestAssets(assets)` and nothing
else, so a public/ file the manifest does not declare never reaches the host —
every unmanaged public file on every native target, not this table alone. Fixing
the asset selection is mechanism work outside this PRD's filed file list, so it is
recorded rather than done here.
The scenario also could not run at all before: its `visibility` assertion is
refused outright on desktop (TN_PLAYTEST_OBSERVATION_UNAVAILABLE), so it failed
before reading a stage. That row is dropped — the runner never produced the
observation on this target — and `grade`/`grain` are added to the stages and
contributions the scenario does read.
Ran: pnpm typecheck, biome on the changed files, packages/runtime-native (7 files)
and packages/playtest (104 of 105 files; `generated-shooter-input` fails on a
browser session and touches nothing in this change), and the create-threenative
scaffold and looks contracts.
A blind judge compared the starter WITH this PR's grade and grain against
WITHOUT and clearly preferred WITHOUT: "strong warm beige/pink cast — grey
concrete reads as sandstone, sky washed toward cream, a bit muddy/low contrast;
grain clearly visible as speckle over sky and flat walls and behind HUD text".
The owner's rule is that a default must never degrade the look, so both
numbers move and the spec says so red-first.
The pivot was the larger half and nothing had measured it. A contrast curve
anchored at 0.18 leaves everything above it brighter: on the captured frame
that lifted mid-grey 8 of 255 steps and the sky 12, which is exactly what
"washed toward cream" looks like. PIVOT 0.18 -> 0.5, and GAIN's warm spread
13% -> 0.5% (r 1.0025 / g 1 / b 0.9975). CONTRAST stays 1.06, so the frame
keeps more separation than the ungraded one, and the table still lifts no
black. grade.spec.ts pins all three claims and is red on the old table.
grainIntensity 0.12 -> 0.0039 at high and medium. film() scales its noise by
the pixel's own value, so 1/255 is under half a step on a mid-grey surface —
below what the framebuffer shows. It stays non-zero so TN_RENDER_CHAIN keeps
naming grain as APPLIED at high, which is this PRD's own ticked box; low
still refuses it with a reason, and quality.ts names 0.12 and 0.3 as the
values to raise it to.
The desktop box passes now, and the packager was never at fault. Phase 2 filed
"the staging loop copies selectManifestAssets(assets) and nothing else, so an
undeclared public/ file never reaches the host"; the selector's rule 3 keeps
everything no cook produced and its spec already proved that. The scaffold that
run used had no public/grade.cube in it at all — the AC-2 before-arm deletes
that file — so there was nothing to stage. With it present: pnpm build --target
desktop lists it among the unmanaged files, the host logs "[Fetch] Read 19909
bytes from bundle: grade.cube", and native-playtests/render-chain.playtest.json
exits 0 with all six assertions true at adapterClass hardware and a non-blank
screenshot (tone mean 117.1, p99 192). A guard in the selector's own spec now
covers a hand-placed file whose extension no asset pipeline emits.
Also recorded: a browser run that reports swiftshader/google is a CPU
rasteriser's picture of a hardware look (this lane's frame came back flat grey
with 7,359 createBuffer errors), and --browser-recipe webgpu alone does not
prevent it — the runner launches chrome-headless-shell. Every capture ran
--headed, on turing/nvidia, at tier high.
Ran: grade.spec.ts 11 passed (2 of the new ones red on the old table),
scaffold.spec.ts 66, looks.spec.ts, asset-manifest.spec.ts 6, the six doc/CI
contracts 241, pnpm typecheck clean, biome clean on every touched file,
pnpm check:docs clean.
Captures for the judge, not judged here: .afk/scratch/prd492-captures/phase1b/
{before,after}.png plus {before,after}-crop.png (centre 960x540 at 2x).
Measured between the pair the grade now moves the frame by rmse 2.22 codes and
-0.32/-0.71/-0.32 mean per channel, from rmse 8.17 and +6.7/+6.3/+6.9.
Carries develop's WebGPU adapter-retention patch, the terrain-jobs work and the CI scope changes. One conflict: the scaffold tree hashes, which develop restated for its own merge and this branch restated for PRD-492's stage seat plus the neutralised starter default. Resolved to the merged-tree measurement — all thirteen trees re-measured through actual createProject trees on this tree, `scaffold.spec.ts` 66 passed.
…is lane Takes the develop CI merge that had already landed on the PR branch. Two conflicts, both resolved to this lane's newer text: the scaffold tree hashes (re-measured through actual createProject trees on this tree, `scaffold.spec.ts` 66 passed) and the PRD (the remote side was the pre-neutralisation file).
Contributor
Author
4 of 10 tasks
Contributor
Author
|
Consolidated into #437 (single runbook PR). |
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.





TL;DR
The starter's frame now ends in a colour grade and a film grain, and the stage that does it takes a
new seat in the chain: display-referred, after the output transform. The filed design put a log
shaper inside the stage instead; it was measured, it costs at least 2.0 eight-bit steps per channel,
and this PRD's own box asks for 1. The engine seat it falls back to — named in the PRD before it was
written — lands the box by construction and makes a
.cubemean what a grading tool means by it.Phase 1 only. Phase 2 (native) has not run.
What changed
templates/starter/src/render/grade.ts(new)grade(a.cubethroughlut3D) andgrain(film)templates/starter/public/grade.cube(new),tools/make-grade-lut.mjs(new)templates/starter/src/render/{postprocessing,quality}.tspackages/core/src/render/chain.ts,renderer.tsafterOutputTransformon a stage;setOutputNode(node, pass, options)hands the renderer its transform back only when such a stage actually rantemplate-assets/worldEnvironment.ts+ 13 kit copiesChainStagetype learns the flagPhase 2, since the first push
pnpm native:buildcompleted (409/409), so the desktop lane ran here.pnpm parity --target desktop --only-tests lut-grade:status: "pass",pixelMismatchRatio: 0,perceptualDeltaE: 0over 1280x720,gpuValidationErrors: [], host rendering 300 frames in 9.4 s, compared against a web-lanereference capture of the same scene. The 17^3 table is a per-channel gain, not the identity, and
the parsed
Data3DTextureis read back in JS against that formula before the frame is compared.(The sweep exits 2 because a bounded
--only-testsrun blocks the other 95 rows.)dropped: [{"name":"grade","reason":"lut:pending"}]and the console carriesTN_GRADE_TABLE grade.cube: File read error.package-desktop.mjsstagesselectManifestAssets(assets)alone, so apublic/file the manifest does not declare neverreaches the host — every unmanaged public file on every native target, not this table alone.
visibilityassertion is refused outrighton desktop (
TN_PLAYTEST_OBSERVATION_UNAVAILABLE), so it failed before reading a stage. That rowis dropped (the runner never produced the observation on this target) and
grade/grainare addedto the stages and contributions the scenario does read.
Evidence that ran
pnpm typecheck,pnpm lint(0 errors), 259 vitest files acrosspackages/coreandpackages/create-threenative— all pass.packages/create-threenative/__tests__/grade.spec.ts— 8 passed. It reads the shipped table backthrough a port of
LUTCubeLoader.parse, applieslut3D's half-texel pull-in and the hardware'sown filtering, and measures the identity round trip at ≤ 1 step over every value the frame can
hold (worst 0.875).
look.playtest.jsonon a scaffolded starter,--browser-recipe webgpu --headed,adapterClass: hardware:renderChain.tier,renderChain.stages.includes,renderChain.stages.orderandrenderChain.contributions.graphOutputChangedall pass withstages: ["ambientOcclusion","bloom","vignette","antialias","grade","grain"],dropped: [].lowleg needs an adapter the harness classes as software, and Chromium here reaches noWebGPU adapter under SwiftShader (
TN_PLAYTEST_CAPTURE_PROVENANCE_MISSING). It was run instead ona throwaway copy with
setupPost(..., { tier: "low" })pinned:gradeapplied,dropped: [{"name":"grain","reason":"grainIntensity:0"}], both assertion rows pass. Nothing about that pinis committed.
Open, with the reason
gpuOtherresolved in 1 of 17 windows graded and 0 of 16 ungraded at 1080p on thislane, so there is no delta to read. A missing observation is not a zero.
quarry compareon the scaffold readsmaxChannelDelta 86, rmse 0.81for ungraded vs identity-with-grain-off, and the same ungradedbuild captured twice reads
maxChannelDelta 114, rmse 0.37. The starter's own animated contentmoves between runs by more than the claim, so a whole-frame maximum cannot carry it. A still-scene
capture would.
.afk/scratch/prd492-captures/phase1/{before,after}.png,1920x1080, same scaffold and same pose, the only difference being the grade.
lut-gradeconformance row have notrun.
pnpm build:webalready reports the table among 5 undeclared files (38,890 bytes), which isthe packager risk this PRD named.
Progress label
prd:25%.