Repository navigation
feat(assets): consolidate native capabilities and high-quality cook - #396
Merged
Merged
Conversation
Partial implementation. Preserve decoder-free fallback and existing desktop V8 behavior; no newly admitted codec. Real runtime screenshot and full native loader qualification remain blocked.
Exercise real selected runtime, decoder-free normalization, bundled bytes, native mailbox screenshots and named adapter provenance. Prepared capture lane; no unrun native PASS claimed.
The actual QuickJS bridge does not expose runtime.diagnostics. Keep native readiness and console errors, require complete real host logs, and fail closed on GPU/device-loss/JavaScript/loader failures even when classified as log. Engine guards unchanged.
Exercise actual bridge target admission locally. Only native-supported startup/resource families belong in the scenario. Native console cleanliness, ready/compile-settled state and device screenshot provenance remain required in the verifier. No engine target guard changed.
Preserve the original native framebuffer and runner summary from successful run 36998104447. Record exact source/runtime/package/adapter/image identities, verified digests and bounded decoder-free fallback scope. Full VQ01/mobile/codec acceptance remains open.
# Conflicts: # docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-01-native-asset-capabilities.md
…flow, per AGENTS.md) The `native-assets` lane joins the shared `paths` gate as an anchored ERE translated from the old workflow's `paths:` globs, and `packaged-quickjs` keeps its steps, env, timeout and artifact names on the same ARM64 QuickJS lane behind `needs: paths`. Editing integration.yml keeps the lane's gate true, exactly as editing integration-native-assets.yml used to. The PRD record now names the lane instead of the deleted file.
Pure biome formatting of docs/verification/vq01/quickjs-native-summary.json, committed on this branch in f13f1ad and untouched since; every value is identical.
Develop moved the PRDs into contextual folders (#419) and rebuilt the integration lane selector around scripts/ci-integration-scope.mjs. Conflicts resolved: - docs/PRDs/assets/PRD-VQ-01-native-asset-capabilities.md: kept this branch's completion record and hosted evidence. Develop's sibling PRDs at the new paths drop the deleted batch EXECUTE.md link, so the surviving sentence uses the same plain-text form. - .github/workflows/integration.yml: took develop's TN_LANE_FILTERS selector and its capture/fluid jobs, then re-applied this branch's native-assets lane: the paths output, its source filter (without the now-redundant self-reference on the workflow, which develop's selector derives from job diffs), and the packaged-quickjs job. That job had lost its steps when the standalone lane was folded in, so the steps come back from 8d6cd88.
…he merge Each Phase 1 and Phase 2 box names a unit proof command. Re-ran them at the merge commit c29a210: the capability suite passes 19/19 and the capability plus fixture pair passes 21/21. The results sit beside each box; the existing partial notes stay, so the open Android artifact resolution, decoder versions, KTX2 block-format validation and manifest-versus-payload work are still recorded as open. Phase 3 and the acceptance boxes are untouched — their proofs are the browser and Android scenarios, which did not run. Also repoint the verification README at the PRD's new assets/ path, which is what pnpm check:docs flagged after develop's folder move.
…lter The re-applied native-assets filter still claimed all of packages/assets/src, which develop's world-capture lane now owns. scripts/__tests__/ci-integration-scope.spec.ts proves a packages/assets/src/index.ts change selects world-capture alone, and it failed on the overlap. Narrow the claim to the two modules this lane's proof actually depends on, compile.ts and report.ts, the decoder-free cook and the build report. Selection checked against the real parser: build.ts and compile.ts select native-assets, index.ts still selects world-capture alone.
…fixture The root tsconfig mapped the bare `@threenative/core` specifier to `packages/core/src` and left the `./playtest` subpath to fall through `node_modules` to `packages/core/dist`. The VQ-01 fixture at `examples/abyss-framework/vq-assets/src/game.ts` puts both in one expression (`plugins: [playtest()]` beside `defineGame`), so nominal types from two copies met and `pnpm typecheck` failed with `#private` missing on `RenderChain`. Map the subpath to its source entry like the bare specifier, the way a sibling example's own program already resolves both through the package exports.
Contributor
Author
|
Runbook B1 (2026-10-03): merged origin/develop (#419 moved this PRD to |
decodeImageBytes reads WebP through sharp — already declared in this package's dependencies and unused until now, so no dependency and no new native binary reaches a game (every template already ships the same one via @gltf-transform/cli). An embedded WebP now cooks like a PNG: KTX2 on a target that decodes it, a capped PNG on the decoder-free path, and the pass disposes EXT_texture_webp once no image is left in it because the glTF writer emits every attached extension. The pass's "never grow a native dependency" rule changes with it: this is a Node-only build-time package and sharp is the only native thing in it. Gates: red before (TN_ASSETS_TEXTURE_CONTAINER: 'character.glb#checker' is not a PNG or JPEG), green after — 26/26 model-texture-pass, 474 passed / 2 skipped across packages/assets/__tests__, pnpm typecheck, pnpm lint, assets build + publint.
The Phase 3 web box names a browser command but nothing could serve the fixture to it: examples/abyss-framework/vq-assets had no web entry at all, and `threenative build` refused it with UNRESOLVED_ENTRY. - index.html + src/main.ts: the minimal template's own canvas mount, so the web build has the entry page every Vite build needs and the unchanged game and cooked assets are what render. - scripts/vq01-web-proof.sh: generates the authored Meshopt/Draco GLBs and PNG through the same function the native verifier uses, cooks the fixture for web, and runs the PRD's proof command against a managed preview server on 127.0.0.1:5193. The box stays open. This lane has no browser, so only the parts a browser does not need were verified: the build publishes dist/ (exit 0), the scenario loads through loadPlaytestScenario, the server command answers 200 for the page and for the 1034430-byte game bundle, and the fixture still typechecks.
…an empty icon Without an icon link the browser requests /favicon.ico, which 404s and fails noConsoleErrors. Phase 3 box 1 ticked with the run.
The web proof writes .generated/; biome reads only the root .gitignore, so the nested ignore did not keep it out of the formatter.
…ator The Android box now has a runner and a result. The box names a playtest command, but that runner launches an already installed app and never builds or installs one, so scripts/vq01-android-proof.sh adds the missing half the web lane already had: it cooks and packages examples/abyss-framework/vq-assets for Android V8 through the maintainer source build, installs that APK on one named serial, and then runs the canonical proof command against it. Result on the emulator (not a phone): serial emulator-5554, AVD threenative_api35, API 35, x86_64, -gpu host. Exit 0, 8/8 assertions - loadedModels 2, texturedMeshes 2, vertices >= 100, frames changed, maxPoseDelta >= 0.05, startup entered 955.7 ms / ready 1055.1 ms with compilation settled, and diagnostics with zero console and zero runtime entries. logcat reports "JS engine created: V8"; the adapter is native:vulkan/NVIDIA/NVIDIA GeForce RTX 2080/615.71.09 and the capture is device.screenshot. The after-frame carries each specimen's authored checker. Android's admitted codec set is still empty, and the box says so rather than implying otherwise. resolveRuntimeAssetCapabilities reports android:unresolved with an unknown engine, so the cook refuses each compressed source by name - TN_ASSETS_COMPRESSION_SKIPPED for model and for texture - and the APK carries the decoder-free rewrites (draco 4,956 -> 20,240 B, meshopt 4,096 -> 3,908 B, checker.png 187 B). What this proves is that the packaged loader decodes what was packaged and that the refusal path works on a device; no qualified mobile compressed decoder is claimed. No executable code changes, so the QuickJS decoder-free fixture is untouched and stays green on hosted run 36998104447. Verified: sh scripts/vq01-android-proof.sh artifacts/vq01-android-verify emulator-5554 exits 0 with the same 8/8; pnpm check:docs clean; the doc, evidence, CI-structure, CI-needs and primary-docs suites pass 248/248.
…cle scenario still owed
…t scene AC-2 owed a Phase 3 scenario that repeats the transition: the fixture now counts its own enters and, after a 30-frame settle window, leaves through the engine's scene-change API (ctx.goto) for an empty scene that returns one frame later, four enters in all. Each enter re-derives the renderer's live GPU tally (ctx.renderer.info.memory.geometries/textures) and publishes it beside the growth against the first enter's baseline, so a resource the exit path forgets to dispose shows up as growth instead of as a missing number. examples/abyss-framework/playtests/vq-native-asset-lifecycle.playtest.json gates on state.settled 2 -> 3 -> 4 and then asserts entries 4, loadedModels/texturedMeshes back to 2/2 and geometryGrowth/textureGrowth 0, on web and Android like the single-load scenario. scripts/vq01-lifecycle-proof.sh <artifact-dir> web|android <serial> is that scenario behind the same cook as the two existing VQ-01 proof scripts. Unrun: no browser or device run of the new scenario here. Validated through the repository's loadPlaytestScenario, tsc on the fixture, pnpm lint, pnpm check:docs and the six doc-contract specs; the AC-2 spec still passes 19/19.
…85 phase 1) lumen-hall's lady.glb and sanctuary.glb cook with `compression skipped: not-smaller` three times each, so the shipped GLBs kept `images: image/webp` and required `EXT_texture_webp` — a container the native desktop/Android/iOS decoder-free paths cannot read. Retention is now conditioned on the container as well as on size: only `UNIVERSAL_IMAGE_CONTAINERS` (PNG, JPEG) may keep authored bytes, so a WebP always converts — to KTX2 on a decoding target even when that is larger, or to the capped PNG on the decoder-free path. `EXT_texture_webp` then always leaves the output, and the size increase is reported by the existing `bytes before -> after` row of the build report (+621% on lady.glb), with no new marker. PNG/JPEG behaviour is unchanged. `keepSmallerSource` moves from `true` to `"universal-containers"` in the model pass's cache key and in the shared-image store settings: a warm cook keyed on the old rule would otherwise keep shipping the WebP-requiring models this replaces. Verified: red (519 -> 519 bytes retained, extension still required) then green in the `embedded EXT_texture_webp` block of `packages/assets/__tests__/model-texture-pass.spec.ts`, whose new PNG leg pins that a PNG whose KTX2 would be larger is still kept; file 28/28, `packages/assets` 476 passed, `pnpm typecheck` and `pnpm lint` clean.
three clones through `new this.constructor().copy(source)`, so `ClusteredMesh` died in its own constructor's table check before a game drew anything: the default cook puts a cluster DAG in every model, and lumen-hall's authored statues stood a second copy of one. `clone()` now builds from the source's own arguments, and `copy()` completes the job the three way: the baked table and the vertex attributes are shared — one copy of the vertices on the GPU, since three keys attributes on the attribute — while the cut is the copy's own, because the cut lives in the geometry's index buffer and two meshes on one geometry would overwrite each other's every frame. `ClusteredBatchRoot.clone()` keeps its batch, so a copied root is still cut instead of freezing on the cut it was cloned with. Red: 5 failing specs on `packages/core/__tests__/clustered*.spec.ts`, the game's TypeError. Green: 49 in those four files, 2322 across packages/core, plus typecheck, lint and build.
…rement artefact ca1bd9c made the fixture leave and re-enter its own scene unconditionally, which broke the original single-load scenario (state.frames stopped advancing after the first leave) and produced geometryGrowth 0 -> 6 with ownedGeometries 1 -> 7 on a web WebGPU run. Neither is a leak. The cycle is now armed by one key. The lifecycle scenario's first step presses it, and both runners deliver a `press` step as the same keydown — page.keyboard.down on the browser lane and the device bridge's input.keyDown, which synthesises keydown with `code` into document, window and canvas (packages/playtest/src/three/device.ts:220). A URL query was not an option: the native host's document.location is a fixed file:///game.html (packages/runtime-native/src/runtime.cpp:3436). Nothing else presses it, so vq-native-asset-capabilities.playtest.json runs the fixture exactly as it did before ca1bd9c, and its required capabilities are unchanged. The growth was the measurement. renderer.info.memory is three's initialised-upload tally: geometries ++ on a geometry's first draw and -- on that geometry's own dispose event (three.webgpu.js:31404,31841), and it is not reset per frame. The run's baseline read 1 geometry and 2 textures for a scene that holds six geometries (three stage cylinders, one plane, one mesh per GLB) and samples three — a value below the scene's own live set can only be a sample taken mid-upload, and every later enter's larger sample then reads as growth. The enter is now read at a tally that has held still, and the leave follows that read rather than the first measured frame, so growth is a difference between two settles and not between two arbitrary frames. Evidence, none of it a GPU run: - The engine path releases everything. packages/core/__tests__/asset-lifetime.spec.ts drives the real defineGame/ctx.goto/ctx.assets mechanism — load two models through ctx.assets, enter, leave via ctx.goto, come back — for three cycles and counts 0 live geometries and 0 live textures against a render-frame tally built the way three's backends build it. Its counter-probe, the same scene with an exit that forgets, reports 12 and 3, so the guard is known to see a leak when there is one. A scene change is scene.exit then clearScene (packages/core/src/game.ts:849,864,541), which drops references without disposing: the loader's cache is game-lifetime (game.ts:1018) and release/clear are its disposal API (packages/core/src/assets.ts:130,173). - The fixture released everything it owned (vq-assets/src/game.ts:150-165): the four stage meshes' geometry and materials, its own configured copy of the PNG, both model entries and the cached texture, with disposeModel disposing each model's geometry, materials and material textures once (assets.ts:1058). - The artefact reproduces in node. A tally still climbing through the measurement window reported geometryGrowth 5 against an unchanged settled live set; it now reports 0, and the paired case where a later enter really does leave more live still reports 6 and 3. Red first, then green, in packages/create-threenative/__tests__/vq-native-fixture.spec.ts, which imports the fixture and re-imports it per run because the enter ledger is module scope: "stays in its scene unless the lifecycle scenario asks it to leave" failed with left [void] on enter 1; "leaves and re-enters once the lifecycle scenario presses its key" failed with the leaves on the wrong enters; "reads an enter only once its tally has settled" failed with geometryGrowth 5. "still reports growth when a later enter leaves more live" passed before and after, which is what keeps the fix from being a measurement that never moves. Gates: pnpm exec vitest run over the two VQ specs, asset-lifetime, scene-warning, game, assets and native-asset-capture-proof — 47 and 108 respectively, all passed. pnpm typecheck, pnpm lint (0 errors) and pnpm build passed; pnpm check:docs passed. The scenario still loads through loadPlaytestScenario and now requires browser.input on both targets, which the Android transport advertises. Unrun: no browser or device run of the corrected scenario here, so AC-2 stays open in the PRD with the verdict and this evidence recorded against it.
The Hornbeam needed no engine fix: on the tarballs packed from this branch the default cook exits 0 in 163 s (127 MB -> 27 MB, 6 embedded PNGs to KTX2, 0 uncooked bytes) and its trunk primitive gets a 3-rung TN_discrete_lod chain with a 1,500-triangle far rung. Box 3 (browser + native scenarios) stays open, and the PRD now records that the 745k-triangle MASK foliage took TN_virtual_geometry with stalled cluster levels rather than a chain.
…D (PRD-485) The Hornbeam's two MASK foliage primitives (225,310 + 520,018 = 745,328 of 755,677 triangles) shipped with no coarse representation at all. The cluster builder simplifies every group with `LockBorder` (dag.ts:259) and every edge of a disconnected alpha-cutout card is a border, so the first fold simplifies nothing (dag.ts:461), dag.ts:319 records `stalled`, and every level reports the source count. The primitive was then `virtual-owned` (eligibility.ts:182), so AutoLOD declined it and the asset's only chain was the 10,349-triangle trunk. `bakeVirtualGeometry` now declines a primitive whose DAG stopped `stalled` while still holding more than 80% of its triangles: no extension is attached, so the primitive is unowned and the discrete ladder's card + terminal levels — the path built for exactly this shape (generate.ts:851) — take it. Measured, not assumed: a DAG that stalls after real folds has shed its triangles and keeps the primitive. Red: on a 16,384-card MASK fixture the cook emitted `TN_virtual_geometry`, `lod.reasons ["virtual-owned"]` and no chain; the report named the bake as "no primitive was dense enough". Green: no extension, `lod.reasons []`, a 3-level card chain whose far rung is 1,638 triangles = 5.0% of LOD0 with `cellCoverage 1` on every level, and the alpha cutoff the asset shipped unchanged. Sandbox proof (fab-import-proof, repacked assets tarball): build exit 0, `lod ...: 3 primitive(s) to 3 level(s), 755677 -> 38767 triangles, 0 skipped`, `TN_virtual_geometry` gone from extensionsUsed, far total 38,767 = 5.13%, cooked GLB 26,990,484 -> 26,428,684 B. Gates: packages/assets 480 passed, pnpm typecheck 0, pnpm lint 0 (1032 warnings, unchanged), pnpm check:docs 0.
Phase 3 box 3. `finding-assets.md` already said the cook picks imported GLBs up with no configuration; it now says what the cook does with them and why `models: "none"` / `textures: "none"` is the wrong repair: a converted Fab pack's embedded PNG/WebP maps become KTX2, its attributes are quantized and Meshopt-compressed, and a `MASK` primitive gets a discrete LOD chain. Turning a pass off ships every source byte verbatim and counts it all against `assets.budget.uncooked`, which is what raises TN_ASSETS_BUDGET_EXCEEDED. Measured on the Hornbeam: 126,934,280 -> 26,428,684 B, six embedded PNGs 45.4 MB -> 16.4 MB, 755,677 triangles to a 5.1% far rung. Also names the three cook outputs a content-addressed glob cannot match (`public/basis/`, `public/shared/`, `public/bake.receipt.json`), found by building metahuman-lab's 301 MB tree through the cook. The Phase 3 box 2 build leg is recorded beside its box, unticked: the cook is exit 0 with 0 uncooked bytes, and the playtest is for the browser lane. Proof: `pnpm exec vitest run scripts/__tests__/primary-docs.spec.ts` 7/7, `pnpm check:docs` 2,229 links, plus check-doc-links/sync-agent-docs/ evidence-citations 30/30. No template file changed, so no scaffold hash moved.
…ok (PRD-485) `packages/assets/src/passes/model.ts:925` called the library's `reorder` on the whole document, which renumbers the vertices of every primitive it finds. That is the cheapest transmission-size win in the cook and it is invisible until something outside the file holds a vertex index: the metahuman-lab's `brows.skin.json` names the head vertex each brow strand root rides and reads its position back every frame, and after the cook it indexed a different vertex (`brow root 0 indexes head vertex 11498, which is not where it was prepared`). Measured on the 26.3 MB Ada specimen, 87 of 24408 head-primitive slots survived the reorder. The cook cannot know who holds an index, so the rule is the broader one: **any primitive whose vertex indices can be addressed from outside is undeclarable**, and deforming — skinned, morph-targeted, or joint-bound — is the measurable proxy, because it is the one case detectable from the file alone and a rig is where index-addressed data comes from. `deforming()` in `lod/eligibility.ts` is now the single definition of that word, shared with the discrete-LOD and joined-rung rules that already refused the same geometry. `reorder-static.ts` runs the remap per shared index buffer and skips any group holding a deforming primitive. Compression is untouched: `EXT_meshopt_compression` encodes whatever order it is handed, and `quantize` renumbers nothing — `specimen.glb` cooks 16,104,464 -> 16,114,804 B (+0.06%), the price of leaving the head's 24408 vertices where the sidecar expects them. `MODEL_REORDER_VERSION` joins the pass configuration, or a warm compile entry would keep serving the renumbered files this replaces. Red/green in `packages/assets/__tests__/model-pass.spec.ts`: a skinned primitive and a morph-targeted one come out of the cook with position k exactly where it started and the same vertex count, while a static mesh in the same document is still reordered (and still only reordered). `packages/assets/__tests__` 481 passed, `pnpm typecheck` and `pnpm lint` clean.
…through the cook (PRD-485) `packages/assets/src/passes/model.ts:964` handed the whole document to the library's `quantize`, which rewrites POSITION into normalized int16 snapped to a grid whose step is the mesh's own bounding volume and then folds that shift into the node holding the mesh, the skin's inverse bind matrices, GPU instancing batches and any volumetric material. Exact for a static mesh; for a rigged one it breaks the invariant this repo holds, one metre is one metre. The metahuman-lab's `brows.skin.json` names the head vertex each brow strand root rides and reads its position back every frame, and the sidecar was prepared against the source file to 1e-5 m — the 16-bit grid over a head-sized volume is micrometres away from every vertex it recorded, and the shift that would undo it lives in a node transform the sidecar never sees. So the same `deforming()` rule that stopped the cook renumbering a rigged vertex now stops it quantizing one: a deforming mesh keeps POSITION and its morph-target POSITION deltas as the float32 values the author wrote, at every requested depth. `quantize-static.ts` holds those meshes across the stage and spends the window with each mesh detached from its nodes, so the library's node, skin, instancing and thickness compensation is never applied and there is nothing to undo; normals, UVs, colours and every static primitive still quantize, and `EXT_meshopt_compression` still encodes whatever the accessors hold (its `QUANTIZE` encoder method only names the pre-processing, which is this stage). The unit of source space is the mesh, because the compensation is per mesh — one deforming primitive holds the whole thing. An index buffer the stage replaced fails the cook by name rather than shipping a mesh whose vertices have quietly moved, and `snapPositions` skips rigged meshes for the same reason it exists to snap them. `MODEL_QUANTIZE_VERSION` joins the pass configuration so a warm compile entry cannot keep serving the quantized files this replaces. Red/green in `packages/assets/__tests__/model-pass.spec.ts`: a skinned primitive that also carries a morph target comes out of the cook with float32 POSITION equal to the source within 1e-6, float32 deltas, and its skin's inverse bind matrices untouched, while a static mesh in the same document still quantizes to normalized int16 under KHR_mesh_quantization. The 4-bit floor test now states what is true of it: that request used to land on the fixture's skinned head and trip the self-verify at 0.167% drift; it no longer reaches deforming geometry at all, and the fail-closed comparison is covered directly by the drift-kind test. `packages/assets/ __tests__` 482 passed | 2 skipped, `pnpm typecheck` clean, biome clean on the four files.
1 of 4 tasks
jonit-dev
marked this pull request as ready for review
October 4, 2026 06:37
jonit-dev
enabled auto-merge
October 4, 2026 06:37
The leak guard requires every test-owned temporary directory to go through test-support/temp-dir, and this spec called mkdtemp directly with its own roots array. Use the helper, which registers the cleanup the guard reads.
github-merge-queue
Bot
removed this pull request from the merge queue due to a conflict with the base branch
Oct 4, 2026
Resolve the integration workflow conflict keeping both intents: - paths: keep the PR's native-assets filter row and develop's added `exposure` alternation in the tone render path filter. - jobs: keep the PR's packaged-quickjs native asset fallback job and develop's exposure and cold-boot auto-exposure jobs. No develop job removed.
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.
Consolidates native asset capability work (#396 / VQ-01) and the high-quality default asset cook (#425 / PRD-485) into one coherent CI board. Exact published source heads are preserved through ordinary merge ancestry; decoder capability/cache identity handling and source-unit/static-only quantization/reordering safeguards remain intact.
Validation: independent source review, 140 focused asset cook/decoder/clustered-mesh/native-proof CPU tests across nine suites, whole-workspace typecheck, declared JavaScript builds, normal lint/docs/instruction/scaffold drift checks, and unchanged repository budgets. Exact-head hosted CI remains required after publication.
Original acceptance is preserved: the recorded native GPU lifecycle growth and remaining Android/cook/game scenarios are unresolved. This consolidation does not certify those criteria or change their checkboxes. Original owner checkouts were untouched; the clean latest #425 head was recorded in a stable task-owned manifest. Source branches remain, and source PR closure requires published ancestry verification and a cross-link.
Original #396 scope and checklist — `4949c2530cc3df939e75d2f8545af24699bea5df`
Actual packaged native screenshot
Original 960×640 native framebuffer from successful run 36998104447, source
46a759ad885cede29e0fbb2c719ed75151c5baec. The Meshopt/Draco sources were normalized by the ordinary decoder-free cook before execution. This does not claim compressed-codec support in QuickJS.The native mailbox captured the actual packaged executable via
device.screenshoton named Mesa llvmpipe (LLVM 20.1.2, 128 bits). Eight live assertions passed, startup reached ready with compilation settled, strict native host-console checks passed and report diagnostics are empty. The PNG was visually inspected, preserved unchanged, and its immutable public URL verified HTTP 200 with SHA-2561ad43cda8e816431b0ab9993066b087c9283351f304ade2c1765eebf8e349c81(71,626 bytes).Proof scope and provenance · Machine-readable provenance · Unchanged runner summary
Implementation
Verification
Still partial, remains draft
The screenshot request is satisfied for the bounded Linux QuickJS decoder-free fallback. Full VQ-01 remains open: Android/iOS selected artifact/cohort metadata, actual decoder versions and block formats, manifest-versus-payload checks, native compressed-codec admission, peak load memory and full repeated-scene lifecycle.
The diagnostics assertion family is web-only. The native fixture uses supported startup/resource assertions and mandatory verifier checks of complete native host-console evidence, readiness, native adapter/capture provenance and opaque specimen pixels. No engine target guards changed. This is hosted software-rendering correctness, not hardware performance.
Root tests remain locally blocked by tsx IPC EPERM; root typecheck previously exited 137; two broader Android consumer checks require missing jar and reproduce unchanged on baseline. Main full CI is draft-skipped. No aggregate board green or PRD completion is claimed. The raw summary's decodedGeometryBytes counts all decoded glTF accessors including animation; it is CPU accessor bytes, not measured GPU allocation or a claimed compression saving.
Canonical checklist (prd:0%)
pnpm exec vitest run packages/create-threenative/__tests__/vq-native-asset-capabilities.spec.ts.pnpm exec vitest run packages/create-threenative/__tests__/vq-native-asset-capabilities.spec.ts.pnpm exec vitest run packages/create-threenative/__tests__/vq-native-asset-capabilities.spec.ts.pnpm exec vitest run packages/create-threenative/__tests__/vq-native-asset-capabilities.spec.ts packages/create-threenative/__tests__/vq-native-fixture.spec.ts.node packages/playtest/dist/runner/cli.js examples/abyss-framework/playtests/vq-native-asset-capabilities.playtest.json --url ${VQ_URL:?} --browser-recipe webgpu.node packages/playtest/dist/runner/cli.js examples/abyss-framework/playtests/vq-native-asset-capabilities.playtest.json --target android --device ${VQ_ANDROID_DEVICE:?}.pnpm exec vitest run packages/create-threenative/__tests__/vq-native-asset-capabilities.spec.tsplus the Phase 3 lifecycle scenario.Canonical PRD: docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-01-native-asset-capabilities.md. One implementation PR for this PRD, from planning #391. Keep draft until remaining qualification and required checks are satisfied.
Original #425 scope and checklist — `7876e4b7bc0caf853650965ed3b0513deb877506`
TL;DR — runbook row B2 (PRD-485): Fab/Megascans-quality assets go through the default cook instead of around it. Phase 1 box 1 is in: an embedded
EXT_texture_webpimage now cooks like PNG — to KTX2 on a decoding target, to a capped PNG on the decoder-free path — and the extension leaves the output. That removes the WebP reasonlumen-hallgave formodels: "none".PRD:
docs/PRDs/open-world/PRD-485-high-quality-assets-go-through-the-cook.md.Done
decodeImageBytesdecodes WebP withsharp(already declared inpackages/assets, previously unused; loads in the pass worker, stays external indist, no new binary for games —@gltf-transform/clialready brings it).EXT_texture_webpdisposed once no WebP image remains. Spec red → green 3/3.Missing
lumen-hallweb build withoutmodels: "none"+ its asset scenario (sandbox repo, GPU run).pnpm visuals:ab --raters 3once a template-visible change lands.Evidence
packages/assets474 passed / 2 skipped;pnpm typecheck,pnpm lint, assets build + publint green.