Skip to content

feat(exposure): meter a histogram, ignore the sun (PRD-571) - #478

Merged
jonit-dev merged 12 commits into
developfrom
feat/prd-571-histogram-exposure
Oct 11, 2026
Merged

jonit-dev merged 12 commits into
developfrom
feat/prd-571-histogram-exposure

Conversation

@jonit-dev

@jonit-dev jonit-dev commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR — Auto exposure is generated template source. The three files are byte-identical in all 13 templates (md5sum on 2026-10-09): src/render/exposure.ts, src/render/exposureGraph.ts and src/render/autoExposure.ts. It ships opt-in (exposure.ts:29, enabled: false).

100% by the PRD's own boxes: 2/2 phases (6/6 boxes), 2/2 acceptance criteria. PRD: docs/PRDs/BLOCKED/requires-physical-device/PRD-571-exposure-meters-a-histogram-and-ignores-the-sun.md.

Phases

Phase 1: The histogram metric in the fixture — 3/3

  • The clipped average matches a CPU reference of the UE walk on synthetic histograms: all weight in one bin, a uniform spread, an empty histogram, and low == high. proof: pnpm exec vitest run --maxWorkers=1 packages/create-threenative/__tests__/auto-exposure.spec.ts — 1 file, 33 tests passed. The GPU graph is checked against the same reference by the in-page histogram probe inside verify.ts (histogram-reference room, exit 0).
  • A sun disc covering 1% of the frame moves the settled exposure by less than 0.1 stop. proof: pnpm exec vitest run --maxWorkers=1 packages/create-threenative/__tests__/auto-exposure-proof.spec.ts (43 passed) plus the GPU room backlit-disc in verify.ts (gate maxShiftStops 0.1): the settled target moved 0.035 stop against the disc-free room, exit 0 at a0b7c91.
  • The existing static, cut, cold-boot and lifecycle fixture scenarios still pass with the histogram metric. proof: pnpm exec vitest run --maxWorkers=1 packages/create-threenative/__tests__/auto-exposure-proof.spec.ts packages/create-threenative/__tests__/auto-exposure-lifecycle.spec.ts — 2 files, 61 tests passed. GPU scenarios at a0b7c91, each exit 0: verify.ts (23 captures: static, cuts, reverse, snap, mutations), verifySnap.ts, verifyLifecycle.ts, verifyColdBoot.ts (20 launches, limit 0.1). The metered luminance of the bright room changed from 3.896 to 3.058 by design (clipped log mean), so the cold-boot and native references moved with it.

Phase 2: All templates, native and the phone — 3/3

  • All 13 templates ship byte-identical exposure source with the histogram metric, and the scaffold test passes. proof: pnpm exec vitest run --maxWorkers=1 packages/create-threenative/__tests__/auto-exposure-scaffold.spec.ts packages/create-threenative/__tests__/scaffold.spec.ts — 2 files, 79 tests passed; md5sum of exposureGraph.ts in template-assets/ and the 13 templates gives one hash. PRD_201_PARENT_SCAFFOLD_HASHES is re-pinned for the 13 templates.
  • The desktop native host settles the static fixture with the histogram metric. proof: node packages/playtest/dist/runner/cli.js packages/create-threenative/__tests__/fixtures/auto-exposure/native-static.playtest.json --target desktop; run through its qualifier TN_NATIVE_EXECUTABLE=<tn-linux/mystral built 2026-10-05> sh scripts/xvfb.sh pnpm exec tsx packages/create-threenative/__tests__/fixtures/auto-exposure/verifyNative.ts, exit 0 at a0b7c91: 180 paired GPU readbacks, native:vulkan/nvidia/NVIDIA GeForce RTX 2080, terminal luminance 3.05789 (browser 3.05789), settled, scopes clean, validation negative arm fails as required. The host binary was prebuilt (no native rebuild in this lane); the change is template JS, so the host source is not part of this diff.
  • The reference doc names lowPercent, highPercent and the meter weight, and the doc checks pass. proof: pnpm check:docs — checked 2988 relative links across 1331 files, exit 0; packages/create-threenative/agent-docs/references/auto-exposure.md names all three.

