Repository navigation
fix(world): PRD-494 — bundle-safety walk stops reading pixels; main pass draw budget measured - #473
Conversation
… (PRD-493 phase 1) Sixteen same-size uncompressed layers bound one sampler each, which is what makes WebGPU's 16 sampled textures a stage the real cap on Android and iOS — the hosts with no KTX2 transcoder, where nothing stacks today. Each set now becomes one DataArrayTexture whose layers are filled by a renderer.copyTextureToTexture out of their own textures: the pixels never come back to the CPU, and the array is created with source.dataReady = false so three never uploads placeholder bytes over the copies. Mixed sizes still fall back, and TN_TERRAIN_SPLAT layers= samplers= stacked= reports what the material actually costs, one line per material. Also corrects the stale @constraint: the recipe is export_terrain_layers in export_world.py, and the cap is now a sampler count the marker names. capabilities.json and the generated reference carry both.
The material hard-coded metalness 0 / roughness 0.92 and gave every layer it, so a Megascans-grade layer's own occlusion, roughness and metalness never reached the screen. A layer with "orm": true in the table now reads <id>_orm.jpg as linear data (r occlusion, g roughness, b metalness) through the same stacked array and the same blend weights as its colour; a layer without one uses the roughness and metalness the table states. A layer that states neither now throws and names the recipe, rather than falling back to a number in the package — the constant is gone. The recipe writes _orm beside _diff and _nrm in both copies of export_world.py, and refuses a table whose ORM layer names no orm source.
…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.
… captures
Unit specs prove the mechanism in node; this example proves it on a real
WebGPU adapter. The package is sixteen layers with an albedo, a normal and an
ORM map each — 48 uncompressed JPEGs — written by the shipped
export_terrain_layers recipe from a table and map files a game would author,
so the thing under test is a DCC export, not a second format.
The run is green: TN_TERRAIN_SPLAT layers=16 samplers=4 stacked=3, 180 frames,
a non-blank changing viewport, and the scenario asserts those numbers through
material.userData rather than trusting a screenshot. The same package on
develop never reaches a frame:
Uncaptured WebGPU GPUValidationError: The number of samplers (34) in the
Fragment stage exceeds the maximum per-stage limit (16).
Captures for the judge, same package and same pose:
.afk/scratch/prd493-captures/phase-1-2/{before,after}.png (4 KB uniform vs a
textured 16-band surface). Not a Machinefall result, so AC-1 stays open.
…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.
The browser lane proves the stacking; the hosts that matter most are the ones with no KTX2 transcoder, and there nothing stacked before. This gives the example a native identity (threenative.config.ts, one description read by both the web dev server and `threenative build`) and the same assertions as a desktop scenario: sixteen layers, at most four samplers, three stacked sets. orbitSeconds is new because a native run that renders one frozen frame would otherwise satisfy every cost assertion; the camera pose has to advance. --target desktop on the built host: pass, layers=16 samplers=4 stacked=3, orbitSeconds 2.03 -> 5.03, diagnostics clean, 0 console errors. The web scenario with the same assertion is pass on the software-and-hardware WebGPU recipe; world-terrain-splat.spec.ts 11/11.
The hosts this PRD is for cannot see KTX2, so nothing stacked there, and nothing proved it: the browser lane cannot fail on a path the browser never takes. terrain-splat-array builds a sixteen-layer package with an albedo, a normal and an ORM map per layer — the shape Android and iOS ship — through loadTerrainSplat, and the scene throws unless the marker reads layers=16 samplers=4 stacked=3, so the row fails on a regression to one sampler per map rather than merely looking different. The 48 maps are DataTextures derived from a pure function of their cell and the mask grid gives each layer its own region, so both lanes derive the same frame from nothing but the scene file: the desktop host matches the browser reference at pixelMismatchRatio 0 and perceptualDeltaE 0 over 1280x720. The Android leg of the row is left open, not claimed: :app:verifyV8Dependency refuses to bundle without a V8 build receipt that only an hours-long source cross-compile writes, and the emulator was up and waiting when it stopped. Gates: parity --target web pass (reference captured); parity --target desktop pass, 0/0 metrics, 0 gpu validation errors; runtime-native conformance suite 76/76; pnpm typecheck and pnpm lint exit 0; pnpm check:docs exit 0.
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).
Merging PRD-493 under PRD-492 moved every no-install scaffold hash: the rewritten `capabilities.json` entry and `capability-reference.md` reach a scaffold verbatim. Measured through actual createProject trees on this merged tree, which is what the table already claims to hold.
…walk, and the two reasons Five map-walk runs on origin/develop's core, GPU quiet at every start (6-15%), host load 18-48, so the CPU numbers are inflated against the earlier 11-19 runs and only the paired lane comparison counts. Median walking render 13.4/25.9 ms, GPU 3.6/21.6 ms, 48 frames over 33 ms of 1024, worst present gap 17.1 s. The pipeline census is what A7 needs: 8 creations attributable to a streamed batch after startup.compileSettledMs in every run (7 main pass, 1 shadow caster half; device service 1.1-1.6 ms), plus 16-22 compute creations and 12 main / 4-5 shadow for objects outside the shared batches. Two causes, both named rather than inferred: - #dressGpu cannot re-dress a mesh three has compiled (its instancing node binds the instanceMatrix it held then), so a key dressed after the prewarm gate gets a fresh object whose node and pipeline its first draw builds, on a walking frame. - TN_WORLD_PREWARM reads shadowPrewarmed=0 castersUnbuilt=182 in every run: the prewarm mints an empty batch and waits for a draw to build its node, and three never submits one — getDrawParameters() returns null at count === 0. AC-3 stays unticked with the measurement beside it. Two attempts to close it on the engine's own compileAsync seam failed on the walk itself: deferring the swap to that compile hangs a live renderer (900 s timeout, walk-a7-r2-1), and doing it behind the loading gate serialises ~200 key compiles behind prewarmed and does the same (walk-a7b-r2-1, aborted at 10 min). A7 needs a preparation seam that does not borrow the frame the walk is drawing, which is PRD-387's open Phase 1.
…t refuses A7's measured cause, one of the two named on 2026-10-04: TN_WORLD_PREWARM read shadowPrewarmed=0 castersUnbuilt=182 in every walk, because the prewarm leaves an empty batch owed a draw that three never makes. `RenderObject.getDrawParameters` returns null at `count === 0`, so the renderer skips the object, builds no node and never calls `onBeforeRender`: 385 keys minted behind the gate prepared nothing, and the walk's first shadow window move built all of them inside a shadow pass. An empty batch still owed its draw now submits one instance of the zero matrix its own fresh buffer already holds (`SharedBatch#publish`), which is the submission that builds the node and the pipeline, main and shadow. It draws nothing — every vertex collapses onto one point — and `prewarmDrew` puts the honest count back. `prewarmInstance` publishes on the mesh which count is the prewarm's, because `casterPrewarmOwed` cannot answer it: a batch can owe its draw and hold records at once. A batch leaving the world hands the borrow back rather than submitting a degenerate instance for the rest of the load, and the prewarm's own submission is not recorded into a draw bundle. The red-green is the harness, not a new one: `world-cells-shadow-prewarm` counted a draw three would not submit (no count gate), so it passed with 8 of 17 casters unbuilt. With three's own gate in both arms it read `castersUnbuilt=8` before this change and `castersUnbuilt=0` after, and every prewarmed caster's first draw is still behind the gate. `pnpm exec vitest run packages/core/__tests__` 184/184 green, `pnpm typecheck` and `pnpm lint` green. `pnpm test` still fails 22 native-host cases in `packages/runtime-native` that need `pnpm native:build` in this worktree — untouched by this change.
…d it blocks the launch The first of A7's two named causes is now closed at the mechanism level and unit-proved, and the walk says the fix cannot merge as it stands. Alternating arms in one lane, map-walk with tnFrameSpans=1, host load 24-32 throughout: - dev pair-1 (core-dev2, 1a6298cd): pass in 2 min 59 s, 18 of 127 creations after settle attributable to a batch key (1.7 ms of device service), 13 of 1024 frames over 33 ms, walking render 8.2/15.7 ms, GPU 1.6/10.6 ms, 17.8 s to compileSettled over 1895 pipelines, and the baseline's own line unchanged: minted=385 shadowPrewarmed=0 castersUnbuilt=182. - dev pair-3 (same core): fail, 2 state failures and 3 console/network errors over 4382 frames. That is this host's noise, not a result, and the lane claims no CPU comparison from these runs. - a7 pair-2 and pair-4 (core-a7b): both stall at the loading gate. TN_STARTUP_STALLED reports 0.0 % progress for 45 s with nothing outstanding, the page's main thread never yields again (the bridge's `describe` exceeded 915000 ms), the 900 s timeout takes the run, the prewarm marker never prints and the walk never starts. No census, no frame series, nothing to capture. What the row now knows: the mechanism is right in the fixture world and fatal at Machinefall's scale, where 385 keys by two variants behind the gate is a different cost from 24 by two. What it does not know is which of two costs the gate is paying — the node builds themselves, or `#drainPrewarm`'s own loop, which invalidates the shadow levels on every update while a caster is owed and now has every prewarmed caster to submit each time. The next step is one instrumented run that separates them. AC-3 stays unticked, the A7 boxes stay unticked, and the arm stays a draft.
Reverts 2d6b930. With it, Machinefall map-walk never leaves the loading gate (two of two runs, 900 s timeout, TN_STARTUP_STALLED). The runbook A7 row keeps the finding; the change returns once one instrumented run names the cost the gate is paying.
…t its loading gate PR #437's `loadTerrainSplat` threw when any terrain layer declared neither an ORM map nor a roughness/metalness pair. Machinefall's exported `terrain/layers.json` predates that column: its base layer states neither and its layers state roughness without metalness. The world's `load()` therefore rejected, `MapWalk.load()` never returned, `state.loading` stayed true, and the playtest timed out in `describe` with 0 frames. A table the earlier export recipe wrote is not a malformed table, so the check now names the layers it cannot honour and the surface builds: `DEFAULT_ROUGHNESS` (0.92, the matte dielectric this material carried before the column existed) and metalness 0. Loading a package is a promise the game cannot cancel, and a hang at the loading gate costs a whole boot. Machinefall loads its layers as KTX2 `CompressedTexture`s, so the uncompressed `stackUncompressed` path the same diff added never runs for it; the throw was the only new blocker. Tests: the spec now feeds `loadTerrainSplat` Machinefall's own table shape (base with no response, layers with roughness and no metalness) and red at the old throw, green after. The old "refuses a layer that states neither" spec encoded the hanging contract and became this one; PRD-493's sixteen-layer stacking and ORM specs stay green.
Machinefall's first frames after the loading overlay drew a meadow of canopy shadows with no trees under them (about 1 run in 6; owner report on #437). With the GPU scene on, the main pass draws only sources the seed has placed into the dispatch, and the seed queues behind the admission budget (`TN_WORLD_GPU_SCENE on ... instances=0`), while every caster half drew its whole resident ring. - WorldCells counts each caster half's placed sources (`SharedBatch.gpuPlaced` against `live`, kept in #placeSources / #clearSegment / the impostor handoff) and publishes `mainAdmitted` per half; the wide `@*` mesh casts once every cell of its asset is placed. - VirtualShadowNode#probe hides a half that is not admitted for each level render (the existing #hidden restore buffer), so admission gates the draw, not only its tally. Admission, not visibility: an off-screen caster whose counterpart is placed still casts. - Admission is the third bit of the per-caster flag word #pollCasters reads, so a flip redraws the cached levels covering that half; without it the maps drawn during the seed stayed shadowless. - TN_WORLD_SHADOW_ADMISSION reports withheld cluster and wide halves and their records (Machinefall: 249 + 47 halves at the seed, 0 after). GPU (RTX 2080, Machinefall map-walk, develop base vs this change, 6 runs each, merge on and off): no orphan frame in the fix runs; forest-floor brightness at the start frame 212 vs develop 211 (the withheld-but-cached state read 226-237); walk frames within 1-2 units, the residual is canopy self-shadow speckle, not ground shadow. Specs: the render-boundary admission spec, the per-half placement spec, the seed-drain specs and two admission-flip invalidation specs, each red before its change and green after; 246 world and shadow specs pass.
The launch hang blamed on the prewarm was the PRD-493 splat throw; with it fixed the prewarm loads but prepares nothing on Machinefall and moves no number (12 alternating runs, order effect only), so it stays off this PR. No post-settle compile costs a frame (max 0.2 ms). The walk on this PR: render p95 6.4 ms, GPU p95 6.5 ms on a loaded host; AC-1/AC-2 stay open for the quiet interleaved rerun and AC-3 raters.
A1: PRD-389 Phase 4 reads 5/0 on origin/develop, one box per probe gap, landed by #423. C1: #397 merged and PRD-339 is in done/ at 11/0. PRD-492 AC-2: visuals:ab with 3 fresh blind raters scores the graded starter frame equal to the ungraded one (4=4, 3=3, minimum detectable effect 0); the HUD moves at most 1.9 codes against 112 in the scene.
The owner asked for a realistic goal before the merge. The main pass alone is 25-45 ms on the Iris Xe at scale 1.0, so AC-1/AC-2 now ask for a 30 fps median and at most one invalidation redraw per 3 s. Records the redraw counters and ticks the GPU-priced invalidation delay (748aab4).
The laptop walk (live11, Iris Xe) still held the scale at 1.0: the stall exception asked for presented p50 over budget, and that read 6-9 ms while the main pass took 25-40 ms and the window ran at 26-45 fps. The exception now asks for a window under target with fresh GPU over budget. A hitchy game whose GPU meets the budget keeps the stall deferral. resolution-scaler.spec.ts: the live11 window, red before, green after; 8 scaler spec files, 82 tests pass.
AC-1: traversed main draws p95 48 in the load window and 85 at worst over 3 Machinefall map-walk runs on the final core. The world gate shows 0 LOSS and 0 disallowed popping but exits 1 on its absolute 4/5 floor, which the reference arm misses too; the PRD records that, the capture race on pose-start, and the adaptive LOD bias as the likely owner of the mid-field frame. Native leg: the streamed-world desktop scenario passes on the checkout host with bundles on, after the native render bundle encoder gained indirect draws. AC-2 sign re-confirmed under load; the quiet-host tick stands. The runbook's link to the PRD follows it to done/.
|
Heads-up for whoever reviews or merges this PR: the body above was written when this branch held only PRD-494 work. The branch has since been fast-forwarded to the PRD-478 runbook branch (head The PRD-494-only work is in #475 (branch |
The owner wants one PR for PRD-494. #475 carried the sharded main record, native bundle indirect draws and the PRD close; it lands here instead. Conflicts: world-cells.ts (kept gpuPlaced and the shard wording) and the native census (regenerated with pnpm census).
CI quality-json.spec failed on #473: 5 suppression-class findings were new. merge-parts.ts (3) and profiling/Spans.ts (1) casts get a quality-allow line naming why the type cannot say it; water-surface.ts carried a waiver for a cast that is no longer there. pnpm quality --json and quality-json.spec pass.
budgets and check-native-coverage.spec failed on #473 with a stale source digest: the merged runtime-native bundle indirect draws changed the inputs. pnpm --filter @threenative/runtime-native native:coverage, check passes.
…t resolve live12 on the Iris Xe still held the scale at 1.0: every window reported gpuAgeFrames 233-593, past maxGpuAgeFrames 16, so the scaler saw no GPU reading at all, while each window held its own samples at a 29-50 ms median. When the last resolve is stale and the window has at least 4 samples, the scaler now reads their median. A GPU that resolves within 16 frames keeps the old path. resolution-scaler.spec.ts: the live12 window, red before, green after; 8 scaler spec files, 82 tests pass.
live13 (lodfix11): the scale went 1.0 -> 0.61 four windows into the walk and held the floor; main-pass GPU median 29-50 ms -> 18-33 ms, late-walk fps 33 -> 47-50. Ticks the Phase 1 gate box with the three fixes it took. Records the cold-start gap of the redraw delay (42 of 54 redraws before the first GPU reading, then one per 7 s) as a new open box.
The starter's `.cube` grade is a Data3DTexture, so every scaffolded game uploads one. Three asks for RENDER_ATTACHMENT on every uncompressed texture, and WebGPU has no 3D render attachment, so wgpu-native rejected the table and then every view, bind group and pass naming it. The patch already withheld the usage from a compute-only 3D storage volume; it now withholds it from any 3D texture that no render pass writes, which is what a sampled table is. The upgrade migration blocks for the same three files and the lockfile patch hash follow, so a project upgrading from a previous release lands on the same bytes a fresh install gets.
…ract dcb5204 made WorldProbe publish stats().bundle and dropped the web-only diagnostics block from the desktop streamed-world scenario; the spec still stubbed stats without bundle (5 TypeErrors) and still demanded the block. The stub carries a bundle reading, desktop now asserts bundleOn instead, android keeps the diagnostics block. world-capture-fixture.spec: 12/12.
… fix Every scaffold copies core/patches/three@0.185.1.patch, so 1d394b0 moved all 13 no-install tree hashes. Recomputed from the drift run; the byte-stable test passes.
The PRD-351 ladder encoded all four compressed rungs of the starter's 4096x2048 sky.jpg (50 s cold, 90 s under load) and then shipped the source: no rung beat the 282 KB JPEG. CI timed out template-assets-compile, build.spec and the golden-path servers on it. ETC1S is the ladder's lowest bitrate, so on an image over 256x256 an ETC1S encode that cannot beat the source bytes now skips the UASTC rungs. Small images walk the whole ladder; floors and smallest-passing selection are unchanged. Starter cold cook: 90 s -> 22 s at load 27-44 (develop ~20 s). model-texture-pass.spec: "stops encoding a large image once ETC1S cannot beat its source bytes", red (4 encodes) then green (1); assets suite green but for a worker-pool load timeout that passes alone.
temp-dir-guard.spec failed on #473: grade.spec.ts created a directory with raw mkdtempSync. It now uses makeTempDirSync, which registers cleanup.
Desktop parity failed both new rows on PR 473 on pixel mismatch alone: lut-grade 2.4% against 0.01, world-terrain-inline-hash 4.6% against 0.04. The metric counts any channel inequality; per the downloaded captures only 0.04% of pixels differ by more than 2 levels and mean deltaE is 0.047 and 0.034 (limits 3 and 6), so the rest is llvmpipe-vs-Chrome rounding in the 3D table filter and shading. Tolerances 0.04 and 0.06 (the registry's common value), each row's reason records the measurement.
…rites Shard 1/4 still reported a grown temporary directory count after 877d671: the identity table is written at module load, outside any test, so makeTempDirSync fell back to a process-exit hook that a vitest worker never runs, and tn-grade-* stayed behind. writeTable now removes its directory itself. Shard 1/4 with an isolated TMPDIR left tn-grade-M7crt3 before; the spec pair leaves nothing after (12/12).
Develop gained PRD-461 (#470) and PRD-536 (#458); PR 473 had gone CONFLICTING and CI stopped running on it. Conflicts: world-terrain-splat spec (both sides added tests, kept both) and the scaffold hashes (rain moved with develop's template change; recomputed). Core typecheck clean, core world specs 500/500, scaffold.spec 66/66.
pnpm census: 2 cells, total 267,266.
The merge queue dropped PR 473 on golden-path-template (platformer): the dev server answers only after the cold cook, one ETC1S encode of the template's 4096x2048 sky is 21 s idle, and the 30 s wait ran out on a loaded runner (TN_GOLDEN_PATH_FAILED at layer dev, 33 s after start). verify-golden-path.spec 23/23.
lodfix11 (PR 473 merge build), quiet RTX 2080: walking render over 8.3 ms pooled 4.1% (509/7200, 291/9600, 306/9900) and GPU 0.39% (12/3085), both under the 5% bound. PRD-478 is left with the owner's aerial review only; PRD-541 with its blind raters on the clamp build.
|
Label
AC-1 (PRD-478) on the merge build, 3 quiet RTX 2080 runs: walking render frames over 8.3 ms pooled 4.1 % (bound 5 %), GPU 0.39 %. CI fixes since the last review: 3D textures no longer request |
TL;DR
Finishes PRD-494 (the main pass fits the draw budget). PR #423 already merged the PRD's engine code. This PR fixes a load stall that the merged code caused on Machinefall, and records AC-1 and the native leg.
Why
On develop
f7dd08621, Machinefall?scene=map-walknever finishes loading under the playtest harness: the bridge times out (TN_PLAYTEST_OPERATION_TIMEOUT,sample> 15 s). A Chrome trace showed one main-thread task of 22.9 s. 18 s of that wassamplesFramebuffer, thebundleSafematerial walk: it enumerated every texture's mip bytes throughObject.values.What changed
packages/core/src/world-cells.ts: the walk skipsArrayBufferviews (a pixel or vertex array holds numbers, never a node).packages/core/__tests__/world-bundles.spec.ts: red-green spec. Red: the walk read a spy on a texture's pixel array 11 times (expected 11 to be +0). Green: 15 of 15.Evidence so far
samplesFramebufferframes are gone; worst task 22.9 s -> 15.1 s (the rest is three's shader node build).Done / missing
--target desktopstreamed world,TN_WORLD_BUNDLEin the log)pnpm visuals:ab --raters 3)