Acceptance criteria

  • AC-1 [local]: In the auto-exposure fixture, a subject in front of a sky ten stops brighter is metered within the PRD-341 tone band. The current mean meter fails the same scene (red), and the histogram meter passes it (green). proof: pnpm exec vitest run --maxWorkers=1 packages/create-threenative/__tests__/auto-exposure-proof.spec.ts (new backlit room; qualifier specs, 43 passed) and the GPU rooms sh scripts/xvfb.sh pnpm exec tsx packages/create-threenative/__tests__/fixtures/auto-exposure/verify.ts, exit 0 at a0b7c91. Histogram: backlit-sky tone mean 129.3, blackFraction 0; backlit-disc passes. Red control (the pre-change mean meter served from 9d4b2a6): backlit-sky-mean and backlit-disc-mean fail the tone gate and move the target 6.2 and 5.5 stops. A fresh judge subagent rated both rooms AFTER_BETTER (5/5): the mean frame is crushed to black, the histogram frame reads cleanly, no tile seams or banding.
  • AC-2 [local]: On the RTX 2080 browser WebGPU lane at 1080p, the histogram meter costs at most 0.1 ms GPU more than the mean meter. proof: node packages/playtest/dist/runner/cli.js perf on the fixture, both arms, --browser-recipe webgpu. Measured with TN_COST_ROUNDS=8 TN_COST_SECONDS=45 sh scripts/xvfb.sh pnpm exec tsx packages/create-threenative/__tests__/fixtures/auto-exposure/measureMeterCost.ts (alternating arms, the engine's own TN_FRAME_BUDGET gpuMs, --enable-webgpu-developer-features, 1920x1080, adapter nvidia/turing RTX 2080; logs in artifacts/prd571-cost/, readable with perf --file <log> --allow-virtual-display --text). Paired round deltas (histogram minus mean, ms): -0.123, +0.112, -0.038, +0.040, +0.007, -0.023, -0.164, +0.016; mean -0.021, standard error 0.031. Two earlier 3-round runs gave pooled deltas of -0.024 and +0.080. The meter is below the instrument noise (single rounds swing about 0.1 ms, one resolved sample per 300-frame window), so the claim is "not measurably above the mean meter", not "0.00 ms". The first single-stage gather cost 3.28 ms and was replaced by the tiled one (Decisions).

Still open

The Pixel 8 cost check is under ## Blocked on (needs the Pixel 8 attached to the desktop, owner: Joao). Every other box is ticked. The PRD sits in docs/PRDs/BLOCKED/requires-physical-device/ until the device check runs, so this PR is held for João’s inspection; no merge or archive is authorized, and the label is not a merge signal.

Before and after (mean meter vs histogram)

Headed --browser-recipe webgpu, adapter nvidia/turing RTX 2080, two runs per arm, BEFORE and AFTER burned in. Fresh read-only judge: IMPROVEMENT on all four pairs, runs agree. The white strip in the sky room is the fixture's intended bright band.

Room Before (mean) After (histogram)
sky run 1
sky run 2
disc run 1
disc run 2

Notes for review

  • Scaffold hashes PRD_201_PARENT_SCAFFOLD_HASHES re-pinned for the 13 templates.
  • The branch also carries the two local-only docs commits that file the PRD batch (PRD-560 to 572) and PRD-550. They duplicate what PR feat(core): shadow casters sort themselves into static and moving (PRD-572) #477 carries and skip on rebase.
  • The native proof used a prebuilt desktop binary (2026-10-05). The change is template JS, so no native source is in this diff.

Generated from the PRD's checkboxes. Regenerate on each push so the PR and the PRD never disagree.

Native contract correction — 2026-10-10

Commit 1a3e2eda6 corrects the 65×33 native fixture’s stale unclipped expectation. The aligned edge tails carry 1.4768% and 4.0098% of metered weight; 10/90 clipping leaves luminance 2 and target −3.473931188 stops. Product rendering, fixture pixels, and assertion tolerances are unchanged. A regression against the independent CPU reference failed before the correction and now passes.

Focused verification: 77 exposure tests, 31 documentation tests, package typecheck, focused Biome, documentation links, precommit, and normal pre-push ci:fast passed. Native execution remains unverified locally: the contract executable is absent (ENOENT). Windows/macOS native contract reruns and the Pixel 8 cost proof remain pending. The previous merge-group run 38071098944 failed; this push does not claim that run passed.

PRD-manager reports 6/6 phase boxes and 2/2 acceptance boxes, 100%, blocked-only on the physical-device measurement. Existing before/after comparisons above are retained; no new visual judgment was performed. Hold this PR for inspection; do not merge or archive.

@jonit-dev jonit-dev added the prd:100% — ready PRD complete, ready to merge label Oct 10, 2026
@jonit-dev

Copy link
Copy Markdown
Contributor Author

Labelled before/after captures (two runs per arm, headed WebGPU, RTX 2080), judged IMPROVEMENT on all four pairs by a fresh read-only subagent.

Room Before (mean) After (histogram)
sky run 1
sky run 2
disc run 1
disc run 2

@jonit-dev
jonit-dev marked this pull request as ready for review October 10, 2026 04:45
@jonit-dev
jonit-dev enabled auto-merge October 10, 2026 04:46
@jonit-dev
jonit-dev disabled auto-merge October 10, 2026 05:42
@jonit-dev
jonit-dev force-pushed the feat/prd-571-histogram-exposure branch from b37fe41 to 314faac Compare October 10, 2026 06:00
@jonit-dev
jonit-dev enabled auto-merge October 10, 2026 07:12
@jonit-dev
jonit-dev force-pushed the feat/prd-571-histogram-exposure branch from 476ce03 to ea5db91 Compare October 10, 2026 07:13
@jonit-dev
jonit-dev added this pull request to the merge queue Oct 10, 2026
@jonit-dev
jonit-dev removed this pull request from the merge queue due to a manual request Oct 10, 2026
jonit-dev and others added 8 commits October 10, 2026 05:47
…mean (PRD-571)

Gather arm: level 1 (16x16 blocks) feeds a 64x1 pass, one texel per bin; adaptExposure walks
the bins with UE's lowPercent/highPercent clip. Adds lowPercent/highPercent (10/90) to
IExposureSettings and copies the three files to all 13 templates.
…d native (PRD-571)

The single 64-thread gather over the second reduction level cost 3.28 ms
at 1080p on the RTX 2080. Draw it as 64 bins x 64 tiles, then sum the
tiles in a 64x1 pass. The histogram, clip and adaptation maths do not
change.

- exposureGraph.ts and autoExposure.ts: histogramTiles + histogramExposure,
  byte-identical in template-assets and all 13 templates.
- Fixture: histogram CPU reference and probe, backlit sky and sun-disc
  rooms, a mean-meter baseline served from 9d4b2a6 as the red control,
  and measureMeterCost.ts for the AC-2 GPU window comparison.
- Cold-boot and native reference luminance 3.896 -> 3.058 (clipped log mean).
- agent-docs/references/auto-exposure.md names lowPercent, highPercent and
  the meter weight.
- Re-pin PRD_201_PARENT_SCAFFOLD_HASHES for the 13 templates.
…om the 200-line smell cap

The histogram meter adds two passes to AutoExposureNode (tiles, bins), so
the generated file grew from 194 to 225 lines. It stays plain Three.js and
framework-free, which is the rule the cap guards; looks.spec.ts now skips
only the length check for it, as it already does for loading.ts and
worldEnvironment.ts.

PRD-571: all 8 boxes ticked with command and result. The Pixel 8 cost
check is recorded under Blocked on.
Every box is ticked except the Pixel 8 cost check, which needs the device
attached to the desktop (owner: Joao). The PRD names it under Blocked on.
README and PRD links repointed.
Headed WebGPU captures on the RTX 2080, two runs per arm, BEFORE and AFTER
burned in, fresh read-only judge: IMPROVEMENT on all four pairs.
…D-571)

Pins regenerated from the spec diff, not hand-merged.
@jonit-dev
jonit-dev force-pushed the feat/prd-571-histogram-exposure branch from cfe8ef1 to 25950e9 Compare October 10, 2026 12:49
@jonit-dev
jonit-dev added this pull request to the merge queue Oct 10, 2026
@jonit-dev jonit-dev added prd:50% PRD half the phases verified prd:100% — ready PRD complete, ready to merge and removed prd:100% — ready PRD complete, ready to merge prd:50% PRD half the phases verified labels Oct 10, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 10, 2026
@jonit-dev

Copy link
Copy Markdown
Contributor Author

Fresh candidate CI is green for 1a3e2eda6da10dd660bd13b54742116067e43ba9: run 38075417271, including ci-required SUCCESS. This run includes the repaired native histogram contract.

The PR remains open for João’s inspection under the current review hold. No merge or deployment was triggered. Physical-device limitations remain explicitly recorded in the PRD.

@jonit-dev
jonit-dev added this pull request to the merge queue Oct 11, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 11, 2026
@jonit-dev
jonit-dev merged commit b4e7462 into develop Oct 11, 2026
78 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prd:100% — ready PRD complete, ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant