From d134451eec23657ad146d2d9c88a6529cd27e118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Furtado?= <30300459+jonit-dev@users.noreply.github.com> Date: Thu, 1 Oct 2026 23:59:56 -0700 Subject: [PATCH 01/12] fix(shooter): release owned decal materials on teardown --- .../PRD-VQ-11-bounded-decals.md | 75 +++++++++++ .../__tests__/shooter-decals-lifetime.spec.ts | 116 ++++++++++++++++++ .../templates/shooter/src/render/decals.ts | 16 ++- 3 files changed, 202 insertions(+), 5 deletions(-) create mode 100644 docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md create mode 100644 packages/create-threenative/__tests__/shooter-decals-lifetime.spec.ts diff --git a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md new file mode 100644 index 0000000000..d0a646f941 --- /dev/null +++ b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md @@ -0,0 +1,75 @@ +--- +prd_contract: v1 +--- + +# PRD-VQ-11 — Impact and environment decals use a bounded portable lifecycle + +**Status:** PARTIAL — 2026-10-02. The existing shooter decal field's owned-material teardown is repaired; the full receiver/projection feature and platform qualification remain open. +**Batch:** [Visual quality execution batch](https://github.com/ThreeNativeHQ/threenative/blob/docs/visual-quality-batch-2026-10-01/docs/PRDs/batch-2026-10-01-visual-quality/README.md). **Wave:** 2 / high-payoff detail. +**Dependencies:** Independent. Share receiver/base-geometry selection with existing LOD and picking paths. + +## Grounding and intended outcome + +Upstream [DecalGeometry](https://threejs.org/docs/pages/DecalGeometry.html) already provides mesh projection. The earlier audit must not be interpreted as permission to rewrite that algorithm. The useful product gap is qualified receiver handling, pooling, lifetime and game-owned material composition for real impacts. + +**Outcome:** Repeated bullet impacts and environment marks remain attached to their receivers, respect normals and occlusion, and stay within a fixed resource cap as the player moves or the scene unloads. + +## Design and ownership + +Start from ordinary Three decal geometry and generated materials. Reuse existing hit results, object transforms and authored LOD0 geometry rather than raycasting the whole world again. Admit a shared lifecycle owner only when at least two consumers need it. Keep material, atlas choice, color, depth bias and fade curves in generated source. A world-static mark and a rigid-moving receiver are separate supported cases; skinned or deforming decals are explicitly refused in v1. + +Surface-color impacts and environment marks on static/rigid opaque receivers. No deferred G-buffer decal renderer, normal/roughness channel rewrite, skinned tattoo system or virtual-texture painting. + +## Required behavior + +- A cap of 256 decals in the fixture is enforced with deterministic eviction, and receiver teardown removes its marks. +- A mark stays on a rigid receiver under translation/rotation; nonuniform or unsupported transforms have a specified behavior. +- Receiver isolation avoids marks projecting through nearby unrelated geometry; corner distortion is documented. +- LOD changes do not alter the gameplay hit surface or strand a decal on disposed geometry. + +## Execution phases + +All proof paths below are **planned implementation targets**, not existing passing tests unless a result is explicitly recorded. Reuse an existing equivalent test or scenario after inspecting current code, and update the canonical proof path rather than adding a duplicate. Follow [EXECUTE.md](https://github.com/ThreeNativeHQ/threenative/blob/docs/visual-quality-batch-2026-10-01/docs/PRDs/batch-2026-10-01-visual-quality/EXECUTE.md) for fixture setup, variables, review and repository gates. + +### Phase 1 — Reuse projection and bound lifetime + +- [ ] Integrate upstream decal projection with existing picking/base-geometry data and explicit receiver support. proof: `pnpm exec vitest run packages/core/__tests__/vq-bounded-decals.spec.ts`. +- [ ] Implement deterministic pool limits, eviction, receiver cleanup and borrowed-resource ownership. proof: `pnpm exec vitest run packages/core/__tests__/vq-bounded-decals.spec.ts`. + - Partial, 2026-10-02: the existing generated shooter `DecalField` now releases each cloned slot material, family material and shared geometry once; its texture remains borrowed. `node node_modules/vitest/vitest.mjs run packages/create-threenative/__tests__/shooter-decals-lifetime.spec.ts` passed 4 tests after reproducing 4 failures on the original implementation. This repairs the existing 224-slot template; it does not implement the proposed 256-decal receiver/projection fixture or introduce an engine pool. + +### Phase 2 — Preserve authored appearance and receiver motion + +- [ ] Add game-owned decal material/atlas composition with bounded bias and fade controls. proof: `pnpm exec vitest run packages/core/__tests__/vq-bounded-decals.spec.ts`. +- [ ] Verify rigid receiver motion, LOD changes and repeated scene teardown without detached marks or allocation growth. proof: `pnpm exec vitest run packages/core/__tests__/vq-bounded-decals.spec.ts`. + - Partial, 2026-10-02: the lifetime regression exercises 20 field construction/teardown cycles, 2,000 pooled placements, clear/reuse, zero-slot teardown and repeated `dispose()`. Real Three disposal events/spies verify owned-resource release and retention of the borrowed map. Rigid receivers, LOD changes, GPU memory and game-scene transitions are not qualified by these unit tests. + +### Phase 3 — Qualify the visible result + +- [ ] The impact sequence covers the 256-mark cap, moving receiver and LOD boundary on browser WebGPU. proof: `node packages/playtest/dist/runner/cli.js examples/abyss-framework/playtests/vq-bounded-decals.playtest.json --url ${VQ_URL:?} --browser-recipe webgpu`. +- [ ] The same sequence runs on Linux native with matching pool/receiver observations and visible marks. proof: `node packages/playtest/dist/runner/cli.js examples/abyss-framework/playtests/vq-bounded-decals.playtest.json --target desktop --executable ${VQ_NATIVE_EXECUTABLE:?}`. + +## Acceptance criteria + +- [ ] The supported receiver cases retain their marks under motion and teardown, and resource use is bounded independently of total shots fired. proof: the completed Phase 3 scenario outputs, with the named fixture, package/cohort and adapter recorded inline here. +- [ ] The feature preserves its documented inactive/fallback path and releases owned resources after repeated lifecycle transitions. proof: `pnpm exec vitest run packages/core/__tests__/vq-bounded-decals.spec.ts` plus the Phase 3 lifecycle scenario. + +## Performance and promotion + +Measure draw calls and geometry bytes at the cap. Batch only compatible marks when it improves the measured cost; do not trade correctness for an advertised one-draw system. Numerical targets are proposed acceptance targets, not benchmark results. Pin the fixture, metrics and thresholds before changing the implementation; never relax a failing threshold just to mark this PRD complete. Shared abstractions must pass the repository's reuse/LOC admission rule. Appearance remains editable source. + +## Blocked on + +Skinned/deforming receivers require a separate demand-backed extension and are not silently treated as rigid meshes. +Browser/native visual qualification needs a runnable fixture and GPU/runtime lane. The current container has no `/dev/dri` or `/dev/kvm`; `node packages/playtest/dist/runner/cli.js doctor --text` exits 2 because `adb` is absent. These limits do not block the still-unimplemented CPU-testable projection, receiver and LOD work. + +## Completion record + +Update the phase boxes and this PRD only after the named proof runs. Record actual results inline, including any remaining exclusions. A merged planning or implementation PR alone is not proof that every acceptance criterion passed. Archive according to the parent PRD filing rules when the work is genuinely complete. + +2026-10-02 bounded ownership repair: + +- Red/green: the final four-test lifetime regression failed all four cases on the original `decals.ts` and passed all four with the repair. No Three disposal implementation is replaced; listeners and call-through spies observe the real methods. +- `node node_modules/vitest/vitest.mjs run --maxWorkers 1 packages/create-threenative/__tests__/shooter-decals-lifetime.spec.ts packages/create-threenative/__tests__/world-environment-lifetime.spec.ts packages/create-threenative/__tests__/shooter-rig.spec.ts packages/create-threenative/__tests__/template-runtime-cost.spec.ts`: 4 files, 13 tests passed after building local package prerequisites. Existing Rapier/Three warnings were emitted. +- `pnpm typecheck` and `pnpm --filter create-threenative build`: exit 0 after building package prerequisites. `pnpm lint`: exit 0 with 1,000 existing warnings. `node --import tsx scripts/check-doc-links.ts`: exit 0, 2,395 links checked. The doc-link/evidence-budget/evidence-citations suites passed 31 tests using the `node --import tsx` loader in place of the CLI's unavailable IPC socket. +- Full `pnpm test` was attempted and stopped at the `tsx` IPC socket with `EPERM`; its loader-based retry was interrupted during aggregate builds to avoid running the batch's full suite concurrently. This is not a full-suite pass. No browser, native, GPU-allocation or visible-result claim is made. +- All six phase boxes and both acceptance boxes remain open. The proposed 256-mark projection fixture, receiver motion/removal, LOD handling, appearance controls and platform scenarios are still to be implemented or qualified. diff --git a/packages/create-threenative/__tests__/shooter-decals-lifetime.spec.ts b/packages/create-threenative/__tests__/shooter-decals-lifetime.spec.ts new file mode 100644 index 0000000000..f5a0f1000e --- /dev/null +++ b/packages/create-threenative/__tests__/shooter-decals-lifetime.spec.ts @@ -0,0 +1,116 @@ +import { + Group, + Material, + Mesh, + type MeshBasicMaterial, + PlaneGeometry, + Texture, + Vector3, +} from "three"; +import { afterEach, expect, it, vi } from "vitest"; +import { Registry } from "../../core/src/entities.js"; +import { DecalField } from "../templates/shooter/src/render/decals.js"; + +afterEach(() => vi.restoreAllMocks()); + +it("releases every owned slot material while retaining the borrowed map", () => { + const parent = new Group(); + const unrelated = new Group(); + parent.add(unrelated); + const map = new Texture(); + const mapDisposed = vi.fn(); + map.addEventListener("dispose", mapDisposed); + const materialDisposed = vi.spyOn(Material.prototype, "dispose"); + const field = new DecalField(parent, { + countPerVariant: 3, + map, + size: 0.13, + tints: { steel: 0xb6bcc4, wood: 0x8a6a44 }, + }); + const slots = parent.children.filter( + (child): child is Mesh => child instanceof Mesh, + ); + expect(slots).toHaveLength(6); + expect(new Set(slots.map((slot) => slot.material)).size).toBe(6); + const releases = slots.map((slot) => { + expect(slot.material.map).toBe(map); + const released = vi.fn(); + slot.material.addEventListener("dispose", released); + return released; + }); + const geometryDisposed = vi.fn(); + slots[0]?.geometry.addEventListener("dispose", geometryDisposed); + + field.place(new Vector3(1, 2, 3), new Vector3(0, 1, 0), "steel"); + field.dispose(); + + for (const released of releases) expect(released).toHaveBeenCalledTimes(1); + // Six cloned slot materials and the two family source materials are owned by the field. + expect(materialDisposed).toHaveBeenCalledTimes(8); + expect(geometryDisposed).toHaveBeenCalledTimes(1); + expect(parent.children).toEqual([unrelated]); + expect(mapDisposed).not.toHaveBeenCalled(); + map.dispose(); + expect(mapDisposed).toHaveBeenCalledTimes(1); +}); + +it.each([0, 3])("tears down only once with %i slots per variant", (countPerVariant) => { + const parent = new Group(); + const map = new Texture(); + const materialDisposed = vi.spyOn(Material.prototype, "dispose"); + const geometryDisposed = vi.spyOn(PlaneGeometry.prototype, "dispose"); + const field = new DecalField(parent, { + countPerVariant, + map, + size: 0.13, + tints: { steel: 0xb6bcc4 }, + }); + + field.dispose(); + field.dispose(); + + expect(materialDisposed).toHaveBeenCalledTimes(countPerVariant + 1); + expect(geometryDisposed).toHaveBeenCalledTimes(1); + expect(parent.children).toHaveLength(0); + map.dispose(); +}); + +it("releases resources after repeated pool reuse, clear and registry teardown", () => { + const map = new Texture(); + const mapDisposed = vi.fn(); + map.addEventListener("dispose", mapDisposed); + const materialDisposed = vi.spyOn(Material.prototype, "dispose"); + const geometryDisposed = vi.spyOn(PlaneGeometry.prototype, "dispose"); + const point = new Vector3(1, 2, 3); + const normal = new Vector3(0, 1, 0); + + for (let cycle = 0; cycle < 20; cycle += 1) { + const parent = new Group(); + const registry = new Registry(); + const field = new DecalField(parent, { + countPerVariant: 4, + map, + size: 0.13, + tints: { steel: 0xb6bcc4, wood: 0x8a6a44 }, + }); + const slots = [...parent.children]; + registry.add("decals", field); + field.settle(); + for (let hit = 0; hit < 100; hit += 1) { + field.place(point, normal, hit % 2 === 0 ? "steel" : "wood"); + } + field.clear(); + expect(parent.children).toEqual(slots); + expect(parent.children).toHaveLength(field.capacity); + expect(materialDisposed).toHaveBeenCalledTimes(cycle * 10); + expect(geometryDisposed).toHaveBeenCalledTimes(cycle); + field.place(point, normal, "wood"); + registry.clear(); + field.dispose(); + expect(parent.children).toHaveLength(0); + expect(materialDisposed).toHaveBeenCalledTimes((cycle + 1) * 10); + expect(geometryDisposed).toHaveBeenCalledTimes(cycle + 1); + expect(mapDisposed).not.toHaveBeenCalled(); + } + map.dispose(); +}); diff --git a/packages/create-threenative/templates/shooter/src/render/decals.ts b/packages/create-threenative/templates/shooter/src/render/decals.ts index 66c7c20657..fe747db62f 100644 --- a/packages/create-threenative/templates/shooter/src/render/decals.ts +++ b/packages/create-threenative/templates/shooter/src/render/decals.ts @@ -1,7 +1,6 @@ import { DataTexture, DoubleSide, - type Material, Mesh, MeshBasicMaterial, type Object3D, @@ -127,7 +126,7 @@ const scratchNormal = new Vector3(); /** One family of marks: its own material, its own slots, its own recycle cursor. */ type Variant = { - readonly slots: Mesh[]; + readonly slots: Mesh[]; readonly material: MeshBasicMaterial; cursor: number; }; @@ -138,6 +137,7 @@ export class DecalField { readonly #offset: number; #placed = 0; #capacity = 0; + #disposed = false; /** Slots stop being submitted once their pipeline exists. See `settle`. */ #settled = false; @@ -148,6 +148,7 @@ export class DecalField { countPerVariant: number; /** Edge length of one mark in metres, before the per-place scale multiplier. */ size: number; + /** Borrowed: the caller retains ownership and disposes this after its last user. */ map: Texture; /** Tint per variant: a hole in steel keeps a colder rim than one in wood. */ tints: Readonly>; @@ -168,7 +169,7 @@ export class DecalField { side: DoubleSide, transparent: true, }); - const slots: Mesh[] = []; + const slots: Mesh[] = []; for (let index = 0; index < options.countPerVariant; index += 1) { const mesh = new Mesh(this.#geometry, material.clone()); // Present from the first frame at a size nothing can see, so this material's pipeline is @@ -261,9 +262,14 @@ export class DecalField { } dispose(): void { + if (this.#disposed) return; + this.#disposed = true; for (const family of this.#variants.values()) { - for (const mesh of family.slots) mesh.removeFromParent(); - (family.material as Material).dispose(); + for (const mesh of family.slots) { + mesh.removeFromParent(); + mesh.material.dispose(); + } + family.material.dispose(); } this.#variants.clear(); this.#geometry.dispose(); From 1a4ad679a7edaf6acd365b6896eb8a9676f7ce01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Furtado?= <30300459+jonit-dev@users.noreply.github.com> Date: Fri, 2 Oct 2026 01:39:10 -0700 Subject: [PATCH 02/12] fix(shooter): release scene-owned decal texture --- .../PRD-VQ-11-bounded-decals.md | 10 ++- .../__tests__/template-runtime-cost.spec.ts | 61 +++++++++++++++++++ .../templates/shooter/src/scenes/Play.ts | 7 ++- 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md index d0a646f941..ed1997aee5 100644 --- a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md +++ b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md @@ -36,6 +36,7 @@ All proof paths below are **planned implementation targets**, not existing passi - [ ] Integrate upstream decal projection with existing picking/base-geometry data and explicit receiver support. proof: `pnpm exec vitest run packages/core/__tests__/vq-bounded-decals.spec.ts`. - [ ] Implement deterministic pool limits, eviction, receiver cleanup and borrowed-resource ownership. proof: `pnpm exec vitest run packages/core/__tests__/vq-bounded-decals.spec.ts`. - Partial, 2026-10-02: the existing generated shooter `DecalField` now releases each cloned slot material, family material and shared geometry once; its texture remains borrowed. `node node_modules/vitest/vitest.mjs run packages/create-threenative/__tests__/shooter-decals-lifetime.spec.ts` passed 4 tests after reproducing 4 failures on the original implementation. This repairs the existing 224-slot template; it does not implement the proposed 256-decal receiver/projection fixture or introduce an engine pool. + - Caller ownership follow-up, 2026-10-02: `Play` registers its procedural decal texture in the existing scene registry after the borrowing field. The real `Play.load`/`enter` and registry teardown regression proves all 224 material disposals precede one texture disposal on each of three scene lifecycles; repeated cleanup does not redispose it, and the borrowed sky texture is retained. proof: `node node_modules/vitest/vitest.mjs run --maxWorkers 1 packages/create-threenative/__tests__/template-runtime-cost.spec.ts -t 'releases the shooter-owned decal texture'` — missing-disposal failure before the fix, pass afterward. ### Phase 2 — Preserve authored appearance and receiver motion @@ -66,10 +67,17 @@ Browser/native visual qualification needs a runnable fixture and GPU/runtime lan Update the phase boxes and this PRD only after the named proof runs. Record actual results inline, including any remaining exclusions. A merged planning or implementation PR alone is not proof that every acceptance criterion passed. Archive according to the parent PRD filing rules when the work is genuinely complete. -2026-10-02 bounded ownership repair: +2026-10-02 initial bounded ownership repair at `d134451e` (historical results from the prior environment; not a fresh aggregate pass for later changes): - Red/green: the final four-test lifetime regression failed all four cases on the original `decals.ts` and passed all four with the repair. No Three disposal implementation is replaced; listeners and call-through spies observe the real methods. - `node node_modules/vitest/vitest.mjs run --maxWorkers 1 packages/create-threenative/__tests__/shooter-decals-lifetime.spec.ts packages/create-threenative/__tests__/world-environment-lifetime.spec.ts packages/create-threenative/__tests__/shooter-rig.spec.ts packages/create-threenative/__tests__/template-runtime-cost.spec.ts`: 4 files, 13 tests passed after building local package prerequisites. Existing Rapier/Three warnings were emitted. - `pnpm typecheck` and `pnpm --filter create-threenative build`: exit 0 after building package prerequisites. `pnpm lint`: exit 0 with 1,000 existing warnings. `node --import tsx scripts/check-doc-links.ts`: exit 0, 2,395 links checked. The doc-link/evidence-budget/evidence-citations suites passed 31 tests using the `node --import tsx` loader in place of the CLI's unavailable IPC socket. - Full `pnpm test` was attempted and stopped at the `tsx` IPC socket with `EPERM`; its loader-based retry was interrupted during aggregate builds to avoid running the batch's full suite concurrently. This is not a full-suite pass. No browser, native, GPU-allocation or visible-result claim is made. - All six phase boxes and both acceptance boxes remain open. The proposed 256-mark projection fixture, receiver motion/removal, LOD handling, appearance controls and platform scenarios are still to be implemented or qualified. + +2026-10-02 caller texture ownership follow-up, verified in the recovered environment: + +- Fresh red/green on the actual shooter caller: its generated map emitted zero disposal events after scene/registry teardown before the fix. After the fix, the new regression passes through three scene construction/teardown cycles, checks borrower-before-texture disposal ordering and idempotent cleanup, and leaves the borrowed sky map untouched. +- The same four-file focused/adjacent command above passes 14/14 tests with `--maxWorkers 1`, including the original four field-ownership regressions. Fixture Rapier/Three warnings remain visible; no real disposal implementation is replaced. +- `node node_modules/typescript/bin/tsc --noEmit -p packages/create-threenative/tsconfig.json`: exit 0. Biome check on the changed caller and test: exit 0 with six existing complexity warnings. `node --import tsx scripts/check-doc-links.ts`: exit 0, 2,395 links checked. +- This follow-up does not rerun aggregate root build/typecheck/test lanes while parallel rendering work is active. Browser/native qualification, actual relevant runtime screenshot proof, required CI and independent review remain open; the PR stays draft. Projection/receiver feature expansion is not included in this ownership repair. diff --git a/packages/create-threenative/__tests__/template-runtime-cost.spec.ts b/packages/create-threenative/__tests__/template-runtime-cost.spec.ts index 32111fb354..dc9922414f 100644 --- a/packages/create-threenative/__tests__/template-runtime-cost.spec.ts +++ b/packages/create-threenative/__tests__/template-runtime-cost.spec.ts @@ -7,12 +7,14 @@ import { MeshBasicMaterial, OrthographicCamera, PerspectiveCamera, + PlaneGeometry, Scene, Texture, Vector2, Vector3, } from "three"; import { describe, expect, it, vi } from "vitest"; +import { Registry } from "../../core/src/entities.js"; import { createRandom } from "../../core/src/random.js"; import { rapier } from "../../physics/src/index.js"; import type { IPhysicsContext } from "../../physics/src/plugin.js"; @@ -276,6 +278,65 @@ function runSceneFrames(frame: unknown, context: unknown, beforeMeasure?: () => } describe("generated template ordinary-frame runtime cost", () => { + it("releases the shooter-owned decal texture after its borrowers on repeated scene teardown", async () => { + const { Play } = await import("../templates/shooter/src/scenes/Play.js"); + const { ENEMY_CLIPS } = await import("../templates/shooter/src/entities/Enemy.js"); + const maps = new Set(); + for (let cycle = 0; cycle < 3; cycle += 1) { + const physics = await physicsFixture(); + const entities = new Registry(); + const context = { ...sceneContext(physics.physics, Play.initialState), entities }; + const sky = new Texture(); + const skyDisposed = vi.fn(); + sky.addEventListener("dispose", skyDisposed); + const play = new Play(); + try { + await play.load({ + ...context, + assets: { + ...context.assets, + model: async (): Promise => templatedRig(ENEMY_CLIPS) as T, + texture: async () => sky, + }, + } as never); + play.enter(context as never); + const slots = context.scene.children.filter( + (child): child is Mesh => + child instanceof Mesh && + child.geometry instanceof PlaneGeometry && + child.renderOrder === 23, + ); + expect(slots).toHaveLength(224); + const map = slots[0]?.material.map; + if (map === undefined || map === null) throw new Error("Shooter decal texture missing."); + expect(new Set(slots.map((slot) => slot.material.map))).toEqual(new Set([map])); + expect(maps.has(map)).toBe(false); + maps.add(map); + let releasedMaterials = 0; + for (const slot of slots) { + slot.material.addEventListener("dispose", () => { + releasedMaterials += 1; + }); + } + const mapDisposed = vi.fn(() => expect(releasedMaterials).toBe(slots.length)); + map.addEventListener("dispose", mapDisposed); + + play.exit(); + entities.clear(); + expect(mapDisposed).toHaveBeenCalledTimes(1); + play.exit(); + entities.clear(); + expect(mapDisposed).toHaveBeenCalledTimes(1); + expect(skyDisposed).not.toHaveBeenCalled(); + } finally { + play.exit(); + entities.clear(); + physics.dispose(); + sky.dispose(); + } + } + }); + it("executes 600 steady frames per template without fresh vector work", async () => { const minimal = await import("../templates/minimal/src/render/camera.js"); const starter = await import("../templates/starter/src/entities/Player.js"); diff --git a/packages/create-threenative/templates/shooter/src/scenes/Play.ts b/packages/create-threenative/templates/shooter/src/scenes/Play.ts index fba8996fce..52d23df1bf 100644 --- a/packages/create-threenative/templates/shooter/src/scenes/Play.ts +++ b/packages/create-threenative/templates/shooter/src/scenes/Play.ts @@ -392,9 +392,10 @@ export class Play extends Scene { // Bullet holes. They stay put: a mark that fades tells the player their rounds went nowhere. // What colour the crushed rim comes out is per material — steel burns bare and cold, plaster // and stone go pale, wood darkens. See `decals.ts` for why the pool is shaped this way. + const decalTexture = bulletHoleTexture(); const decals = new DecalField(ctx.scene, { countPerVariant: 56, - map: bulletHoleTexture(), + map: decalTexture, size: 0.13, tints: { plaster: 0xf3ead8, @@ -404,7 +405,11 @@ export class Play extends Scene { }, }); ctx.entities.remove("decals"); + ctx.entities.remove("decal-texture"); ctx.entities.add("decals", decals); + // The field borrows its map. Register the scene-owned texture after its borrowers so + // registry teardown releases their materials first, then the texture, exactly once. + ctx.entities.add("decal-texture", decalTexture); // `hit.face.normal` is object-local; transform it into world space before any // spawn math, or rotated meshes send their bursts into the wall. const impactNormal = new Vector3(); From 7396738b33a2b3018a2e6235a0e59ca07620a324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Furtado?= <30300459+jonit-dev@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:21:12 -0700 Subject: [PATCH 03/12] feat(shooter): add opt-in bounded receiver decal projection --- .github/workflows/integration-decals.yml | 45 +++ .../PRD-VQ-11-bounded-decals.md | 33 +- .../__tests__/fixtures/bounded-decals/game.ts | 203 +++++++++++ .../fixtures/bounded-decals/index.html | 2 + .../__tests__/fixtures/bounded-decals/main.ts | 12 + .../bounded-decals/motion.playtest.json | 95 +++++ .../fixtures/bounded-decals/native.ts | 2 + .../public/assets.manifest.json | 14 + .../bounded-decals/public/receiver.glb | Bin 0 -> 4712 bytes .../bounded-decals/restart.playtest.json | 83 +++++ .../bounded-decals/static.playtest.json | 71 ++++ .../bounded-decals/teardown.playtest.json | 89 +++++ .../fixtures/bounded-decals/verify.ts | 105 ++++++ .../__tests__/shooter-decals-fixture.spec.ts | 87 +++++ .../shooter-decals-projection.spec.ts | 328 ++++++++++++++++++ .../templates/shooter/src/render/decals.ts | 228 +++++++++++- 16 files changed, 1387 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/integration-decals.yml create mode 100644 packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts create mode 100644 packages/create-threenative/__tests__/fixtures/bounded-decals/index.html create mode 100644 packages/create-threenative/__tests__/fixtures/bounded-decals/main.ts create mode 100644 packages/create-threenative/__tests__/fixtures/bounded-decals/motion.playtest.json create mode 100644 packages/create-threenative/__tests__/fixtures/bounded-decals/native.ts create mode 100644 packages/create-threenative/__tests__/fixtures/bounded-decals/public/assets.manifest.json create mode 100644 packages/create-threenative/__tests__/fixtures/bounded-decals/public/receiver.glb create mode 100644 packages/create-threenative/__tests__/fixtures/bounded-decals/restart.playtest.json create mode 100644 packages/create-threenative/__tests__/fixtures/bounded-decals/static.playtest.json create mode 100644 packages/create-threenative/__tests__/fixtures/bounded-decals/teardown.playtest.json create mode 100644 packages/create-threenative/__tests__/fixtures/bounded-decals/verify.ts create mode 100644 packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts create mode 100644 packages/create-threenative/__tests__/shooter-decals-projection.spec.ts diff --git a/.github/workflows/integration-decals.yml b/.github/workflows/integration-decals.yml new file mode 100644 index 0000000000..f2f3011bc1 --- /dev/null +++ b/.github/workflows/integration-decals.yml @@ -0,0 +1,45 @@ +name: Integration bounded decals +on: + pull_request: + paths: + - 'packages/create-threenative/templates/shooter/src/render/decals.ts' + - 'packages/create-threenative/templates/shooter/src/scenes/Play.ts' + - 'packages/create-threenative/__tests__/shooter-decals*.spec.ts' + - 'packages/create-threenative/__tests__/fixtures/bounded-decals/**' + - '.github/workflows/integration-decals.yml' + push: + branches: ['fix/vq11-decal-material-lifetime'] +permissions: + contents: read +concurrency: + group: integration-decals-${{ github.event.pull_request.head.ref || github.ref_name }} + cancel-in-progress: true +jobs: + decals: + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: ./.github/actions/pnpm + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: '22' + cache: pnpm + - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/workspace-dist + with: + pack-archives: 'false' + save-bundles: 'false' + - uses: ./.github/actions/playwright-chromium + - run: pnpm exec vitest run --maxWorkers=1 packages/create-threenative/__tests__/shooter-decals-projection.spec.ts packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts packages/create-threenative/__tests__/shooter-decals-lifetime.spec.ts packages/create-threenative/__tests__/template-runtime-cost.spec.ts + - name: Capture actual receiver projection, motion, removal and restart + run: node --import tsx packages/create-threenative/__tests__/fixtures/bounded-decals/verify.ts + - uses: actions/upload-artifact@v4 + if: always() + with: + name: vq11-decals-${{ github.event.pull_request.head.sha || github.sha }} + path: artifacts/vq11-decals + if-no-files-found: error diff --git a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md index ed1997aee5..251da973a1 100644 --- a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md +++ b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md @@ -4,7 +4,7 @@ prd_contract: v1 # PRD-VQ-11 — Impact and environment decals use a bounded portable lifecycle -**Status:** PARTIAL — 2026-10-02. The existing shooter decal field's owned-material teardown is repaired; the full receiver/projection feature and platform qualification remain open. +**Status:** PARTIAL — 2026-10-02. Owned-resource teardown and opt-in bounded receiver projection are CPU-verified. Authored fade/atlas qualification and browser/native visible-result gates remain open. **Batch:** [Visual quality execution batch](https://github.com/ThreeNativeHQ/threenative/blob/docs/visual-quality-batch-2026-10-01/docs/PRDs/batch-2026-10-01-visual-quality/README.md). **Wave:** 2 / high-payoff detail. **Dependencies:** Independent. Share receiver/base-geometry selection with existing LOD and picking paths. @@ -20,6 +20,14 @@ Start from ordinary Three decal geometry and generated materials. Reuse existing Surface-color impacts and environment marks on static/rigid opaque receivers. No deferred G-buffer decal renderer, normal/roughness channel rewrite, skinned tattoo system or virtual-texture painting. +### Opt-in projection implementation slice + +Extend the existing game-owned shooter `DecalField`; preserve `place(point, normal, variant, scale)` and its authored default scene. Add `project(receiver, geometry, point, normal, variant, { depth, scale? })`, where the game passes its existing hit object and public `baseGeometryOf(receiver)` result. The render source remains ordinary Three.js with no framework import. Upstream `DecalGeometry` clips only that receiver's borrowed geometry; the field owns the resulting buffers, keeps forward-facing triangles, applies the authored normal offset, and transforms the result into receiver-local space. + +Reuse existing material/slot budgets and deterministic per-variant round-robin eviction. A fixture with one 256-slot family proves the required total cap. Release projected buffers on slot replacement, clear, receiver removal and field disposal. Direct receiver removal triggers cleanup; `update()` also cleans receivers whose ancestor subtree left the field's parent. Rigid translation/rotation and positive uniform scale are supported. Reject skinned, morph/deforming, instanced/batched, transparent/cutout, mirrored, singular, sheared or nonuniformly scaled receivers explicitly. Source geometry is borrowed, is never reassigned or disposed, and may differ from the current render LOD. + +CPU proof belongs in `packages/create-threenative/__tests__/shooter-decals-projection.spec.ts`, alongside existing lifetime proof. An opt-in fixture will exercise real framework picking/base geometry, receiver motion, cap/eviction and teardown. Projection corner distortion and same-receiver overlapping forward-facing surfaces remain upstream limitations; the caller supplies a narrow projection depth. Browser/native and actual relevant screenshot gates remain open until executed. + ## Required behavior - A cap of 256 decals in the fixture is enforced with deterministic eviction, and receiver teardown removes its marks. @@ -33,26 +41,28 @@ All proof paths below are **planned implementation targets**, not existing passi ### Phase 1 — Reuse projection and bound lifetime -- [ ] Integrate upstream decal projection with existing picking/base-geometry data and explicit receiver support. proof: `pnpm exec vitest run packages/core/__tests__/vq-bounded-decals.spec.ts`. -- [ ] Implement deterministic pool limits, eviction, receiver cleanup and borrowed-resource ownership. proof: `pnpm exec vitest run packages/core/__tests__/vq-bounded-decals.spec.ts`. +- [x] Integrate upstream decal projection with existing picking/base-geometry data and explicit receiver support. proof: `node node_modules/vitest/vitest.mjs run --maxWorkers 1 packages/create-threenative/__tests__/shooter-decals-projection.spec.ts packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts` — 20 tests passed, including a real cooked GLB through the asset loader, 128-to-2-triangle LOD selection, framework picking and `baseGeometryOf`; renderer proof remains Phase 3. +- [x] Implement deterministic pool limits, eviction, receiver cleanup and borrowed-resource ownership. proof: `node node_modules/vitest/vitest.mjs run --maxWorkers 1 packages/create-threenative/__tests__/shooter-decals-projection.spec.ts packages/create-threenative/__tests__/shooter-decals-lifetime.spec.ts` — 23 tests passed; 1,024 impacts retained 256 slots, released 768 evicted projected buffers, and released the remaining 256 once at teardown while retaining borrowed source geometry/map. - Partial, 2026-10-02: the existing generated shooter `DecalField` now releases each cloned slot material, family material and shared geometry once; its texture remains borrowed. `node node_modules/vitest/vitest.mjs run packages/create-threenative/__tests__/shooter-decals-lifetime.spec.ts` passed 4 tests after reproducing 4 failures on the original implementation. This repairs the existing 224-slot template; it does not implement the proposed 256-decal receiver/projection fixture or introduce an engine pool. - Caller ownership follow-up, 2026-10-02: `Play` registers its procedural decal texture in the existing scene registry after the borrowing field. The real `Play.load`/`enter` and registry teardown regression proves all 224 material disposals precede one texture disposal on each of three scene lifecycles; repeated cleanup does not redispose it, and the borrowed sky texture is retained. proof: `node node_modules/vitest/vitest.mjs run --maxWorkers 1 packages/create-threenative/__tests__/template-runtime-cost.spec.ts -t 'releases the shooter-owned decal texture'` — missing-disposal failure before the fix, pass afterward. ### Phase 2 — Preserve authored appearance and receiver motion -- [ ] Add game-owned decal material/atlas composition with bounded bias and fade controls. proof: `pnpm exec vitest run packages/core/__tests__/vq-bounded-decals.spec.ts`. -- [ ] Verify rigid receiver motion, LOD changes and repeated scene teardown without detached marks or allocation growth. proof: `pnpm exec vitest run packages/core/__tests__/vq-bounded-decals.spec.ts`. +- [ ] Add game-owned decal material/atlas composition with bounded bias and fade controls. proof: `node node_modules/vitest/vitest.mjs run --maxWorkers 1 packages/create-threenative/__tests__/shooter-decals-projection.spec.ts` plus the runtime fixture. + - Materials, tints, map, offset, projection depth and size remain game-authored. Atlas/fade controls and their visible qualification are not implemented by this slice. +- [ ] Verify rigid receiver motion, LOD changes and repeated scene teardown without detached marks or allocation growth. proof: `node node_modules/vitest/vitest.mjs run --maxWorkers 1 packages/create-threenative/__tests__/shooter-decals-projection.spec.ts packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts` plus the Phase 3 lifecycle scenario. + - Receiver/ancestor rigid motion, positive uniform scale, direct and ancestor removal, explicit cleanup, legacy-slot reuse and actual loader/picker LOD independence are CPU-verified. Repeated projected scene resets and GPU residency still require the runtime fixture. - Partial, 2026-10-02: the lifetime regression exercises 20 field construction/teardown cycles, 2,000 pooled placements, clear/reuse, zero-slot teardown and repeated `dispose()`. Real Three disposal events/spies verify owned-resource release and retention of the borrowed map. Rigid receivers, LOD changes, GPU memory and game-scene transitions are not qualified by these unit tests. ### Phase 3 — Qualify the visible result -- [ ] The impact sequence covers the 256-mark cap, moving receiver and LOD boundary on browser WebGPU. proof: `node packages/playtest/dist/runner/cli.js examples/abyss-framework/playtests/vq-bounded-decals.playtest.json --url ${VQ_URL:?} --browser-recipe webgpu`. -- [ ] The same sequence runs on Linux native with matching pool/receiver observations and visible marks. proof: `node packages/playtest/dist/runner/cli.js examples/abyss-framework/playtests/vq-bounded-decals.playtest.json --target desktop --executable ${VQ_NATIVE_EXECUTABLE:?}`. +- [ ] The impact sequence covers the 256-mark cap, moving receiver and LOD boundary on browser WebGPU. proof: `node --import tsx packages/create-threenative/__tests__/fixtures/bounded-decals/verify.ts` / `.github/workflows/integration-decals.yml` — opt-in portable fixture and static/motion/teardown/restart scenarios are built and schema-validated; actual hosted captures are pending. +- [ ] The same sequence runs on Linux native with matching pool/receiver observations and visible marks. proof: `node packages/playtest/dist/runner/cli.js packages/create-threenative/__tests__/fixtures/bounded-decals/motion.playtest.json --target desktop --executable ${VQ_NATIVE_EXECUTABLE:?}` — portable `native.ts` entry is authored, but its native bundle/service must be staged with `public/receiver.glb` and `public/assets.manifest.json` and actually executed before qualification. ## Acceptance criteria - [ ] The supported receiver cases retain their marks under motion and teardown, and resource use is bounded independently of total shots fired. proof: the completed Phase 3 scenario outputs, with the named fixture, package/cohort and adapter recorded inline here. -- [ ] The feature preserves its documented inactive/fallback path and releases owned resources after repeated lifecycle transitions. proof: `pnpm exec vitest run packages/core/__tests__/vq-bounded-decals.spec.ts` plus the Phase 3 lifecycle scenario. +- [ ] The feature preserves its documented inactive/fallback path and releases owned resources after repeated lifecycle transitions. proof: the projection/lifetime CPU suites named above plus the Phase 3 restart scenario. ## Performance and promotion @@ -81,3 +91,10 @@ Update the phase boxes and this PRD only after the named proof runs. Record actu - The same four-file focused/adjacent command above passes 14/14 tests with `--maxWorkers 1`, including the original four field-ownership regressions. Fixture Rapier/Three warnings remain visible; no real disposal implementation is replaced. - `node node_modules/typescript/bin/tsc --noEmit -p packages/create-threenative/tsconfig.json`: exit 0. Biome check on the changed caller and test: exit 0 with six existing complexity warnings. `node --import tsx scripts/check-doc-links.ts`: exit 0, 2,395 links checked. - This follow-up does not rerun aggregate root build/typecheck/test lanes while parallel rendering work is active. Browser/native qualification, actual relevant runtime screenshot proof, required CI and independent review remain open; the PR stays draft. Projection/receiver feature expansion is not included in this ownership repair. + +2026-10-02 opt-in receiver projection milestone: + +- Initial projection tests: 17 failed because the opt-in API was absent; all passed after implementation. Additional invalid-normal regression failed before its validation repair; the final projection suite passes 19 tests. The real GLB loader/LOD/picking integration passes one additional test. +- Focused and adjacent validation: 6 files, 34 tests passed with `--maxWorkers 1` (projection, fixture, lifetime, template-runtime-cost, shooter-rig, world-environment-lifetime). Package typecheck passes. Changed-file Biome check passes with the existing texture-generator complexity warning and two fixture complexity warnings. No root aggregate build/test pass is claimed. +- The portable real engine fixture builds through Vite, and all four scenarios pass the maintained schema validator. The local pixel-producing run reaches the maintained runner but fails closed because no usable X display/Xvfb is available. No local screenshot is claimed. +- The hosted workflow uses the built public runner and retains actual reports/PNGs before result checks. Exact source SHA and adapter accompany each capture; software rendering is correctness-only. Error diagnostics or `TN_PLAYTEST_SOFTWARE_DEVICE_LOST` prevent qualification even if a runner report says pass. Actual screenshot progress must be inspected and attached to the PR, including incomplete gates, before any readiness claim. diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts b/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts new file mode 100644 index 0000000000..9da4bebca1 --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts @@ -0,0 +1,203 @@ +import { + AmbientLight, + BoxGeometry, + type BufferGeometry, + Color, + DirectionalLight, + Mesh, + MeshStandardMaterial, + type Object3D, + type PerspectiveCamera, + Vector3, +} from "three"; +import type { WebGPURenderer } from "three/webgpu"; +import { type ICtx, Scene, baseGeometryOf, defineGame } from "../../../../core/dist/index.js"; +import { playtest } from "../../../../core/dist/playtest.js"; +import { DecalField, bulletHoleTexture } from "../../../templates/shooter/src/render/decals.js"; + +/** Portable, opt-in runtime proof. Neither the shooter entry nor its authored look is changed. */ +export function createDecalFixture() { + let generation = 0; + class DecalRoom extends Scene { + #source: Object3D | undefined; + #dispose = () => {}; + + override async load(ctx: ICtx): Promise { + const model = await ctx.assets.model<{ scene: Object3D }>("receiver.glb"); + this.#source = model.scene; + } + + override enter(ctx: ICtx) { + generation += 1; + const source = this.#source?.getObjectByProperty("isMesh", true); + if (!(source instanceof Mesh)) throw new Error("Decal fixture receiver did not load."); + const camera = ctx.camera as PerspectiveCamera; + camera.position.set(0, 4.5, 12); + camera.lookAt(0, 2.3, 0); + ctx.scene.background = new Color(0x17232f); + const materials = [0xc7b493, 0x84a9ad, 0x36485a].map( + (color) => new MeshStandardMaterial({ color, roughness: 0.9 }), + ); + const left = source.clone(); + const right = source.clone(); + left.material = materials[0] as MeshStandardMaterial; + right.material = materials[1] as MeshStandardMaterial; + left.position.set(-2.5, 2.4, 0); + right.position.set(2.5, 2.4, 0); + ctx.add(left); + ctx.add(right); + const floorGeometry = new BoxGeometry(15, 0.15, 10); + const floor = new Mesh(floorGeometry, materials[2]); + floor.position.y = -0.1; + ctx.add(floor); + const sun = new DirectionalLight(0xffefd2, 3); + sun.position.set(2, 7, 8); + ctx.add(sun); + ctx.add(new AmbientLight(0xdbeaff, 1.5)); + const map = bulletHoleTexture(64); + const field = new DecalField(ctx.scene, { + countPerVariant: 256, + map, + offset: 0.008, + size: 0.19, + tints: { impact: 0xffffff }, + }); + field.settle(); + let created = 0; + let released = 0; + const stamp = (receiver: Mesh, x: number, y: number): void => { + receiver.updateWorldMatrix(true, false); + const origin = receiver.localToWorld(new Vector3(x, y, 2)); + const direction = new Vector3(0, 0, -1).transformDirection(receiver.matrixWorld); + const hit = ctx.raycast({ + origin, + direction, + targets: receiver, + exclude: receiver.children, + }); + if (hit?.object !== receiver || hit.face === null || hit.face === undefined) { + throw new Error("Decal fixture lost its authored picking surface."); + } + const normal = hit.face.normal.clone().transformDirection(receiver.matrixWorld); + if ( + !field.project(receiver, baseGeometryOf(receiver), hit.point, normal, "impact", { + depth: 0.1, + }) + ) { + throw new Error("Decal fixture projection unexpectedly missed."); + } + const slot = receiver.children.at(-1); + if (!(slot instanceof Mesh)) throw new Error("Decal fixture has no projected slot."); + created += 1; + slot.geometry.addEventListener("dispose", () => { + released += 1; + }); + }; + for (let hit = 0; hit < 320; hit += 1) { + const index = Math.floor(hit / 2); + stamp( + hit % 2 === 0 ? left : right, + -1.6 + (index % 16) * 0.213, + -1.45 + Math.floor(index / 16) * 0.32, + ); + } + let moving = false; + let elapsed = 0; + let frames = 0; + let motionError = 0; + let lodHit = false; + let maxDrawCalls = 0; + const base = baseGeometryOf(left); + if (base.index?.count !== 384) + throw new Error("Decal fixture LOD0 must contain 128 triangles."); + const observation = () => { + let geometryBytes = 0; + let active = 0; + for (const receiver of [left, right]) { + for (const child of receiver.children) { + if (!(child instanceof Mesh)) continue; + active += 1; + const geometry: BufferGeometry = child.geometry; + for (const attribute of Object.values(geometry.attributes)) { + geometryBytes += attribute.array.byteLength; + } + geometryBytes += geometry.index?.array.byteLength ?? 0; + } + } + return { + generation, + active, + capacity: field.capacity, + created, + released, + geometryBytes, + maxDrawCalls, + motionError, + receiverMotion: Math.abs(right.rotation.y) + Math.abs(right.position.y - 2.4), + lodHit, + renderTriangles: (left.geometry.index?.count ?? 0) / 3, + receiverRemoved: right.parent === null, + }; + }; + ctx.entities.add("decals", { debug: observation }); + this.#dispose = () => { + field.dispose(); + map.dispose(); + floorGeometry.dispose(); + for (const material of materials) material.dispose(); + }; + return (frame: ICtx, dt: number) => { + maxDrawCalls = Math.max( + maxDrawCalls, + (ctx.renderer.raw as WebGPURenderer).info.render.drawCalls, + ); + if (frame.input.justPressed("motion")) moving = !moving; + if (frame.input.justPressed("remove")) right.removeFromParent(); + if (frame.input.justPressed("reset")) { + void frame.goto("room"); + return; + } + if (moving) { + elapsed += dt; + right.rotation.y = Math.sin(elapsed) * 0.5; + right.position.y = 2.4 + Math.sin(elapsed * 1.5) * 0.35; + } + field.update(); + right.updateWorldMatrix(true, true); + const mark = right.children[0]; + if (mark instanceof Mesh) { + const local = new Vector3().fromBufferAttribute( + mark.geometry.getAttribute("position"), + 0, + ); + motionError = Math.max( + motionError, + mark.localToWorld(local.clone()).distanceTo(right.localToWorld(local)), + ); + } + if (!lodHit && left.geometry !== base) { + stamp(left, 1.7, 1.7); + lodHit = true; + } + frames += 1; + if (frames % 30 === 0) console.info(`TN_DECAL_FIXTURE:${JSON.stringify(observation())}`); + }; + } + + override exit(): void { + this.#dispose(); + this.#dispose = () => {}; + } + } + return defineGame({ + camera: { far: 100, fov: 48, near: 0.1, projection: "perspective" }, + display: { maxFps: 60 }, + initialState: {}, + input: { motion: { keys: ["KeyM"] }, remove: { keys: ["KeyX"] }, reset: { keys: ["KeyR"] } }, + plugins: [playtest({ holdUntilAttached: true })], + renderer: { preferWebGPU: true }, + scenes: { room: DecalRoom }, + seed: 11, + start: "room", + }); +} diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/index.html b/packages/create-threenative/__tests__/fixtures/bounded-decals/index.html new file mode 100644 index 0000000000..1d6085dc63 --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/index.html @@ -0,0 +1,2 @@ + +VQ11 bounded receiver decals diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/main.ts b/packages/create-threenative/__tests__/fixtures/bounded-decals/main.ts new file mode 100644 index 0000000000..0447654e21 --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/main.ts @@ -0,0 +1,12 @@ +import { createDecalFixture } from "./game.js"; +const game = createDecalFixture(); +void game + .start() + .then(() => { + const canvas = game.ctx?.renderer.domElement; + if (canvas === undefined) throw new Error("Decal fixture has no canvas."); + document.body.append(canvas); + }) + .catch((error: unknown) => { + console.error(error); + }); diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/motion.playtest.json b/packages/create-threenative/__tests__/fixtures/bounded-decals/motion.playtest.json new file mode 100644 index 0000000000..687a7d9a19 --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/motion.playtest.json @@ -0,0 +1,95 @@ +{ + "name": "vq11-motion", + "schemaVersion": 1, + "target": "web", + "viewport": { + "width": 960, + "height": 540 + }, + "warmupFrames": 10, + "artifacts": { + "screenshots": true, + "console": true + }, + "steps": [ + { + "kind": "wait", + "label": "lod-and-pool", + "waitFrames": 40 + }, + { + "kind": "input", + "label": "motion", + "press": "KeyM", + "holdTicks": 2, + "release": true + }, + { + "kind": "wait", + "label": "settled", + "waitFrames": 90 + } + ], + "assert": { + "diagnostics": { + "runtimeReady": true, + "noConsoleErrors": true, + "noNetworkErrors": true + }, + "components": [ + { + "entity": "decals", + "component": "active", + "equals": 256, + "allowTrivial": "Real projected marks survive more than 256 impacts and eviction." + }, + { + "entity": "decals", + "component": "created", + "gte": 321, + "allowTrivial": "320 initial marks plus a real post-LOD raycast hit." + }, + { + "entity": "decals", + "component": "geometryBytes", + "allowTrivial": "Measures actual projected GPU-upload buffer bytes, not an empty field.", + "gte": 1 + }, + { + "entity": "decals", + "component": "renderTriangles", + "equals": 2, + "allowTrivial": "The loaded 128-triangle authored receiver actually selects its two-triangle LOD." + }, + { + "entity": "decals", + "component": "motionError", + "lte": 1e-6, + "allowTrivial": "Measures mark vertices against receiver-local coordinates during actual rigid motion." + }, + { + "entity": "decals", + "component": "generation", + "equals": 1, + "allowTrivial": "The restart input must construct a new scene; unchanged initial state is not evidence." + }, + { + "entity": "decals", + "component": "receiverMotion", + "allowTrivial": "The actual receiver transform must change, so a dropped motion input cannot pass.", + "gte": 0.1 + } + ], + "visual": [ + { + "region": { + "x": 0, + "y": 0, + "width": 960, + "height": 540, + "minNonblankPixelRatio": 0.15 + } + } + ] + } +} diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/native.ts b/packages/create-threenative/__tests__/fixtures/bounded-decals/native.ts new file mode 100644 index 0000000000..4369be64a0 --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/native.ts @@ -0,0 +1,2 @@ +import { createDecalFixture } from "./game.js"; +export default createDecalFixture(); diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/public/assets.manifest.json b/packages/create-threenative/__tests__/fixtures/bounded-decals/public/assets.manifest.json new file mode 100644 index 0000000000..c86e13268b --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/public/assets.manifest.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "entries": { + "receiver.glb": { + "output": "receiver.glb", + "lod": { + "runtime": { + "maxPixelError": 1, + "hysteresis": 0.15 + } + } + } + } +} diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/public/receiver.glb b/packages/create-threenative/__tests__/fixtures/bounded-decals/public/receiver.glb new file mode 100644 index 0000000000000000000000000000000000000000..7dcc89a384ee5d935da87ba5f4144702fe1ad313 GIT binary patch literal 4712 zcmeH~>2BLb6ouJzOZR1qbz=C3VXko%Y|r=wUg8w6;3))FG>rk zX&QN$7LMc7Y>=98R1Et~*&(cz9QDK6{xIw%M+Pb#j1G#q8-`ueZ_8W;K|5y{o}_U% zrt0P5D9h6LaFAh#;r;F1jmE}y-Il0t?`$n^hPAR3j(bOOlQ|XHEXqt8M;$xKoMxuS zA?^A`{lQV(Z>A8DgolTMPZM@{?w+%jLyCx_lD!|kpvZ!Yi1_j)xz z_3=B%Io;v>dO6UfJD2b0(%ViQo@HkH_}Ko`q}kX_(`#k&%`ePJ+AK<2{0ytp3$v9; zmPgjw$^UtEd0|=(u9uf*svJdCuRf-Fb$b3<_zI1@vO4dB>w!9Hb*41UH#PYg{?Rw` zyW0=1{}c~Kg+k%)mx5qrqb`4A1!H$37ZW~L9B7s)b`82X;d8}f#lg~K4(n_UhGb9A z=ZXW(Zk~v(mCu*`JyXjwwQ?Q@!MW_k#h7Q*bD&w~<>QMJ=3IW9TArzu^H>(kUR-0; zv3>5?j(KLH=fw$A%a2pbv+bduKbCQKjs2MTbH({OKhZ0Dy35vbY-{-kOCChWa=SIQ zmSbDX?{zVmBb3j#>->Dwv90CUp3C!C&fw=&c6d%anqB_58|c||Cc^IAwi#$8+~5!E$7h}6hG-a`l4d_?}+uN{Ij3))6SzW zD}Kg#^cBU=I*-1p__p)tYmUiHX8G@s)i)HA8IQiHn9O+eEyZNUqi-uFGah|MF`4n` zyNbz-N8eLSW<2`7V{()61E(J+|(JvK~5s!YQxZphcwPG^j(Qg!!5s!YWn9O*z=$PDO zyz6vIF&XjbcZ$h~M@x#yj7Q6g$%scQiphvatBT2pN2e8&8IR65CN~*poz5vHBOaYs zOh!DqpqPw!w5FJhc=UV4WW=LCC?+Ew{ZTQQ@#s&E$xX&Zr%Q^-h)0(dlM#=uC?+Ew zT~$m*Jo>X@GUCxS#bm^z>x#*YM}Kil?j^Z@b-JOLjCk~(Vlv{<-xQM(k8Ua^BOcvS dOh!CfS4>7cx~-VZc=Wzwk>Ix6xbaVg! literal 0 HcmV?d00001 diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/restart.playtest.json b/packages/create-threenative/__tests__/fixtures/bounded-decals/restart.playtest.json new file mode 100644 index 0000000000..4bbf5da157 --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/restart.playtest.json @@ -0,0 +1,83 @@ +{ + "name": "vq11-restart", + "schemaVersion": 1, + "target": "web", + "viewport": { + "width": 960, + "height": 540 + }, + "warmupFrames": 10, + "artifacts": { + "screenshots": true, + "console": true + }, + "steps": [ + { + "kind": "wait", + "label": "lod-and-pool", + "waitFrames": 40 + }, + { + "kind": "input", + "label": "restart", + "press": "KeyR", + "holdTicks": 2, + "release": true + }, + { + "kind": "wait", + "label": "settled", + "waitFrames": 90 + } + ], + "assert": { + "diagnostics": { + "runtimeReady": true, + "noConsoleErrors": true, + "noNetworkErrors": true + }, + "components": [ + { + "entity": "decals", + "component": "active", + "equals": 256, + "allowTrivial": "Real projected marks survive more than 256 impacts and eviction." + }, + { + "entity": "decals", + "component": "created", + "gte": 321, + "allowTrivial": "320 initial marks plus a real post-LOD raycast hit." + }, + { + "entity": "decals", + "component": "geometryBytes", + "allowTrivial": "Measures actual projected GPU-upload buffer bytes, not an empty field.", + "gte": 1 + }, + { + "entity": "decals", + "component": "renderTriangles", + "equals": 2, + "allowTrivial": "The loaded 128-triangle authored receiver actually selects its two-triangle LOD." + }, + { + "entity": "decals", + "component": "generation", + "equals": 2, + "allowTrivial": "The restart input must construct a new scene; unchanged initial state is not evidence." + } + ], + "visual": [ + { + "region": { + "x": 0, + "y": 0, + "width": 960, + "height": 540, + "minNonblankPixelRatio": 0.15 + } + } + ] + } +} diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/static.playtest.json b/packages/create-threenative/__tests__/fixtures/bounded-decals/static.playtest.json new file mode 100644 index 0000000000..f0a126467f --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/static.playtest.json @@ -0,0 +1,71 @@ +{ + "name": "vq11-static", + "schemaVersion": 1, + "target": "web", + "viewport": { + "width": 960, + "height": 540 + }, + "warmupFrames": 10, + "artifacts": { + "screenshots": true, + "console": true + }, + "steps": [ + { + "kind": "wait", + "label": "lod-and-pool", + "waitFrames": 90 + } + ], + "assert": { + "diagnostics": { + "runtimeReady": true, + "noConsoleErrors": true, + "noNetworkErrors": true + }, + "components": [ + { + "entity": "decals", + "component": "active", + "equals": 256, + "allowTrivial": "Real projected marks survive more than 256 impacts and eviction." + }, + { + "entity": "decals", + "component": "created", + "gte": 321, + "allowTrivial": "320 initial marks plus a real post-LOD raycast hit." + }, + { + "entity": "decals", + "component": "geometryBytes", + "allowTrivial": "Measures actual projected GPU-upload buffer bytes, not an empty field.", + "gte": 1 + }, + { + "entity": "decals", + "component": "renderTriangles", + "equals": 2, + "allowTrivial": "The loaded 128-triangle authored receiver actually selects its two-triangle LOD." + }, + { + "entity": "decals", + "component": "generation", + "equals": 1, + "allowTrivial": "The restart input must construct a new scene; unchanged initial state is not evidence." + } + ], + "visual": [ + { + "region": { + "x": 0, + "y": 0, + "width": 960, + "height": 540, + "minNonblankPixelRatio": 0.15 + } + } + ] + } +} diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/teardown.playtest.json b/packages/create-threenative/__tests__/fixtures/bounded-decals/teardown.playtest.json new file mode 100644 index 0000000000..4b985a1739 --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/teardown.playtest.json @@ -0,0 +1,89 @@ +{ + "name": "vq11-teardown", + "schemaVersion": 1, + "target": "web", + "viewport": { + "width": 960, + "height": 540 + }, + "warmupFrames": 10, + "artifacts": { + "screenshots": true, + "console": true + }, + "steps": [ + { + "kind": "wait", + "label": "lod-and-pool", + "waitFrames": 40 + }, + { + "kind": "input", + "label": "teardown", + "press": "KeyX", + "holdTicks": 2, + "release": true + }, + { + "kind": "wait", + "label": "settled", + "waitFrames": 90 + } + ], + "assert": { + "diagnostics": { + "runtimeReady": true, + "noConsoleErrors": true, + "noNetworkErrors": true + }, + "components": [ + { + "entity": "decals", + "component": "active", + "equals": 128, + "allowTrivial": "Removing one receiver releases its remaining 128 marks while the other retains its half." + }, + { + "entity": "decals", + "component": "created", + "gte": 321, + "allowTrivial": "320 initial marks plus a real post-LOD raycast hit." + }, + { + "entity": "decals", + "component": "geometryBytes", + "allowTrivial": "Measures actual projected GPU-upload buffer bytes, not an empty field.", + "gte": 1 + }, + { + "entity": "decals", + "component": "renderTriangles", + "equals": 2, + "allowTrivial": "The loaded 128-triangle authored receiver actually selects its two-triangle LOD." + }, + { + "entity": "decals", + "component": "released", + "gte": 193, + "allowTrivial": "65 cap evictions plus 128 receiver-owned projections." + }, + { + "entity": "decals", + "component": "generation", + "equals": 1, + "allowTrivial": "The restart input must construct a new scene; unchanged initial state is not evidence." + } + ], + "visual": [ + { + "region": { + "x": 0, + "y": 0, + "width": 960, + "height": 540, + "minNonblankPixelRatio": 0.15 + } + } + ] + } +} diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/verify.ts b/packages/create-threenative/__tests__/fixtures/bounded-decals/verify.ts new file mode 100644 index 0000000000..0b4a7ff7ff --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/verify.ts @@ -0,0 +1,105 @@ +import { execFileSync } from "node:child_process"; +import { mkdir, stat, writeFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { build } from "vite"; +import { + WEBGPU_BROWSER_ARGS, + runStandalonePlaytest, +} from "../../../../playtest/dist/runner/index.js"; + +const fixture = dirname(fileURLToPath(import.meta.url)); +const root = resolve(fixture, "../../../../.."); +const artifacts = join(root, "artifacts/vq11-decals"); +const site = join(artifacts, "site"); +await mkdir(artifacts, { recursive: true }); +await build({ configFile: false, root: fixture, build: { outDir: site, emptyOutDir: true } }); +if (!process.argv.includes("--build-only")) { + const sourceSha = execFileSync("git", ["rev-parse", "HEAD"], { + cwd: root, + encoding: "utf8", + }).trim(); + const vite = join( + dirname(fileURLToPath(import.meta.resolve("vite/package.json"))), + "bin/vite.js", + ); + const results = []; + for (const name of ["static", "motion", "teardown", "restart"]) { + const directory = join(artifacts, name); + const report = await runStandalonePlaytest({ + artifactDirectory: directory, + projectPath: fixture, + scenarioPath: join(fixture, `${name}.playtest.json`), + url: "http://127.0.0.1:4173/", + port: 0, + server: { + command: `${JSON.stringify(process.execPath)} ${JSON.stringify(vite)} preview --host 127.0.0.1 --port $PORT --strictPort --outDir ${JSON.stringify(site)}`, + cwd: fixture, + }, + timeoutMs: 180_000, + headless: false, + trace: false, + target: "browser", + browserArgs: WEBGPU_BROWSER_ARGS, + allowSoftwareAdapter: true, + captureArtifactScreenshots: true, + }); + // Keep real partial frames/report even when qualification fails. Never turn lost-device + // frames into passing proof just because the software-adapter runner downgraded a warning. + await writeFile( + join(directory, "report.json"), + `${JSON.stringify({ sourceSha, ...report }, null, 2)}\n`, + ); + const last = report.observations?.console + .filter((entry) => entry.text.startsWith("TN_DECAL_FIXTURE:")) + .at(-1); + const measurement = + last === undefined ? undefined : JSON.parse(last.text.slice("TN_DECAL_FIXTURE:".length)); + const screenshot = join(directory, "after.png"); + const screenshotPresent = await stat(screenshot) + .then((file) => file.size > 0) + .catch(() => false); + const disallowedDiagnostics = report.diagnostics.filter( + (diagnostic) => + diagnostic.severity === "error" || diagnostic.code === "TN_PLAYTEST_SOFTWARE_DEVICE_LOST", + ); + const qualified = + report.pass && + report.capture?.rendererKind === "webgpu" && + Object.values(report.capture.adapter).some((value) => value.length > 0) && + disallowedDiagnostics.length === 0 && + screenshotPresent && + measurement !== undefined && + measurement.active === (name === "teardown" ? 128 : 256) && + measurement.capacity === 256 && + measurement.created >= 321 && + measurement.lodHit === true && + measurement.renderTriangles === 2 && + Number.isFinite(measurement.geometryBytes) && + measurement.geometryBytes > 0 && + Number.isFinite(measurement.maxDrawCalls) && + measurement.maxDrawCalls > 0 && + Number.isFinite(measurement.motionError) && + measurement.motionError <= 1e-6; + results.push({ + name, + sourceSha, + qualified, + measurement, + capture: report.capture, + screenshot: screenshotPresent ? screenshot : null, + diagnostics: report.diagnostics, + }); + await writeFile( + join(artifacts, "summary.json"), + `${JSON.stringify({ sourceSha, correctnessOnly: true, results }, null, 2)}\n`, + ); + } + if (results.some((result) => !result.qualified)) + throw new Error( + "VQ11 runtime qualification failed; retained frames are diagnostic only. Inspect artifacts/vq11-decals/summary.json.", + ); + console.info( + `VQ11_DECAL_PROOF ${JSON.stringify({ sourceSha, captures: results.length, correctnessOnly: true })}`, + ); +} diff --git a/packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts b/packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts new file mode 100644 index 0000000000..be99222a4e --- /dev/null +++ b/packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts @@ -0,0 +1,87 @@ +import { readFile } from "node:fs/promises"; +import { Mesh, PerspectiveCamera, Scene, Vector2, Vector3 } from "three"; +import { afterEach, expect, it, vi } from "vitest"; +import { createAssetLoader } from "../../core/src/assets.js"; +import { baseGeometryOf, updateModelLods } from "../../core/src/model-lod.js"; +import { ScenePicker } from "../../core/src/picking.js"; +import { DecalField, bulletHoleTexture } from "../templates/shooter/src/render/decals.js"; + +afterEach(() => vi.unstubAllGlobals()); + +it("projects real framework hits onto the fixture's authored surface after actual LOD selection", async () => { + const glb = await readFile( + new URL("./fixtures/bounded-decals/public/receiver.glb", import.meta.url), + ); + const manifest = await readFile( + new URL("./fixtures/bounded-decals/public/assets.manifest.json", import.meta.url), + "utf8", + ); + vi.stubGlobal("fetch", async (url: string) => + url.endsWith("assets.manifest.json") + ? new Response(manifest, { headers: { "content-type": "application/json" } }) + : new Response(glb, { headers: { "content-type": "model/gltf-binary" } }), + ); + const loader = createAssetLoader({ basePath: "https://fixture.invalid" }); + const loaded = await loader.model<{ scene: Scene }>("receiver.glb"); + const receiver = loaded.scene.getObjectByProperty("isMesh", true); + if (!(receiver instanceof Mesh)) throw new Error("Fixture mesh missing"); + const base = baseGeometryOf(receiver); + expect(base.index?.count).toBe(384); + const root = new Scene(); + root.add(receiver); + const camera = new PerspectiveCamera(48, 16 / 9, 0.1, 100); + camera.position.set(0, 0, 12); + camera.updateMatrixWorld(true); + root.updateMatrixWorld(true); + expect(updateModelLods(root, camera, 540)).toBe(2); + expect(receiver.geometry).not.toBe(base); + expect(baseGeometryOf(receiver)).toBe(base); + const picker = new ScenePicker({ + camera, + pointer: () => new Vector2(), + scene: root, + viewport: {} as never, + }); + const hit = picker.raycast({ + origin: new Vector3(1, 1, 2), + direction: new Vector3(0, 0, -1), + targets: receiver, + }); + expect(hit?.object).toBe(receiver); + expect(hit?.point.toArray()).toEqual([1, 1, 0]); + const map = bulletHoleTexture(8); + const field = new DecalField(root, { + countPerVariant: 1, + size: 0.2, + offset: 0.01, + map, + tints: { impact: 0xffffff }, + }); + if (hit?.face === null || hit?.face === undefined) throw new Error("Fixture hit normal missing"); + expect( + field.project(receiver, baseGeometryOf(receiver), hit.point, hit.face.normal, "impact", { + depth: 0.1, + }), + ).toBe(true); + const mark = receiver.children[0]; + if (!(mark instanceof Mesh)) throw new Error("Fixture mark missing"); + const projected = mark.geometry; + camera.position.z = 0.3; + camera.updateMatrixWorld(true); + expect(updateModelLods(root, camera, 540)).toBe(128); + expect(receiver.geometry).toBe(base); + expect(mark.geometry).toBe(projected); + expect( + picker + .raycast({ + origin: new Vector3(1, 1, 2), + direction: new Vector3(0, 0, -1), + targets: receiver, + exclude: receiver.children, + }) + ?.point.toArray(), + ).toEqual([1, 1, 0]); + field.dispose(); + map.dispose(); + picker.dispose(); +}); diff --git a/packages/create-threenative/__tests__/shooter-decals-projection.spec.ts b/packages/create-threenative/__tests__/shooter-decals-projection.spec.ts new file mode 100644 index 0000000000..0f26a4f70a --- /dev/null +++ b/packages/create-threenative/__tests__/shooter-decals-projection.spec.ts @@ -0,0 +1,328 @@ +import { + BatchedMesh, + BoxGeometry, + type BufferGeometry, + Float32BufferAttribute, + Group, + InstancedMesh, + Mesh, + MeshBasicMaterial, + PlaneGeometry, + SkinnedMesh, + Texture, + Vector3, +} from "three"; +import { DecalGeometry } from "three/addons/geometries/DecalGeometry.js"; +import { describe, expect, it, vi } from "vitest"; +import { baseGeometryOf } from "../../core/src/model-lod.js"; +import { DecalField } from "../templates/shooter/src/render/decals.js"; + +function fixture(count = 2) { + const root = new Group(); + const map = new Texture(); + const receiver = new Mesh( + new PlaneGeometry(8, 8), + new MeshBasicMaterial(), + ); + root.add(receiver); + const field = new DecalField(root, { + countPerVariant: count, + map, + offset: 0.01, + size: 1, + tints: { stone: 0xffffff }, + }); + const slots = root.children.filter( + (child): child is Mesh => + child instanceof Mesh && child !== receiver, + ); + return { root, map, receiver, field, slots }; +} + +const normal = new Vector3(0, 0, 1); + +describe("game-owned receiver decal projection", () => { + it("uses upstream projection on only the selected receiver with authored UVs and bias", () => { + const { root, receiver, field, slots } = fixture(); + const other = new Mesh(new PlaneGeometry(8, 8), new MeshBasicMaterial()); + other.position.z = 0.02; + root.add(other); + expect( + field.project(receiver, baseGeometryOf(receiver), new Vector3(), normal, "stone", { + depth: 0.2, + }), + ).toBe(true); + const slot = slots[0]; + expect(slot?.parent).toBe(receiver); + expect(slot?.geometry).toBeInstanceOf(DecalGeometry); + expect(other.children).toHaveLength(0); + const positions = slot?.geometry.getAttribute("position"); + const uvs = slot?.geometry.getAttribute("uv"); + expect(positions?.count).toBeGreaterThan(0); + for (let index = 0; index < (positions?.count ?? 0); index += 1) { + expect(positions?.getZ(index)).toBeCloseTo(0.01, 6); + expect(uvs?.getX(index)).toBeGreaterThanOrEqual(-1e-6); + expect(uvs?.getX(index)).toBeLessThanOrEqual(1 + 1e-6); + expect(uvs?.getY(index)).toBeGreaterThanOrEqual(-1e-6); + expect(uvs?.getY(index)).toBeLessThanOrEqual(1 + 1e-6); + } + field.dispose(); + }); + + it("retains local geometry through rigid receiver and ancestor motion", () => { + const { root, receiver, field, slots } = fixture(); + const parent = new Group(); + root.add(parent); + parent.add(receiver); + receiver.position.set(2, 1, 3); + receiver.rotation.y = 0.7; + receiver.scale.setScalar(2); + receiver.updateWorldMatrix(true, false); + const point = receiver.localToWorld(new Vector3()); + const worldNormal = normal.clone().transformDirection(receiver.matrixWorld); + field.project(receiver, receiver.geometry, point, worldNormal, "stone", { depth: 0.2 }); + const slot = slots[0]; + if (slot === undefined) throw new Error("Missing slot"); + const local = new Vector3().fromBufferAttribute(slot.geometry.getAttribute("position"), 0); + parent.position.set(-3, 4, 1); + parent.rotation.z = 0.4; + receiver.position.x += 1; + receiver.rotation.y += 0.5; + root.updateMatrixWorld(true); + const expected = receiver.localToWorld(local.clone()); + const actual = slot.localToWorld(local.clone()); + expect(actual.distanceTo(expected)).toBeLessThan(1e-6); + expect( + new Vector3().fromBufferAttribute(slot.geometry.getAttribute("position"), 0).equals(local), + ).toBe(true); + field.dispose(); + }); + + it("does not project the back face of a thin receiver", () => { + const { receiver, field, slots } = fixture(); + receiver.geometry = new BoxGeometry(2, 2, 0.01); + field.project(receiver, receiver.geometry, new Vector3(0, 0, 0.005), normal, "stone", { + depth: 0.2, + }); + const geometry = slots[0]?.geometry; + const positions = geometry?.getAttribute("position"); + const indices = geometry?.getIndex(); + expect(indices?.count).toBeGreaterThan(0); + for (let index = 0; index < (indices?.count ?? 0); index += 1) { + expect(positions?.getZ(indices?.getX(index) ?? -1)).toBeCloseTo(0.015, 6); + } + field.dispose(); + }); + + it("bounds 1,024 impacts to 256 slots and disposes evicted projected geometry", () => { + const { receiver, field, slots, map } = fixture(256); + const borrowed = receiver.geometry; + const borrowedDisposed = vi.fn(); + borrowed.addEventListener("dispose", borrowedDisposed); + const mapDisposed = vi.fn(); + map.addEventListener("dispose", mapDisposed); + let disposed = 0; + for (let hit = 0; hit < 1024; hit += 1) { + expect( + field.project(receiver, borrowed, new Vector3(), normal, "stone", { depth: 0.2 }), + ).toBe(true); + slots[hit % 256]?.geometry.addEventListener("dispose", () => { + disposed += 1; + }); + expect(receiver.children).toHaveLength(Math.min(hit + 1, 256)); + } + expect(disposed).toBe(768); + expect(field.capacity).toBe(256); + expect(field.placed).toBe(1024); + field.dispose(); + field.dispose(); + expect(disposed).toBe(1024); + expect(receiver.children).toHaveLength(0); + expect(borrowedDisposed).not.toHaveBeenCalled(); + expect(mapDisposed).not.toHaveBeenCalled(); + }); + + it("releases a removed receiver immediately and a detached ancestor on update", () => { + const { root, receiver, field, slots } = fixture(); + const parent = new Group(); + root.add(parent); + parent.add(receiver); + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { depth: 0.2 }); + const firstDisposed = vi.fn(); + slots[0]?.geometry.addEventListener("dispose", firstDisposed); + receiver.removeFromParent(); + expect(firstDisposed).toHaveBeenCalledTimes(1); + expect(receiver.children).toHaveLength(0); + parent.add(receiver); + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { depth: 0.2 }); + const secondDisposed = vi.fn(); + slots[1]?.geometry.addEventListener("dispose", secondDisposed); + parent.removeFromParent(); + field.update(); + field.update(); + expect(secondDisposed).toHaveBeenCalledTimes(1); + expect(receiver.children).toHaveLength(0); + field.dispose(); + }); + + it("restores the legacy plane on reuse and clears only owned projected geometry", () => { + const { root, receiver, field, slots } = fixture(1); + const plane = slots[0]?.geometry; + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { depth: 0.2 }); + const projectedDisposed = vi.fn(); + slots[0]?.geometry.addEventListener("dispose", projectedDisposed); + field.place(new Vector3(1, 2, 3), normal, "stone", 2); + expect(projectedDisposed).toHaveBeenCalledTimes(1); + expect(slots[0]?.parent).toBe(root); + expect(slots[0]?.geometry).toBe(plane); + expect(slots[0]?.position.toArray()).toEqual([1, 2, 3.01]); + expect(slots[0]?.scale.x).toBe(2); + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { depth: 0.2 }); + const clearedDisposed = vi.fn(); + slots[0]?.geometry.addEventListener("dispose", clearedDisposed); + field.clear(); + field.clear(); + expect(clearedDisposed).toHaveBeenCalledTimes(1); + expect(slots[0]?.geometry).toBe(plane); + expect(receiver.children).toHaveLength(0); + field.dispose(); + }); + + it("uses a supplied base geometry across render LOD swaps without mutating or owning it", () => { + const { receiver, field, slots } = fixture(); + const base = baseGeometryOf(receiver); + const coarse = new PlaneGeometry(0.1, 0.1); + receiver.geometry = coarse; + field.project(receiver, base, new Vector3(2, 0, 0), normal, "stone", { depth: 0.2 }); + expect(slots[0]?.geometry.getAttribute("position").count).toBeGreaterThan(0); + expect(receiver.geometry).toBe(coarse); + const localPositions = Array.from(slots[0]?.geometry.getAttribute("position").array ?? []); + coarse.dispose(); + receiver.geometry = base; + expect(Array.from(slots[0]?.geometry.getAttribute("position").array ?? [])).toEqual( + localPositions, + ); + field.dispose(); + }); + + it.each([ + ["skinned", () => new SkinnedMesh(new PlaneGeometry(8, 8), new MeshBasicMaterial())], + ["instanced", () => new InstancedMesh(new PlaneGeometry(8, 8), new MeshBasicMaterial(), 1)], + ["batched", () => new BatchedMesh(2, 20, 30, new MeshBasicMaterial())], + [ + "transparent", + () => new Mesh(new PlaneGeometry(8, 8), new MeshBasicMaterial({ transparent: true })), + ], + ["cutout", () => new Mesh(new PlaneGeometry(8, 8), new MeshBasicMaterial({ alphaTest: 0.5 }))], + [ + "morphed", + () => { + const mesh = new Mesh(new PlaneGeometry(8, 8), new MeshBasicMaterial()); + mesh.geometry.morphAttributes.position = [ + new Float32BufferAttribute(new Float32Array(12), 3), + ]; + return mesh; + }, + ], + [ + "nonuniform", + () => { + const mesh = new Mesh(new PlaneGeometry(8, 8), new MeshBasicMaterial()); + mesh.scale.set(1, 2, 1); + return mesh; + }, + ], + [ + "mirrored", + () => { + const mesh = new Mesh(new PlaneGeometry(8, 8), new MeshBasicMaterial()); + mesh.scale.x = -1; + return mesh; + }, + ], + [ + "singular", + () => { + const mesh = new Mesh(new PlaneGeometry(8, 8), new MeshBasicMaterial()); + mesh.scale.x = 0; + return mesh; + }, + ], + ])("refuses %s receivers before recycling a live slot", (_name, makeReceiver) => { + const { root, receiver, field, slots } = fixture(1); + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { depth: 0.2 }); + const existing = slots[0]?.geometry; + const unsupported = makeReceiver(); + root.add(unsupported); + expect(() => + field.project(unsupported, unsupported.geometry, new Vector3(), normal, "stone", { + depth: 0.2, + }), + ).toThrow(/unsupported receiver/i); + expect(slots[0]?.geometry).toBe(existing); + expect(field.placed).toBe(1); + field.dispose(); + }); + + it("does not consume a slot on a miss, after disposal, or with an invalid projector", () => { + const { receiver, field, slots } = fixture(1); + const original = slots[0]?.geometry; + expect( + field.project(receiver, receiver.geometry, new Vector3(20, 0, 0), normal, "stone", { + depth: 0.2, + }), + ).toBe(false); + expect(() => + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { depth: 0 }), + ).toThrow(/projector/i); + expect(() => + field.project(receiver, receiver.geometry, new Vector3(), new Vector3(), "stone", { + depth: 0.2, + }), + ).toThrow(/projector/i); + expect(() => + field.project(receiver, receiver.geometry, new Vector3(), new Vector3(1e308, 0, 0), "stone", { + depth: 0.2, + }), + ).toThrow(/projector/i); + expect(field.placed).toBe(0); + expect(slots[0]?.geometry).toBe(original); + field.dispose(); + expect( + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { depth: 0.2 }), + ).toBe(false); + }); + + it("refuses nonuniform ancestor scale and shear", () => { + const { root, receiver, field } = fixture(); + const parent = new Group(); + root.add(parent); + parent.add(receiver); + parent.scale.set(1, 2, 1); + expect(() => + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { depth: 0.2 }), + ).toThrow(/unsupported receiver/i); + parent.scale.setScalar(1); + receiver.matrixAutoUpdate = false; + receiver.matrix.makeShear(0.5, 0, 0, 0, 0, 0); + expect(() => + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { depth: 0.2 }), + ).toThrow(/unsupported receiver/i); + field.dispose(); + }); + + it("explicit receiver cleanup does not retire another receiver's marks", () => { + const { root, receiver, field, slots } = fixture(); + const other = new Mesh(new PlaneGeometry(8, 8), new MeshBasicMaterial()); + root.add(other); + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { depth: 0.2 }); + field.project(other, other.geometry, new Vector3(), normal, "stone", { depth: 0.2 }); + const retained = slots[1]?.geometry; + field.removeReceiver(receiver); + field.removeReceiver(receiver); + expect(receiver.children).toHaveLength(0); + expect(other.children).toHaveLength(1); + expect(slots[1]?.geometry).toBe(retained); + field.dispose(); + }); +}); diff --git a/packages/create-threenative/templates/shooter/src/render/decals.ts b/packages/create-threenative/templates/shooter/src/render/decals.ts index fe747db62f..4e70c61fa9 100644 --- a/packages/create-threenative/templates/shooter/src/render/decals.ts +++ b/packages/create-threenative/templates/shooter/src/render/decals.ts @@ -1,6 +1,9 @@ import { + type BufferGeometry, DataTexture, DoubleSide, + Euler, + Matrix4, Mesh, MeshBasicMaterial, type Object3D, @@ -11,6 +14,7 @@ import { UnsignedByteType, Vector3, } from "three"; +import { DecalGeometry } from "three/addons/geometries/DecalGeometry.js"; /** * Bullet holes, and any other flat mark a game wants to leave stuck to a wall. @@ -124,9 +128,101 @@ const scratchQuaternion = new Quaternion(); const scratchRoll = new Quaternion(); const scratchNormal = new Vector3(); +type DecalMesh = Mesh; + +/** Projection is deliberately limited to static opaque triangles under rigid/uniform transforms. */ +function validateReceiver(receiver: Mesh, geometry: BufferGeometry): void { + const kind = receiver as Mesh & { + isSkinnedMesh?: boolean; + isInstancedMesh?: boolean; + isBatchedMesh?: boolean; + }; + const materials = Array.isArray(receiver.material) ? receiver.material : [receiver.material]; + const unsupportedMaterial = materials.some((material) => { + const custom = material as typeof material & { + isShaderMaterial?: boolean; + displacementMap?: Texture | null; + positionNode?: unknown; + vertexNode?: unknown; + transmission?: number; + }; + return ( + material.transparent || + material.opacity !== 1 || + material.alphaTest > 0 || + material.alphaHash || + custom.isShaderMaterial || + custom.displacementMap != null || + custom.positionNode != null || + custom.vertexNode != null || + (custom.transmission ?? 0) > 0 + ); + }); + if ( + !receiver.isMesh || + kind.isSkinnedMesh || + kind.isInstancedMesh || + kind.isBatchedMesh || + receiver.userData.deforming === true || + unsupportedMaterial || + Object.values(geometry.morphAttributes).some((attributes) => attributes.length > 0) || + Object.values(receiver.geometry.morphAttributes).some((attributes) => attributes.length > 0) + ) { + throw new Error("Unsupported receiver: decals require a static, opaque, non-instanced mesh."); + } + receiver.updateWorldMatrix(true, false); + const matrix = receiver.matrixWorld; + const x = new Vector3().setFromMatrixColumn(matrix, 0); + const y = new Vector3().setFromMatrixColumn(matrix, 1); + const z = new Vector3().setFromMatrixColumn(matrix, 2); + const length = x.lengthSq(); + const tolerance = length * 1e-6; + if ( + !matrix.elements.every(Number.isFinite) || + matrix.determinant() <= 0 || + length === 0 || + Math.abs(y.lengthSq() - length) > tolerance || + Math.abs(z.lengthSq() - length) > tolerance || + Math.abs(x.dot(y)) > tolerance || + Math.abs(x.dot(z)) > tolerance || + Math.abs(y.dot(z)) > tolerance + ) { + throw new Error( + "Unsupported receiver: decals require a rigid or positive uniform-scale transform.", + ); + } + const position = geometry.getAttribute("position"); + const count = geometry.index?.count ?? position?.count ?? 0; + if ( + position === undefined || + position.itemSize !== 3 || + count % 3 !== 0 || + geometry.drawRange.start !== 0 || + geometry.drawRange.count !== Number.POSITIVE_INFINITY + ) { + throw new Error("Unsupported receiver: decals require complete triangle geometry."); + } +} + +/** Keep upstream clipping/UVs, but do not paint a thin receiver's reverse-facing triangles. */ +function frontFaces(geometry: BufferGeometry, normal: Vector3): number[] { + const position = geometry.getAttribute("position"); + const a = new Vector3(); + const b = new Vector3(); + const c = new Vector3(); + const indices: number[] = []; + for (let index = 0; index < position.count; index += 3) { + a.fromBufferAttribute(position, index); + b.fromBufferAttribute(position, index + 1).sub(a); + c.fromBufferAttribute(position, index + 2).sub(a); + if (b.cross(c).dot(normal) > 0) indices.push(index, index + 1, index + 2); + } + return indices; +} + /** One family of marks: its own material, its own slots, its own recycle cursor. */ type Variant = { - readonly slots: Mesh[]; + readonly slots: DecalMesh[]; readonly material: MeshBasicMaterial; cursor: number; }; @@ -134,6 +230,9 @@ type Variant = { export class DecalField { readonly #variants = new Map(); readonly #geometry: PlaneGeometry; + readonly #parent: Object3D; + readonly #size: number; + readonly #projected = new Map void }>(); readonly #offset: number; #placed = 0; #capacity = 0; @@ -157,6 +256,8 @@ export class DecalField { renderOrder?: number; }, ) { + this.#parent = parent; + this.#size = options.size; this.#offset = options.offset ?? 0.018; this.#geometry = new PlaneGeometry(options.size, options.size); for (const [name, tint] of Object.entries(options.tints) as [TVariant, number][]) { @@ -169,7 +270,7 @@ export class DecalField { side: DoubleSide, transparent: true, }); - const slots: Mesh[] = []; + const slots: DecalMesh[] = []; for (let index = 0; index < options.countPerVariant; index += 1) { const mesh = new Mesh(this.#geometry, material.clone()); // Present from the first frame at a size nothing can see, so this material's pipeline is @@ -230,6 +331,7 @@ export class DecalField { const mesh = family.slots[family.cursor % family.slots.length]; family.cursor += 1; if (mesh === undefined) return; + this.#releaseProjection(mesh); this.#placed += 1; scratchNormal.copy(normal).normalize(); scratchQuaternion.setFromUnitVectors(FORWARD, scratchNormal); @@ -245,6 +347,126 @@ export class DecalField { mesh.updateMatrixWorld(true); } + /** + * Opt-in projection: pass the existing hit object and its authored/base geometry from the game. + * `point`, `normal`, `depth` and the size multiplier are world-space. Returns false on a miss or + * absent slot. Only this receiver is clipped; nearby objects are never projected onto. The + * upstream corner/overlap distortion remains: keep depth narrow for thin or concave surfaces. + * Call `update` each frame to clean receivers removed through an ancestor subtree. + */ + project( + receiver: Mesh, + geometry: BufferGeometry, + point: Vector3, + normal: Vector3, + variant: TVariant, + options: { depth: number; scale?: number }, + ): boolean { + const family = this.#variants.get(variant); + const mesh = family?.slots[family.cursor % family.slots.length]; + if (family === undefined || mesh === undefined) return false; + const size = this.#size * (options.scale ?? 1); + if ( + ![ + point.x, + point.y, + point.z, + normal.x, + normal.y, + normal.z, + size, + options.depth, + this.#offset, + ].every(Number.isFinite) || + size <= 0 || + options.depth <= 0 || + !Number.isFinite(normal.lengthSq()) || + normal.lengthSq() === 0 + ) { + throw new Error( + "Invalid decal projector: finite point/normal and positive size/depth required.", + ); + } + validateReceiver(receiver, geometry); + if (!this.#contains(receiver)) + throw new Error("Unsupported receiver: mesh is outside the field's parent."); + const direction = normal.clone().normalize(); + const orientation = new Quaternion().setFromUnitVectors(FORWARD, direction); + orientation.multiply( + new Quaternion().setFromAxisAngle(FORWARD, ((this.#placed + 1) * 2.399) % (Math.PI * 2)), + ); + // A proxy borrows the authored geometry without ever swapping the receiver's render LOD. + const source = new Mesh(geometry, receiver.material); + source.matrixWorld.copy(receiver.matrixWorld); + const projected = new DecalGeometry( + source, + point, + new Euler().setFromQuaternion(orientation), + new Vector3(size, size, options.depth), + ); + const indices = frontFaces(projected, direction); + if (indices.length === 0) { + projected.dispose(); + return false; + } + projected.setIndex(indices); + projected.translate( + direction.x * this.#offset, + direction.y * this.#offset, + direction.z * this.#offset, + ); + projected.applyMatrix4(new Matrix4().copy(receiver.matrixWorld).invert()); + this.#releaseProjection(mesh); + mesh.geometry = projected; + mesh.position.set(0, 0, 0); + mesh.quaternion.identity(); + mesh.scale.setScalar(1); + mesh.visible = true; + receiver.add(mesh); + const removed = (): void => this.#releaseProjection(mesh); + this.#projected.set(mesh, { receiver, removed }); + receiver.addEventListener("removed", removed); + mesh.updateWorldMatrix(true, false); + family.cursor += 1; + this.#placed += 1; + return true; + } + + /** Release marks before a receiver is destroyed; its geometry and material remain borrowed. */ + removeReceiver(receiver: Mesh): void { + for (const [mesh, entry] of this.#projected) { + if (entry.receiver === receiver) this.#releaseProjection(mesh); + } + } + + /** A direct removal fires synchronously; this bounded sweep also observes removed ancestors. */ + update(): void { + for (const [mesh, entry] of this.#projected) { + if (!this.#contains(entry.receiver)) this.#releaseProjection(mesh); + } + } + + #contains(object: Object3D): boolean { + for (let current: Object3D | null = object; current !== null; current = current.parent) { + if (current === this.#parent) return true; + } + return false; + } + + #releaseProjection(mesh: DecalMesh): void { + const entry = this.#projected.get(mesh); + if (entry === undefined) return; + entry.receiver.removeEventListener("removed", entry.removed); + this.#projected.delete(mesh); + mesh.geometry.dispose(); + mesh.geometry = this.#geometry; + this.#parent.add(mesh); + mesh.position.set(0, 0, 0); + mesh.quaternion.identity(); + mesh.scale.setScalar(0.0001); + mesh.visible = false; + } + /** Registered as an entity so a scenario can assert that a round actually left a mark. */ debug(): { capacity: number; placed: number } { return { capacity: this.#capacity, placed: this.#placed }; @@ -254,6 +476,7 @@ export class DecalField { clear(): void { for (const family of this.#variants.values()) { for (const mesh of family.slots) { + this.#releaseProjection(mesh); mesh.scale.setScalar(0.0001); if (this.#settled) mesh.visible = false; } @@ -266,6 +489,7 @@ export class DecalField { this.#disposed = true; for (const family of this.#variants.values()) { for (const mesh of family.slots) { + this.#releaseProjection(mesh); mesh.removeFromParent(); mesh.material.dispose(); } From fb2c819ce9c4ae74853ee7dc0394a1dcac531aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Furtado?= <30300459+jonit-dev@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:35:03 -0700 Subject: [PATCH 04/12] docs(vq11): retain inspected hosted decal screenshot progress --- .../PRD-VQ-11-bounded-decals.md | 9 +- .../vq11-decals-36989297734/motion.png | Bin 0 -> 52465 bytes .../vq11-decals-36989297734/static.png | Bin 0 -> 52548 bytes .../vq11-decals-36989297734/summary.json | 186 ++++++++++++++++++ .../vq11-decals-36989297734/teardown.png | Bin 0 -> 30378 bytes 5 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 docs/verification/vq11-decals-36989297734/motion.png create mode 100644 docs/verification/vq11-decals-36989297734/static.png create mode 100644 docs/verification/vq11-decals-36989297734/summary.json create mode 100644 docs/verification/vq11-decals-36989297734/teardown.png diff --git a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md index 251da973a1..70200c8470 100644 --- a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md +++ b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md @@ -56,7 +56,7 @@ All proof paths below are **planned implementation targets**, not existing passi ### Phase 3 — Qualify the visible result -- [ ] The impact sequence covers the 256-mark cap, moving receiver and LOD boundary on browser WebGPU. proof: `node --import tsx packages/create-threenative/__tests__/fixtures/bounded-decals/verify.ts` / `.github/workflows/integration-decals.yml` — opt-in portable fixture and static/motion/teardown/restart scenarios are built and schema-validated; actual hosted captures are pending. +- [ ] The impact sequence covers the 256-mark cap, moving receiver and LOD boundary on browser WebGPU. proof: `node --import tsx packages/create-threenative/__tests__/fixtures/bounded-decals/verify.ts` / `.github/workflows/integration-decals.yml` — opt-in portable fixture and static/motion/teardown/restart scenarios are built and schema-validated; hosted run [36989297734](https://github.com/ThreeNativeHQ/threenative/actions/runs/36989297734) passed at `7396738b`; actual PNGs are retained and visually inspected below. Automatic visible-mark discrimination remains incomplete, so this gate stays open. - [ ] The same sequence runs on Linux native with matching pool/receiver observations and visible marks. proof: `node packages/playtest/dist/runner/cli.js packages/create-threenative/__tests__/fixtures/bounded-decals/motion.playtest.json --target desktop --executable ${VQ_NATIVE_EXECUTABLE:?}` — portable `native.ts` entry is authored, but its native bundle/service must be staged with `public/receiver.glb` and `public/assets.manifest.json` and actually executed before qualification. ## Acceptance criteria @@ -98,3 +98,10 @@ Update the phase boxes and this PRD only after the named proof runs. Record actu - Focused and adjacent validation: 6 files, 34 tests passed with `--maxWorkers 1` (projection, fixture, lifetime, template-runtime-cost, shooter-rig, world-environment-lifetime). Package typecheck passes. Changed-file Biome check passes with the existing texture-generator complexity warning and two fixture complexity warnings. No root aggregate build/test pass is claimed. - The portable real engine fixture builds through Vite, and all four scenarios pass the maintained schema validator. The local pixel-producing run reaches the maintained runner but fails closed because no usable X display/Xvfb is available. No local screenshot is claimed. - The hosted workflow uses the built public runner and retains actual reports/PNGs before result checks. Exact source SHA and adapter accompany each capture; software rendering is correctness-only. Error diagnostics or `TN_PLAYTEST_SOFTWARE_DEVICE_LOST` prevent qualification even if a runner report says pass. Actual screenshot progress must be inspected and attached to the PR, including incomplete gates, before any readiness claim. + +2026-10-02 actual hosted screenshot progress, source `7396738b33a2b3018a2e6235a0e59ca07620a324`: + +- [Run 36989297734](https://github.com/ThreeNativeHQ/threenative/actions/runs/36989297734) passed all four authored scenarios at 960 × 540 using WebGPU, Google SwiftShader. Artifact `11218713068` SHA-256: `f432d6d5ca32cc09ee0633d3c3560445dc01ebc6e13b0280fa657774d2842652`. Software-adapter correctness evidence only; no hardware performance claim. +- Exact unmodified captures: [static](../../verification/vq11-decals-36989297734/static.png), [rigid motion](../../verification/vq11-decals-36989297734/motion.png), [receiver teardown](../../verification/vq11-decals-36989297734/teardown.png). Both plates visibly carry marks, the right plate and its marks move together, and removing it leaves only the left plate/marks. [Raw summary](../../verification/vq11-decals-36989297734/summary.json) preserves capture provenance and observations: cap 256, 321 creations, 65 releases, 275,028 geometry bytes and 516 maximum reported draw calls; teardown leaves 128 marks/193 releases; restart reaches generation 2. +- The summary's historical `qualified: true` means the then-current verifier passed. Independent review found that counters and whole-frame nonblank pixels alone do not prove visible decals. These images are human-inspected progress; a same-pose visible-effect discriminator and hidden-decal negative control are being added before automated visible qualification. Phase 3 remains open. +- Native execution, authored atlas/fade controls, repeated projected-scene GPU residency, full acceptance and independent final review remain open. The PR stays draft. diff --git a/docs/verification/vq11-decals-36989297734/motion.png b/docs/verification/vq11-decals-36989297734/motion.png new file mode 100644 index 0000000000000000000000000000000000000000..d2af7564069eaad070707e573eeae900f8edde00 GIT binary patch literal 52465 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

(a?3S7bmQPQ4EE}gjhhA&+-`V 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 J6h1M0|6f^YE>r*j literal 0 HcmV?d00001 From 4cfc3c2024da1cb37533dc67fd07679e364c27f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Furtado?= <30300459+jonit-dev@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:44:48 -0700 Subject: [PATCH 05/12] test(vq11): discriminate visible decals from populated hidden slots --- .../PRD-VQ-11-bounded-decals.md | 9 +++- .../__tests__/fixtures/bounded-decals/game.ts | 5 +- .../__tests__/fixtures/bounded-decals/main.ts | 2 +- .../bounded-decals/motion.playtest.json | 24 +++++++++ .../bounded-decals/restart.playtest.json | 24 +++++++++ .../bounded-decals/static.playtest.json | 24 +++++++++ .../bounded-decals/teardown.playtest.json | 12 +++++ .../fixtures/bounded-decals/verify.ts | 35 +++++++++--- .../__tests__/shooter-decals-fixture.spec.ts | 53 +++++++++++++++++++ 9 files changed, 177 insertions(+), 11 deletions(-) diff --git a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md index 70200c8470..c33f48f5a7 100644 --- a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md +++ b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md @@ -71,7 +71,7 @@ Measure draw calls and geometry bytes at the cap. Batch only compatible marks wh ## Blocked on Skinned/deforming receivers require a separate demand-backed extension and are not silently treated as rigid meshes. -Browser/native visual qualification needs a runnable fixture and GPU/runtime lane. The current container has no `/dev/dri` or `/dev/kvm`; `node packages/playtest/dist/runner/cli.js doctor --text` exits 2 because `adb` is absent. These limits do not block the still-unimplemented CPU-testable projection, receiver and LOD work. +Linux-native qualification needs a runnable native host/display or device lane; the portable entry must be bundled and its assets staged before that run. The current container has no `/dev/dri` or `/dev/kvm`; the actual browser attempt fails without an X display/Xvfb. Hosted browser WebGPU now supplies actual frames, so this does not block browser qualification or still-doable atlas/fade and repeated lifecycle work. ## Completion record @@ -105,3 +105,10 @@ Update the phase boxes and this PRD only after the named proof runs. Record actu - Exact unmodified captures: [static](../../verification/vq11-decals-36989297734/static.png), [rigid motion](../../verification/vq11-decals-36989297734/motion.png), [receiver teardown](../../verification/vq11-decals-36989297734/teardown.png). Both plates visibly carry marks, the right plate and its marks move together, and removing it leaves only the left plate/marks. [Raw summary](../../verification/vq11-decals-36989297734/summary.json) preserves capture provenance and observations: cap 256, 321 creations, 65 releases, 275,028 geometry bytes and 516 maximum reported draw calls; teardown leaves 128 marks/193 releases; restart reaches generation 2. - The summary's historical `qualified: true` means the then-current verifier passed. Independent review found that counters and whole-frame nonblank pixels alone do not prove visible decals. These images are human-inspected progress; a same-pose visible-effect discriminator and hidden-decal negative control are being added before automated visible qualification. Phase 3 remains open. - Native execution, authored atlas/fade controls, repeated projected-scene GPU residency, full acceptance and independent final review remain open. The PR stays draft. + +2026-10-02 automatic visible-mark gate repair: + +- A regression evaluated the real retained teardown PNG against the static scenario: the old whole-frame nonblank gate incorrectly accepted the missing right receiver. Five regression cases failed before adding receiver-region assertions and pass afterward. The checks reuse maintained playtest pixel measurement/evaluation; no screenshot is generated or edited by unit tests. +- Each present receiver now has an interior ROI (left 270,185 / 170 × 125; right 550,190 / 95 × 90). At maximum luminance 0.35 the dark-pixel ratio must be between 0.01 and 0.20, excluding both empty bright plates and the dark background. Actual retained marked captures measure roughly 0.052–0.057. These thresholds are pinned before the new hosted run. +- The verifier also runs the same static fixture with projected materials hidden. Picking, projection buffers, slot counts and LOD remain active; only real mark rendering is disabled. The control must fail exactly both receiver dark-pixel assertions while all other assertions pass and no unrelated diagnostics appear. Actual hosted execution of this new control is pending; it is not yet claimed as passing proof. +- Fresh scoped validation: 39/39 tests pass across six focused/adjacent suites; package typecheck and real Vite fixture build pass. Changed-file lint passes with two existing fixture complexity warnings. Root aggregate checks are not rerun here. Phase and acceptance boxes remain unchanged pending the hosted control and remaining PRD work. diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts b/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts index 9da4bebca1..6879560a72 100644 --- a/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts @@ -16,7 +16,7 @@ import { playtest } from "../../../../core/dist/playtest.js"; import { DecalField, bulletHoleTexture } from "../../../templates/shooter/src/render/decals.js"; /** Portable, opt-in runtime proof. Neither the shooter entry nor its authored look is changed. */ -export function createDecalFixture() { +export function createDecalFixture(hideDecals = false) { let generation = 0; class DecalRoom extends Scene { #source: Object3D | undefined; @@ -88,6 +88,9 @@ export function createDecalFixture() { } const slot = receiver.children.at(-1); if (!(slot instanceof Mesh)) throw new Error("Decal fixture has no projected slot."); + // Negative control: preserve real picking, projection, allocations and observations, + // but suppress the material's actual draw. The pixel gate must reject this scene. + slot.material.visible = !hideDecals; created += 1; slot.geometry.addEventListener("dispose", () => { released += 1; diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/main.ts b/packages/create-threenative/__tests__/fixtures/bounded-decals/main.ts index 0447654e21..0ba37857a5 100644 --- a/packages/create-threenative/__tests__/fixtures/bounded-decals/main.ts +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/main.ts @@ -1,5 +1,5 @@ import { createDecalFixture } from "./game.js"; -const game = createDecalFixture(); +const game = createDecalFixture(new URLSearchParams(location.search).get("hideDecals") === "1"); void game .start() .then(() => { diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/motion.playtest.json b/packages/create-threenative/__tests__/fixtures/bounded-decals/motion.playtest.json index 687a7d9a19..969878b789 100644 --- a/packages/create-threenative/__tests__/fixtures/bounded-decals/motion.playtest.json +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/motion.playtest.json @@ -89,6 +89,30 @@ "height": 540, "minNonblankPixelRatio": 0.15 } + }, + { + "region": { + "x": 270, + "y": 185, + "width": 170, + "height": 125, + "minNonblankPixelRatio": 0.9, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "maxDarkPixelRatio": 0.2 + } + }, + { + "region": { + "x": 550, + "y": 190, + "width": 95, + "height": 90, + "minNonblankPixelRatio": 0.9, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "maxDarkPixelRatio": 0.2 + } } ] } diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/restart.playtest.json b/packages/create-threenative/__tests__/fixtures/bounded-decals/restart.playtest.json index 4bbf5da157..4b79543e1c 100644 --- a/packages/create-threenative/__tests__/fixtures/bounded-decals/restart.playtest.json +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/restart.playtest.json @@ -77,6 +77,30 @@ "height": 540, "minNonblankPixelRatio": 0.15 } + }, + { + "region": { + "x": 270, + "y": 185, + "width": 170, + "height": 125, + "minNonblankPixelRatio": 0.9, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "maxDarkPixelRatio": 0.2 + } + }, + { + "region": { + "x": 550, + "y": 190, + "width": 95, + "height": 90, + "minNonblankPixelRatio": 0.9, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "maxDarkPixelRatio": 0.2 + } } ] } diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/static.playtest.json b/packages/create-threenative/__tests__/fixtures/bounded-decals/static.playtest.json index f0a126467f..b070b1904c 100644 --- a/packages/create-threenative/__tests__/fixtures/bounded-decals/static.playtest.json +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/static.playtest.json @@ -65,6 +65,30 @@ "height": 540, "minNonblankPixelRatio": 0.15 } + }, + { + "region": { + "x": 270, + "y": 185, + "width": 170, + "height": 125, + "minNonblankPixelRatio": 0.9, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "maxDarkPixelRatio": 0.2 + } + }, + { + "region": { + "x": 550, + "y": 190, + "width": 95, + "height": 90, + "minNonblankPixelRatio": 0.9, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "maxDarkPixelRatio": 0.2 + } } ] } diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/teardown.playtest.json b/packages/create-threenative/__tests__/fixtures/bounded-decals/teardown.playtest.json index 4b985a1739..ba4e255b27 100644 --- a/packages/create-threenative/__tests__/fixtures/bounded-decals/teardown.playtest.json +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/teardown.playtest.json @@ -83,6 +83,18 @@ "height": 540, "minNonblankPixelRatio": 0.15 } + }, + { + "region": { + "x": 270, + "y": 185, + "width": 170, + "height": 125, + "minNonblankPixelRatio": 0.9, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "maxDarkPixelRatio": 0.2 + } } ] } diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/verify.ts b/packages/create-threenative/__tests__/fixtures/bounded-decals/verify.ts index 0b4a7ff7ff..6911753774 100644 --- a/packages/create-threenative/__tests__/fixtures/bounded-decals/verify.ts +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/verify.ts @@ -24,13 +24,14 @@ if (!process.argv.includes("--build-only")) { "bin/vite.js", ); const results = []; - for (const name of ["static", "motion", "teardown", "restart"]) { + for (const name of ["static", "motion", "teardown", "restart", "hidden-decals"]) { + const hidden = name === "hidden-decals"; const directory = join(artifacts, name); const report = await runStandalonePlaytest({ artifactDirectory: directory, projectPath: fixture, - scenarioPath: join(fixture, `${name}.playtest.json`), - url: "http://127.0.0.1:4173/", + scenarioPath: join(fixture, `${hidden ? "static" : name}.playtest.json`), + url: `http://127.0.0.1:4173/${hidden ? "?hideDecals=1" : ""}`, port: 0, server: { command: `${JSON.stringify(process.execPath)} ${JSON.stringify(vite)} preview --host 127.0.0.1 --port $PORT --strictPort --outDir ${JSON.stringify(site)}`, @@ -63,8 +64,7 @@ if (!process.argv.includes("--build-only")) { (diagnostic) => diagnostic.severity === "error" || diagnostic.code === "TN_PLAYTEST_SOFTWARE_DEVICE_LOST", ); - const qualified = - report.pass && + const observed = report.capture?.rendererKind === "webgpu" && Object.values(report.capture.adapter).some((value) => value.length > 0) && disallowedDiagnostics.length === 0 && @@ -81,25 +81,44 @@ if (!process.argv.includes("--build-only")) { measurement.maxDrawCalls > 0 && Number.isFinite(measurement.motionError) && measurement.motionError <= 1e-6; + const failedAssertions = report.assertionResults?.filter((assertion) => !assertion.pass); + const negativeControlPassed = + hidden && + !report.pass && + report.capture?.rendererKind === "webgpu" && + Object.values(report.capture.adapter).some((value) => value.length > 0) && + screenshotPresent && + // Require both receiver interiors to lose their mark pixels while every counter, + // LOD, runtime and whole-frame assertion still passes. A crash is not a control. + JSON.stringify(failedAssertions?.map(({ id }) => id).sort()) === + JSON.stringify(["visual.1.region.darkPixels", "visual.2.region.darkPixels"]) && + report.diagnostics.length === 2 && + report.diagnostics.every(({ code }) => code === "TN_PLAYTEST_REGION_DARK_PIXEL_RATIO_FAILED"); results.push({ name, sourceSha, - qualified, + qualified: !hidden && report.pass && observed, + ...(hidden ? { negativeControlPassed } : {}), measurement, capture: report.capture, screenshot: screenshotPresent ? screenshot : null, diagnostics: report.diagnostics, + visual: report.observations?.visual, }); await writeFile( join(artifacts, "summary.json"), `${JSON.stringify({ sourceSha, correctnessOnly: true, results }, null, 2)}\n`, ); } - if (results.some((result) => !result.qualified)) + if ( + results.some((result) => + result.name === "hidden-decals" ? !result.negativeControlPassed : !result.qualified, + ) + ) throw new Error( "VQ11 runtime qualification failed; retained frames are diagnostic only. Inspect artifacts/vq11-decals/summary.json.", ); console.info( - `VQ11_DECAL_PROOF ${JSON.stringify({ sourceSha, captures: results.length, correctnessOnly: true })}`, + `VQ11_DECAL_PROOF ${JSON.stringify({ sourceSha, captures: results.length, hiddenDecalsRejected: true, correctnessOnly: true })}`, ); } diff --git a/packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts b/packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts index be99222a4e..4484c97300 100644 --- a/packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts +++ b/packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts @@ -1,13 +1,66 @@ import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; import { Mesh, PerspectiveCamera, Scene, Vector2, Vector3 } from "three"; import { afterEach, expect, it, vi } from "vitest"; import { createAssetLoader } from "../../core/src/assets.js"; import { baseGeometryOf, updateModelLods } from "../../core/src/model-lod.js"; import { ScenePicker } from "../../core/src/picking.js"; +import { evaluateRichPlaytestAssertions } from "../../playtest/src/assertion-evaluators.js"; +import { screenshotObservations } from "../../playtest/src/runner/steps.js"; +import { loadPlaytestScenario } from "../../playtest/src/scenario.js"; import { DecalField, bulletHoleTexture } from "../templates/shooter/src/render/decals.js"; afterEach(() => vi.unstubAllGlobals()); +async function evaluateCapturedPixels(scenarioName: string, captureName: string) { + const fixture = fileURLToPath(new URL("./fixtures/bounded-decals/", import.meta.url)); + const loaded = await loadPlaytestScenario(fixture, `${scenarioName}.playtest.json`); + const scenario = { ...loaded, assert: { visual: loaded.assert?.visual } }; + const png = await readFile( + new URL( + `../../../docs/verification/vq11-decals-36989297734/${captureName}.png`, + import.meta.url, + ), + ); + return evaluateRichPlaytestAssertions({ + scenario, + report: { + diagnostics: [], + distance: 0, + entity: "", + expectMoved: false, + frames: 1, + trivialityOptOuts: [], + observations: { + console: [], + hud: {}, + network: [], + resources: {}, + visual: screenshotObservations(undefined, png, scenario), + }, + }, + }).assertions; +} + +it("rejects a real screenshot missing the right receiver despite a nonblank lit scene", async () => { + const assertions = await evaluateCapturedPixels("static", "teardown"); + expect(assertions.find(({ id }) => id === "visual.0.region")?.pass).toBe(true); + expect(assertions.filter(({ pass }) => !pass).map(({ id }) => id)).toEqual([ + "visual.2.region.maxDarkPixels", + ]); +}); + +it.each(["static", "motion", "teardown", "restart"])( + "recognizes visible marks inside the %s receiver regions of actual hosted captures", + async (name) => { + const assertions = await evaluateCapturedPixels(name, name === "restart" ? "static" : name); + expect(assertions.filter(({ id }) => id.endsWith(".region.darkPixels"))).toHaveLength( + name === "teardown" ? 1 : 2, + ); + expect(assertions.every(({ pass }) => pass)).toBe(true); + }, +); + it("projects real framework hits onto the fixture's authored surface after actual LOD selection", async () => { const glb = await readFile( new URL("./fixtures/bounded-decals/public/receiver.glb", import.meta.url), From e1566472ee439650c74109a92e7fabae1770f71a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Furtado?= <30300459+jonit-dev@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:53:32 -0700 Subject: [PATCH 06/12] docs(vq11): record verified visible marks and hidden-material control --- .../PRD-VQ-11-bounded-decals.md | 14 +- .../vq11-decals-36991562324/hidden-decals.png | Bin 0 -> 6813 bytes .../hidden-report.json | 786 ++++++++++++++++++ .../vq11-decals-36991562324/summary.json | 428 ++++++++++ 4 files changed, 1225 insertions(+), 3 deletions(-) create mode 100644 docs/verification/vq11-decals-36991562324/hidden-decals.png create mode 100644 docs/verification/vq11-decals-36991562324/hidden-report.json create mode 100644 docs/verification/vq11-decals-36991562324/summary.json diff --git a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md index c33f48f5a7..8127ec00a2 100644 --- a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md +++ b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md @@ -4,7 +4,7 @@ prd_contract: v1 # PRD-VQ-11 — Impact and environment decals use a bounded portable lifecycle -**Status:** PARTIAL — 2026-10-02. Owned-resource teardown and opt-in bounded receiver projection are CPU-verified. Authored fade/atlas qualification and browser/native visible-result gates remain open. +**Status:** PARTIAL — 2026-10-02. Owned-resource teardown and opt-in bounded receiver projection are CPU-verified. Browser visible-mark/hidden-material discrimination is verified; authored fade/atlas, repeated GPU residency and native gates remain open. **Batch:** [Visual quality execution batch](https://github.com/ThreeNativeHQ/threenative/blob/docs/visual-quality-batch-2026-10-01/docs/PRDs/batch-2026-10-01-visual-quality/README.md). **Wave:** 2 / high-payoff detail. **Dependencies:** Independent. Share receiver/base-geometry selection with existing LOD and picking paths. @@ -56,7 +56,7 @@ All proof paths below are **planned implementation targets**, not existing passi ### Phase 3 — Qualify the visible result -- [ ] The impact sequence covers the 256-mark cap, moving receiver and LOD boundary on browser WebGPU. proof: `node --import tsx packages/create-threenative/__tests__/fixtures/bounded-decals/verify.ts` / `.github/workflows/integration-decals.yml` — opt-in portable fixture and static/motion/teardown/restart scenarios are built and schema-validated; hosted run [36989297734](https://github.com/ThreeNativeHQ/threenative/actions/runs/36989297734) passed at `7396738b`; actual PNGs are retained and visually inspected below. Automatic visible-mark discrimination remains incomplete, so this gate stays open. +- [x] The impact sequence covers the 256-mark cap, moving receiver and LOD boundary on browser WebGPU. proof: `node --import tsx packages/create-threenative/__tests__/fixtures/bounded-decals/verify.ts` / `.github/workflows/integration-decals.yml` — [run 36991562324](https://github.com/ThreeNativeHQ/threenative/actions/runs/36991562324) passed at `4cfc3c20`, 960 × 540 Google SwiftShader WebGPU; four actual captures satisfy receiver-region pixel checks and pool/motion/LOD observations. The hidden-material control retains 256 live slots but fails exactly both mark-pixel assertions. Correctness only; native and hardware performance are not qualified. - [ ] The same sequence runs on Linux native with matching pool/receiver observations and visible marks. proof: `node packages/playtest/dist/runner/cli.js packages/create-threenative/__tests__/fixtures/bounded-decals/motion.playtest.json --target desktop --executable ${VQ_NATIVE_EXECUTABLE:?}` — portable `native.ts` entry is authored, but its native bundle/service must be staged with `public/receiver.glb` and `public/assets.manifest.json` and actually executed before qualification. ## Acceptance criteria @@ -110,5 +110,13 @@ Update the phase boxes and this PRD only after the named proof runs. Record actu - A regression evaluated the real retained teardown PNG against the static scenario: the old whole-frame nonblank gate incorrectly accepted the missing right receiver. Five regression cases failed before adding receiver-region assertions and pass afterward. The checks reuse maintained playtest pixel measurement/evaluation; no screenshot is generated or edited by unit tests. - Each present receiver now has an interior ROI (left 270,185 / 170 × 125; right 550,190 / 95 × 90). At maximum luminance 0.35 the dark-pixel ratio must be between 0.01 and 0.20, excluding both empty bright plates and the dark background. Actual retained marked captures measure roughly 0.052–0.057. These thresholds are pinned before the new hosted run. -- The verifier also runs the same static fixture with projected materials hidden. Picking, projection buffers, slot counts and LOD remain active; only real mark rendering is disabled. The control must fail exactly both receiver dark-pixel assertions while all other assertions pass and no unrelated diagnostics appear. Actual hosted execution of this new control is pending; it is not yet claimed as passing proof. +- The verifier also runs the same static fixture with projected materials hidden. Picking, projection buffers, slot counts and LOD remain active; only real mark rendering is disabled. The control must fail exactly both receiver dark-pixel assertions while all other assertions pass and no unrelated diagnostics appear. At implementation commit `4cfc3c20`, hosted execution was pending; the result is recorded below. - Fresh scoped validation: 39/39 tests pass across six focused/adjacent suites; package typecheck and real Vite fixture build pass. Changed-file lint passes with two existing fixture complexity warnings. Root aggregate checks are not rerun here. Phase and acceptance boxes remain unchanged pending the hosted control and remaining PRD work. + +2026-10-02 visible-mark discrimination verified at `4cfc3c2024da1cb37533dc67fd07679e364c27f6`: + +- [Hosted run 36991562324](https://github.com/ThreeNativeHQ/threenative/actions/runs/36991562324) completed successfully, including all four real marked scenarios and the fifth hidden-material control. Artifact `11218994493` SHA-256 `a84190c40b3cdd6679ff9fea858dd6cfe2373646f090192ee531411fbaba1070` was independently verified after download. [Raw summary](../../verification/vq11-decals-36991562324/summary.json) records the exact source, 960 × 540 viewport, Google SwiftShader WebGPU adapter, diagnostics and pixel measurements. +- Both marked receiver interiors measure dark-pixel ratios 0.0534 and 0.0566 in the static frame (moving right receiver 0.0518), passing the pinned 0.01–0.20 bounds. The [actual hidden-material frame](../../verification/vq11-decals-36991562324/hidden-decals.png) has the same lit plates and zero mark pixels in both regions. Its [raw report](../../verification/vq11-decals-36991562324/hidden-report.json) fails exactly `visual.1.region.darkPixels` and `visual.2.region.darkPixels`; all remaining assertions pass with 256 slots, 321 placements and real LOD selection. The expected two pixel errors are the only diagnostics; positive runs have none, and none report device loss. +- All five PNGs were visually inspected. Static, motion, teardown and restart PNGs are byte-identical to the retained prior captures (restart equals static); the existing [static](../../verification/vq11-decals-36989297734/static.png), [motion](../../verification/vq11-decals-36989297734/motion.png) and [teardown](../../verification/vq11-decals-36989297734/teardown.png) files therefore also preserve the exact new-run bytes without duplicate binaries. +- Browser Phase 3 box is now verified. Phase 2 atlas/fade controls, repeated scene GPU residency, Linux-native execution and both full acceptance criteria remain open. Required general CI is skipped while draft; this is not a full-CI or hardware-performance pass. +- Proof-attachment doc checks: 2,405 links pass; the six-suite documentation run has 178 passes and two environment-blocked CLI tests (`evidence-budget`: long record and many small files). Both subprocesses fail at the `tsx` Unix IPC socket with `EPERM`, before evaluating their fixtures. The actual staged tree passes `node --import tsx scripts/check-evidence-budget.ts`; no aggregate documentation-suite pass is claimed. diff --git a/docs/verification/vq11-decals-36991562324/hidden-decals.png b/docs/verification/vq11-decals-36991562324/hidden-decals.png new file mode 100644 index 0000000000000000000000000000000000000000..6c43259ac7c6fff2600b09c29fc8503345e510ba GIT binary patch literal 6813 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 literal 0 HcmV?d00001 diff --git a/docs/verification/vq11-decals-36991562324/hidden-report.json b/docs/verification/vq11-decals-36991562324/hidden-report.json new file mode 100644 index 0000000000..064ce5758d --- /dev/null +++ b/docs/verification/vq11-decals-36991562324/hidden-report.json @@ -0,0 +1,786 @@ +{ + "sourceSha": "4cfc3c2024da1cb37533dc67fd07679e364c27f6", + "capture": { + "adapter": { + "architecture": "swiftshader", + "vendor": "google", + "features": "bgra8unorm-storage,chromium-experimental-timestamp-query-inside-passes,clip-distances,core-features-and-limits,depth-clip-control,depth32float-stencil8,float32-blendable,float32-filterable,indirect-first-instance,rg11b10ufloat-renderable,subgroup-size-control,subgroups,texture-component-swizzle,texture-compression-astc,texture-compression-astc-sliced-3d,texture-compression-bc,texture-compression-bc-sliced-3d,texture-compression-etc2,timestamp-query", + "limit.maxBindGroups": "4", + "limit.maxTextureDimension2D": "8192", + "limit.maxStorageBufferBindingSize": "1073741824" + }, + "browserArgs": [ + "--ozone-platform=x11", + "--enable-unsafe-webgpu", + "--disable-gpu-sandbox", + "--ignore-gpu-blocklist", + "--enable-features=Vulkan" + ], + "captureMethod": "page.screenshot", + "rendererKind": "webgpu", + "target": "web", + "viewport": { + "height": 540, + "width": 960 + } + }, + "diagnostics": [ + { + "code": "TN_PLAYTEST_REGION_DARK_PIXEL_RATIO_FAILED", + "message": "Screenshot region at (270, 185) contained 0 dark pixels, below required ratio 0.01.", + "severity": "error", + "suggestion": "Check whether the expected foreground silhouette occupies the asserted raster region." + }, + { + "code": "TN_PLAYTEST_REGION_DARK_PIXEL_RATIO_FAILED", + "message": "Screenshot region at (550, 190) contained 0 dark pixels, below required ratio 0.01.", + "severity": "error", + "suggestion": "Check whether the expected foreground silhouette occupies the asserted raster region." + } + ], + "diagnosticsPolicy": { + "noConsoleErrors": true, + "noNetworkErrors": true, + "noRuntimeDiagnostics": true, + "runtimeReady": true + }, + "distance": 0, + "entity": "", + "expectMoved": false, + "frames": 90, + "trivialityOptOuts": [ + { + "id": "component.decals.active.value", + "reason": "Real projected marks survive more than 256 impacts and eviction." + }, + { + "id": "component.decals.geometryBytes.value", + "reason": "Measures actual projected GPU-upload buffer bytes, not an empty field." + }, + { + "id": "component.decals.renderTriangles.value", + "reason": "The loaded 128-triangle authored receiver actually selects its two-triangle LOD." + }, + { + "id": "component.decals.generation.value", + "reason": "The restart input must construct a new scene; unchanged initial state is not evidence." + } + ], + "target": "web", + "observations": { + "components": { + "decals": { + "generation": { + "before": 1, + "after": 1 + }, + "active": { + "before": 256, + "after": 256 + }, + "capacity": { + "before": 256, + "after": 256 + }, + "created": { + "before": 320, + "after": 321 + }, + "released": { + "before": 64, + "after": 65 + }, + "geometryBytes": { + "before": 259896, + "after": 259794 + }, + "maxDrawCalls": { + "before": 0, + "after": 4 + }, + "motionError": { + "before": 0, + "after": 0 + }, + "receiverMotion": { + "before": 0, + "after": 0 + }, + "lodHit": { + "before": false, + "after": true + }, + "renderTriangles": { + "before": 2, + "after": 2 + }, + "receiverRemoved": { + "before": false, + "after": false + } + } + }, + "componentSeries": [ + { + "label": "lod-and-pool", + "snapshots": { + "decals": { + "generation": 1, + "active": 256, + "capacity": 256, + "created": 321, + "released": 65, + "geometryBytes": 259794, + "maxDrawCalls": 4, + "motionError": 0, + "receiverMotion": 0, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": false + } + }, + "tick": 150 + } + ], + "console": [ + { + "source": "browser-console", + "text": "TN_RENDER_PROJECTION:{\"projecting\":false,\"reasonCode\":\"belowMeshFloor\",\"reason\":\"fewer than 200 batchable meshes; the mirror would cost more than it saves\",\"sourceRenderables\":0,\"resultDrawCandidates\":0,\"batches\":0,\"instancedBatches\":0,\"materialBatches\":0,\"skinnedBatches\":0,\"projectedObjects\":0,\"exactObjects\":0,\"exact\":{},\"materialChecks\":\"spread\",\"materialChecksPerFrame\":512,\"materialCheckStaleFrames\":0}", + "type": "info" + }, + { + "source": "browser-console", + "text": "THREE.WebGPURenderer: Timestamp tracking is disabled.", + "type": "warning" + }, + { + "source": "browser-console", + "text": "TN_DECAL_FIXTURE:{\"generation\":1,\"active\":256,\"capacity\":256,\"created\":320,\"released\":64,\"geometryBytes\":259896,\"maxDrawCalls\":0,\"motionError\":0,\"receiverMotion\":0,\"lodHit\":false,\"renderTriangles\":128,\"receiverRemoved\":false}", + "type": "info" + }, + { + "source": "browser-console", + "text": "TN_DECAL_FIXTURE:{\"generation\":1,\"active\":256,\"capacity\":256,\"created\":320,\"released\":64,\"geometryBytes\":259896,\"maxDrawCalls\":0,\"motionError\":0,\"receiverMotion\":0,\"lodHit\":false,\"renderTriangles\":128,\"receiverRemoved\":false}", + "type": "info" + }, + { + "source": "browser-console", + "text": "TN_DECAL_FIXTURE:{\"generation\":1,\"active\":256,\"capacity\":256,\"created\":321,\"released\":65,\"geometryBytes\":259794,\"maxDrawCalls\":4,\"motionError\":0,\"receiverMotion\":0,\"lodHit\":true,\"renderTriangles\":2,\"receiverRemoved\":false}", + "type": "info" + }, + { + "source": "browser-console", + "text": "TN_DECAL_FIXTURE:{\"generation\":1,\"active\":256,\"capacity\":256,\"created\":321,\"released\":65,\"geometryBytes\":259794,\"maxDrawCalls\":4,\"motionError\":0,\"receiverMotion\":0,\"lodHit\":true,\"renderTriangles\":2,\"receiverRemoved\":false}", + "type": "info" + }, + { + "source": "browser-console", + "text": "TN_DECAL_FIXTURE:{\"generation\":1,\"active\":256,\"capacity\":256,\"created\":321,\"released\":65,\"geometryBytes\":259794,\"maxDrawCalls\":4,\"motionError\":0,\"receiverMotion\":0,\"lodHit\":true,\"renderTriangles\":2,\"receiverRemoved\":false}", + "type": "info" + } + ], + "hud": {}, + "network": [], + "pipelineCensus": { + "version": 1, + "complete": true, + "overflowed": false, + "unsupported": false, + "limit": 512, + "clock": { + "originMs": 212, + "source": "performance" + }, + "backend": { + "kind": "webgpu", + "identity": "webgpu:dN" + }, + "build": { + "identity": "@threenative/core@0.3.4" + }, + "adapter": { + "identity": "webgpu:architecture=swiftshader|vendor=google", + "thermal": "unavailable" + }, + "firstPresent": { + "boundaryMs": 66, + "eventsSettled": 1 + }, + "counts": { + "lookups": 3, + "creations": 3, + "failures": 0, + "pending": 0, + "uniquePrograms": 2, + "uniquePipelines": 3, + "recordedEvents": 3, + "droppedEvents": 0, + "deviceCreations": 3, + "directCreations": 0 + }, + "events": [ + { + "sequence": 1, + "programIdentity": "00fc0a8ad03630cc/458120a84b226496", + "pipelineIdentity": "backend-pipeline-1", + "kind": "render", + "pass": "output", + "mode": "sync", + "status": "created", + "vertex": { + "bytes": 1299, + "hash": "00fc0a8ad03630cc" + }, + "fragment": { + "bytes": 1595, + "hash": "458120a84b226496" + }, + "provenance": { + "material": { + "id": 15, + "name": "outputColorTransform", + "type": "NodeMaterial" + }, + "object": { + "id": 17, + "name": "Output Color Transform", + "uuid": "877e2c7e-ca92-4a57-9951-fca4484531a2" + }, + "unknown": false + }, + "reasons": [ + "unknown-structural-difference" + ], + "startedMs": 64.39999999999418, + "settledMs": 65.39999999999418, + "serviceMs": 1, + "beforeFirstPresent": true + }, + { + "sequence": 2, + "programIdentity": "2eb07d0bdc0da0c6/e792f11cd4d28ee0", + "pipelineIdentity": "backend-pipeline-2", + "kind": "render", + "pass": "main-clipped", + "mode": "sync", + "status": "created", + "vertex": { + "bytes": 1953, + "hash": "2eb07d0bdc0da0c6" + }, + "fragment": { + "bytes": 14203, + "hash": "e792f11cd4d28ee0" + }, + "provenance": { + "material": { + "id": 18, + "type": "MeshStandardMaterial" + }, + "object": { + "id": 20, + "name": "mesh_0", + "uuid": "62353851-9af3-4c64-8c92-22dc5af3cb64" + }, + "unknown": false + }, + "reasons": [ + "unknown-structural-difference" + ], + "startedMs": 272.0999999999767, + "settledMs": 272.19999999998254, + "serviceMs": 0.10000000000582077, + "beforeFirstPresent": false + }, + { + "sequence": 3, + "programIdentity": "2eb07d0bdc0da0c6/e792f11cd4d28ee0", + "pipelineIdentity": "backend-pipeline-3", + "kind": "render", + "pass": "main-clipped", + "mode": "sync", + "status": "created", + "vertex": { + "bytes": 1953, + "hash": "2eb07d0bdc0da0c6" + }, + "fragment": { + "bytes": 14203, + "hash": "e792f11cd4d28ee0" + }, + "provenance": { + "material": { + "id": 20, + "type": "MeshStandardMaterial" + }, + "object": { + "id": 22, + "uuid": "1542e84a-b64e-4a20-9976-3cc960edc945" + }, + "unknown": false + }, + "reasons": [ + "unknown-structural-difference" + ], + "startedMs": 289.0999999999767, + "settledMs": 289.19999999998254, + "serviceMs": 0.10000000000582077, + "beforeFirstPresent": false + } + ], + "incompleteReasons": [] + }, + "resources": { + "state": { + "before": {}, + "after": {} + }, + "GameState": { + "before": {}, + "after": {} + }, + "assets": { + "before": { + "receiver": { + "glb": { + "url": "receiver.glb", + "via": "manifest" + } + } + }, + "after": { + "receiver": { + "glb": { + "url": "receiver.glb", + "via": "manifest" + } + } + } + } + }, + "resourceSeries": [ + { + "label": "lod-and-pool", + "snapshots": { + "state": {}, + "GameState": {}, + "assets": { + "receiver": { + "glb": { + "url": "receiver.glb", + "via": "manifest" + } + } + } + }, + "tick": 150 + } + ], + "runtimeDiagnostics": { + "recentRuntimeErrors": [], + "runtimeReadouts": [], + "scene": { + "renderedEntities": [] + } + }, + "runtimeDiagnosticsBefore": { + "recentRuntimeErrors": [], + "runtimeReadouts": [], + "scene": { + "renderedEntities": [] + } + }, + "runtimeObservations": { + "gameplayBefore": { + "animation": {}, + "contacts": [], + "states": {}, + "tags": {}, + "transitions": [], + "audio": { + "cues": {}, + "paused": 0, + "pooled": 0, + "queued": 0, + "recentCues": [], + "unsupported": [], + "voices": 0 + }, + "world": { + "seed": 11, + "runtime": { + "agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36", + "core": "0.3.4", + "randomState": 11, + "rapier": null, + "step": 0.016666666666666666 + } + } + }, + "gameplay": { + "animation": {}, + "contacts": [], + "states": {}, + "tags": {}, + "transitions": [], + "audio": { + "cues": {}, + "paused": 0, + "pooled": 0, + "queued": 0, + "recentCues": [], + "unsupported": [], + "voices": 0 + }, + "world": { + "seed": 11, + "runtime": { + "agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36", + "core": "0.3.4", + "randomState": 11, + "rapier": null, + "step": 0.016666666666666666 + } + } + }, + "gameplaySeries": [ + { + "gameplay": { + "animation": {}, + "contacts": [], + "states": {}, + "tags": {}, + "transitions": [], + "audio": { + "cues": {}, + "paused": 0, + "pooled": 0, + "queued": 0, + "recentCues": [], + "unsupported": [], + "voices": 0 + }, + "world": { + "seed": 11, + "runtime": { + "agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36", + "core": "0.3.4", + "randomState": 11, + "rapier": null, + "step": 0.016666666666666666 + } + } + }, + "label": "lod-and-pool", + "tick": 150 + } + ] + }, + "scene": { + "background": "color:#17232f", + "camera": { + "far": 100, + "forward": [ + 0, + -0.18032786885245905, + -0.9836065573770492 + ], + "fov": 48, + "near": 0.1, + "position": [ + 0, + 4.5, + 12 + ], + "type": "PerspectiveCamera" + }, + "lights": [ + { + "color": "#ffefd2", + "intensity": 3, + "type": "DirectionalLight", + "visible": true + }, + { + "color": "#dbeaff", + "intensity": 1.5, + "type": "AmbientLight", + "visible": true + } + ], + "materials": { + "MeshStandardMaterial": 3, + "MeshBasicMaterial": 256 + }, + "objects": 262, + "truncated": false, + "worldExtent": { + "max": [ + 7.5, + 4.4, + 5 + ], + "min": [ + -7.5, + -0.17500000298023224, + -5 + ] + } + }, + "signals": [], + "signalSeries": [ + { + "label": "lod-and-pool", + "signals": [], + "tick": 150 + } + ], + "startup": { + "phase": "ready", + "progress": 1, + "compileSettled": true, + "timeline": { + "loadStartedMs": 228.19999999998254, + "enteredMs": 428, + "compileSettledMs": 926.3999999999942, + "frameworkReadyMs": 926.3999999999942, + "readyMs": 926.3999999999942 + }, + "rule": "compile-settled" + }, + "visual": { + "changedPixelRatio": 0, + "comparisonSource": "before-after", + "nonblankRegions": [ + { + "height": 540, + "width": 960, + "x": 0, + "y": 0, + "minNonblankPixelRatio": 0.15, + "darkPixelRatio": 0.5417457561728395, + "nonblankPixelRatio": 1 + }, + { + "height": 125, + "width": 170, + "x": 270, + "y": 185, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0, + "nonblankPixelRatio": 1 + }, + { + "height": 90, + "width": 95, + "x": 550, + "y": 190, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0, + "nonblankPixelRatio": 1 + } + ] + } + }, + "artifactDirectory": "/home/runner/work/threenative/threenative/artifacts/vq11-decals/hidden-decals", + "assertionResults": [ + { + "id": "visual.0.region", + "pass": true, + "details": { + "after": true, + "expected": { + "equals": true + }, + "minimum": 0.15, + "observed": 1 + } + }, + { + "id": "visual.1.region", + "pass": true, + "details": { + "after": true, + "expected": { + "equals": true + }, + "minimum": 0.9, + "observed": 1 + } + }, + { + "id": "visual.1.region.darkPixels", + "pass": false, + "details": { + "maximumLuminance": 0.35, + "minimumDarkPixelRatio": 0.01, + "observedDarkPixelRatio": 0 + } + }, + { + "id": "visual.1.region.maxDarkPixels", + "pass": true, + "details": { + "maximumDarkPixelRatio": 0.2, + "observedDarkPixelRatio": 0 + } + }, + { + "id": "visual.2.region", + "pass": true, + "details": { + "after": true, + "expected": { + "equals": true + }, + "minimum": 0.9, + "observed": 1 + } + }, + { + "id": "visual.2.region.darkPixels", + "pass": false, + "details": { + "maximumLuminance": 0.35, + "minimumDarkPixelRatio": 0.01, + "observedDarkPixelRatio": 0 + } + }, + { + "id": "visual.2.region.maxDarkPixels", + "pass": true, + "details": { + "maximumDarkPixelRatio": 0.2, + "observedDarkPixelRatio": 0 + } + }, + { + "details": { + "after": 256, + "before": 256, + "component": "active", + "entity": "decals", + "expected": { + "component": "active", + "entity": "decals", + "allowTrivial": "Real projected marks survive more than 256 impacts and eviction.", + "equals": 256 + }, + "trivial": true, + "trivialityOptOut": true + }, + "id": "component.decals.active.value", + "pass": true + }, + { + "details": { + "after": 321, + "before": 320, + "component": "created", + "entity": "decals", + "expected": { + "component": "created", + "entity": "decals", + "allowTrivial": "320 initial marks plus a real post-LOD raycast hit.", + "gte": 321 + }, + "trivial": false + }, + "id": "component.decals.created.value", + "pass": true + }, + { + "details": { + "after": 259794, + "before": 259896, + "component": "geometryBytes", + "entity": "decals", + "expected": { + "component": "geometryBytes", + "entity": "decals", + "allowTrivial": "Measures actual projected GPU-upload buffer bytes, not an empty field.", + "gte": 1 + }, + "trivial": true, + "trivialityOptOut": true + }, + "id": "component.decals.geometryBytes.value", + "pass": true + }, + { + "details": { + "after": 2, + "before": 2, + "component": "renderTriangles", + "entity": "decals", + "expected": { + "component": "renderTriangles", + "entity": "decals", + "allowTrivial": "The loaded 128-triangle authored receiver actually selects its two-triangle LOD.", + "equals": 2 + }, + "trivial": true, + "trivialityOptOut": true + }, + "id": "component.decals.renderTriangles.value", + "pass": true + }, + { + "details": { + "after": 1, + "before": 1, + "component": "generation", + "entity": "decals", + "expected": { + "component": "generation", + "entity": "decals", + "allowTrivial": "The restart input must construct a new scene; unchanged initial state is not evidence.", + "equals": 1 + }, + "trivial": true, + "trivialityOptOut": true + }, + "id": "component.decals.generation.value", + "pass": true + }, + { + "details": { + "consoleErrors": 0, + "networkErrors": 0, + "policy": { + "noConsoleErrors": true, + "noNetworkErrors": true, + "noRuntimeDiagnostics": true, + "runtimeReady": true + }, + "runtimeDiagnostics": 0 + }, + "id": "diagnostics", + "pass": true + } + ], + "pass": false, + "runtime": "web", + "scenario": "vq11-static", + "startup": { + "phase": "ready", + "progress": 1, + "compileSettled": true, + "timeline": { + "loadStartedMs": 228.19999999998254, + "enteredMs": 428, + "compileSettledMs": 926.3999999999942, + "frameworkReadyMs": 926.3999999999942, + "readyMs": 926.3999999999942 + }, + "rule": "compile-settled" + }, + "trivialityOptOutCount": 4, + "url": "http://127.0.0.1:38363/?hideDecals=1" +} diff --git a/docs/verification/vq11-decals-36991562324/summary.json b/docs/verification/vq11-decals-36991562324/summary.json new file mode 100644 index 0000000000..036da3a007 --- /dev/null +++ b/docs/verification/vq11-decals-36991562324/summary.json @@ -0,0 +1,428 @@ +{ + "sourceSha": "4cfc3c2024da1cb37533dc67fd07679e364c27f6", + "correctnessOnly": true, + "results": [ + { + "name": "static", + "sourceSha": "4cfc3c2024da1cb37533dc67fd07679e364c27f6", + "qualified": true, + "measurement": { + "generation": 1, + "active": 256, + "capacity": 256, + "created": 321, + "released": 65, + "geometryBytes": 275076, + "maxDrawCalls": 516, + "motionError": 0, + "receiverMotion": 0, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": false + }, + "capture": { + "adapter": { + "architecture": "swiftshader", + "vendor": "google", + "features": "bgra8unorm-storage,chromium-experimental-timestamp-query-inside-passes,clip-distances,core-features-and-limits,depth-clip-control,depth32float-stencil8,float32-blendable,float32-filterable,indirect-first-instance,rg11b10ufloat-renderable,subgroup-size-control,subgroups,texture-component-swizzle,texture-compression-astc,texture-compression-astc-sliced-3d,texture-compression-bc,texture-compression-bc-sliced-3d,texture-compression-etc2,timestamp-query", + "limit.maxBindGroups": "4", + "limit.maxTextureDimension2D": "8192", + "limit.maxStorageBufferBindingSize": "1073741824" + }, + "browserArgs": [ + "--ozone-platform=x11", + "--enable-unsafe-webgpu", + "--disable-gpu-sandbox", + "--ignore-gpu-blocklist", + "--enable-features=Vulkan" + ], + "captureMethod": "page.screenshot", + "rendererKind": "webgpu", + "target": "web", + "viewport": { + "height": 540, + "width": 960 + } + }, + "screenshot": "/home/runner/work/threenative/threenative/artifacts/vq11-decals/static/after.png", + "diagnostics": [], + "visual": { + "changedPixelRatio": 0.00017939814814814815, + "comparisonSource": "before-after", + "nonblankRegions": [ + { + "height": 540, + "width": 960, + "x": 0, + "y": 0, + "minNonblankPixelRatio": 0.15, + "darkPixelRatio": 0.5420428240740741, + "nonblankPixelRatio": 0.9999710648148148 + }, + { + "height": 125, + "width": 170, + "x": 270, + "y": 185, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.05341176470588235, + "nonblankPixelRatio": 0.9994823529411765 + }, + { + "height": 90, + "width": 95, + "x": 550, + "y": 190, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.05660818713450293, + "nonblankPixelRatio": 1 + } + ] + } + }, + { + "name": "motion", + "sourceSha": "4cfc3c2024da1cb37533dc67fd07679e364c27f6", + "qualified": true, + "measurement": { + "generation": 1, + "active": 256, + "capacity": 256, + "created": 321, + "released": 65, + "geometryBytes": 275076, + "maxDrawCalls": 516, + "motionError": 0, + "receiverMotion": 0.8042230500706451, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": false + }, + "capture": { + "adapter": { + "architecture": "swiftshader", + "vendor": "google", + "features": "bgra8unorm-storage,chromium-experimental-timestamp-query-inside-passes,clip-distances,core-features-and-limits,depth-clip-control,depth32float-stencil8,float32-blendable,float32-filterable,indirect-first-instance,rg11b10ufloat-renderable,subgroup-size-control,subgroups,texture-component-swizzle,texture-compression-astc,texture-compression-astc-sliced-3d,texture-compression-bc,texture-compression-bc-sliced-3d,texture-compression-etc2,timestamp-query", + "limit.maxBindGroups": "4", + "limit.maxTextureDimension2D": "8192", + "limit.maxStorageBufferBindingSize": "1073741824" + }, + "browserArgs": [ + "--ozone-platform=x11", + "--enable-unsafe-webgpu", + "--disable-gpu-sandbox", + "--ignore-gpu-blocklist", + "--enable-features=Vulkan" + ], + "captureMethod": "page.screenshot", + "rendererKind": "webgpu", + "target": "web", + "viewport": { + "height": 540, + "width": 960 + } + }, + "screenshot": "/home/runner/work/threenative/threenative/artifacts/vq11-decals/motion/after.png", + "diagnostics": [], + "visual": { + "changedPixelRatio": 0.04210648148148148, + "comparisonSource": "before-after", + "nonblankRegions": [ + { + "height": 540, + "width": 960, + "x": 0, + "y": 0, + "minNonblankPixelRatio": 0.15, + "darkPixelRatio": 0.5506944444444445, + "nonblankPixelRatio": 0.9999729938271605 + }, + { + "height": 125, + "width": 170, + "x": 270, + "y": 185, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.05341176470588235, + "nonblankPixelRatio": 0.9994823529411765 + }, + { + "height": 90, + "width": 95, + "x": 550, + "y": 190, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.051812865497076026, + "nonblankPixelRatio": 0.9997660818713451 + } + ] + } + }, + { + "name": "teardown", + "sourceSha": "4cfc3c2024da1cb37533dc67fd07679e364c27f6", + "qualified": true, + "measurement": { + "generation": 1, + "active": 128, + "capacity": 256, + "created": 321, + "released": 193, + "geometryBytes": 136140, + "maxDrawCalls": 516, + "motionError": 0, + "receiverMotion": 0, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": true + }, + "capture": { + "adapter": { + "architecture": "swiftshader", + "vendor": "google", + "features": "bgra8unorm-storage,chromium-experimental-timestamp-query-inside-passes,clip-distances,core-features-and-limits,depth-clip-control,depth32float-stencil8,float32-blendable,float32-filterable,indirect-first-instance,rg11b10ufloat-renderable,subgroup-size-control,subgroups,texture-component-swizzle,texture-compression-astc,texture-compression-astc-sliced-3d,texture-compression-bc,texture-compression-bc-sliced-3d,texture-compression-etc2,timestamp-query", + "limit.maxBindGroups": "4", + "limit.maxTextureDimension2D": "8192", + "limit.maxStorageBufferBindingSize": "1073741824" + }, + "browserArgs": [ + "--ozone-platform=x11", + "--enable-unsafe-webgpu", + "--disable-gpu-sandbox", + "--ignore-gpu-blocklist", + "--enable-features=Vulkan" + ], + "captureMethod": "page.screenshot", + "rendererKind": "webgpu", + "target": "web", + "viewport": { + "height": 540, + "width": 960 + } + }, + "screenshot": "/home/runner/work/threenative/threenative/artifacts/vq11-decals/teardown/after.png", + "diagnostics": [], + "visual": { + "changedPixelRatio": 0.07619598765432099, + "comparisonSource": "before-after", + "nonblankRegions": [ + { + "height": 540, + "width": 960, + "x": 0, + "y": 0, + "minNonblankPixelRatio": 0.15, + "darkPixelRatio": 0.6017264660493827, + "nonblankPixelRatio": 0.9999787808641976 + }, + { + "height": 125, + "width": 170, + "x": 270, + "y": 185, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.05341176470588235, + "nonblankPixelRatio": 0.9994823529411765 + } + ] + } + }, + { + "name": "restart", + "sourceSha": "4cfc3c2024da1cb37533dc67fd07679e364c27f6", + "qualified": true, + "measurement": { + "generation": 2, + "active": 256, + "capacity": 256, + "created": 321, + "released": 65, + "geometryBytes": 275028, + "maxDrawCalls": 516, + "motionError": 0, + "receiverMotion": 0, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": false + }, + "capture": { + "adapter": { + "architecture": "swiftshader", + "vendor": "google", + "features": "bgra8unorm-storage,chromium-experimental-timestamp-query-inside-passes,clip-distances,core-features-and-limits,depth-clip-control,depth32float-stencil8,float32-blendable,float32-filterable,indirect-first-instance,rg11b10ufloat-renderable,subgroup-size-control,subgroups,texture-component-swizzle,texture-compression-astc,texture-compression-astc-sliced-3d,texture-compression-bc,texture-compression-bc-sliced-3d,texture-compression-etc2,timestamp-query", + "limit.maxBindGroups": "4", + "limit.maxTextureDimension2D": "8192", + "limit.maxStorageBufferBindingSize": "1073741824" + }, + "browserArgs": [ + "--ozone-platform=x11", + "--enable-unsafe-webgpu", + "--disable-gpu-sandbox", + "--ignore-gpu-blocklist", + "--enable-features=Vulkan" + ], + "captureMethod": "page.screenshot", + "rendererKind": "webgpu", + "target": "web", + "viewport": { + "height": 540, + "width": 960 + } + }, + "screenshot": "/home/runner/work/threenative/threenative/artifacts/vq11-decals/restart/after.png", + "diagnostics": [], + "visual": { + "changedPixelRatio": 0.00017939814814814815, + "comparisonSource": "before-after", + "nonblankRegions": [ + { + "height": 540, + "width": 960, + "x": 0, + "y": 0, + "minNonblankPixelRatio": 0.15, + "darkPixelRatio": 0.5420428240740741, + "nonblankPixelRatio": 0.9999710648148148 + }, + { + "height": 125, + "width": 170, + "x": 270, + "y": 185, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.05341176470588235, + "nonblankPixelRatio": 0.9994823529411765 + }, + { + "height": 90, + "width": 95, + "x": 550, + "y": 190, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.05660818713450293, + "nonblankPixelRatio": 1 + } + ] + } + }, + { + "name": "hidden-decals", + "sourceSha": "4cfc3c2024da1cb37533dc67fd07679e364c27f6", + "qualified": false, + "negativeControlPassed": true, + "measurement": { + "generation": 1, + "active": 256, + "capacity": 256, + "created": 321, + "released": 65, + "geometryBytes": 259794, + "maxDrawCalls": 4, + "motionError": 0, + "receiverMotion": 0, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": false + }, + "capture": { + "adapter": { + "architecture": "swiftshader", + "vendor": "google", + "features": "bgra8unorm-storage,chromium-experimental-timestamp-query-inside-passes,clip-distances,core-features-and-limits,depth-clip-control,depth32float-stencil8,float32-blendable,float32-filterable,indirect-first-instance,rg11b10ufloat-renderable,subgroup-size-control,subgroups,texture-component-swizzle,texture-compression-astc,texture-compression-astc-sliced-3d,texture-compression-bc,texture-compression-bc-sliced-3d,texture-compression-etc2,timestamp-query", + "limit.maxBindGroups": "4", + "limit.maxTextureDimension2D": "8192", + "limit.maxStorageBufferBindingSize": "1073741824" + }, + "browserArgs": [ + "--ozone-platform=x11", + "--enable-unsafe-webgpu", + "--disable-gpu-sandbox", + "--ignore-gpu-blocklist", + "--enable-features=Vulkan" + ], + "captureMethod": "page.screenshot", + "rendererKind": "webgpu", + "target": "web", + "viewport": { + "height": 540, + "width": 960 + } + }, + "screenshot": "/home/runner/work/threenative/threenative/artifacts/vq11-decals/hidden-decals/after.png", + "diagnostics": [ + { + "code": "TN_PLAYTEST_REGION_DARK_PIXEL_RATIO_FAILED", + "message": "Screenshot region at (270, 185) contained 0 dark pixels, below required ratio 0.01.", + "severity": "error", + "suggestion": "Check whether the expected foreground silhouette occupies the asserted raster region." + }, + { + "code": "TN_PLAYTEST_REGION_DARK_PIXEL_RATIO_FAILED", + "message": "Screenshot region at (550, 190) contained 0 dark pixels, below required ratio 0.01.", + "severity": "error", + "suggestion": "Check whether the expected foreground silhouette occupies the asserted raster region." + } + ], + "visual": { + "changedPixelRatio": 0, + "comparisonSource": "before-after", + "nonblankRegions": [ + { + "height": 540, + "width": 960, + "x": 0, + "y": 0, + "minNonblankPixelRatio": 0.15, + "darkPixelRatio": 0.5417457561728395, + "nonblankPixelRatio": 1 + }, + { + "height": 125, + "width": 170, + "x": 270, + "y": 185, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0, + "nonblankPixelRatio": 1 + }, + { + "height": 90, + "width": 95, + "x": 550, + "y": 190, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0, + "nonblankPixelRatio": 1 + } + ] + } + } + ] +} From ce1c03426f68fb74d218be9b57b3876cf25442d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Furtado?= <30300459+jonit-dev@users.noreply.github.com> Date: Fri, 2 Oct 2026 04:59:32 -0700 Subject: [PATCH 07/12] docs(vq11): retain verified atlas fade captures and open residency failure --- .../PRD-VQ-11-bounded-decals.md | 11 +- .../vq11-decals-37003284983/atlas.png | Bin 0 -> 34274 bytes .../vq11-decals-37003284983/fading.png | Bin 0 -> 32169 bytes .../vq11-decals-37003284983/summary.json | 1071 +++++++++++++++++ 4 files changed, 1080 insertions(+), 2 deletions(-) create mode 100644 docs/verification/vq11-decals-37003284983/atlas.png create mode 100644 docs/verification/vq11-decals-37003284983/fading.png create mode 100644 docs/verification/vq11-decals-37003284983/summary.json diff --git a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md index 108d4e841a..e949ff8616 100644 --- a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md +++ b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md @@ -54,8 +54,8 @@ All proof paths below are **planned implementation targets**, not existing passi ### Phase 2 — Preserve authored appearance and receiver motion -- [ ] Add game-owned decal material/atlas composition with bounded bias and fade controls. proof: `node node_modules/vitest/vitest.mjs run --maxWorkers 1 packages/create-threenative/__tests__/shooter-decals-projection.spec.ts` plus the runtime fixture. - - Materials, tints, map, offset, projection depth and size remain game-authored. Optional atlas/fade controls and bounded projection bias are now implemented and CPU-verified; current runtime qualification is pending. +- [x] Add game-owned decal material/atlas composition with bounded bias and fade controls. proof: `node node_modules/vitest/vitest.mjs run --maxWorkers 1 packages/create-threenative/__tests__/shooter-decals-projection.spec.ts` plus the runtime fixture. + - Materials, tints, map, offset, projection depth and size remain game-authored. Optional atlas/fade controls and bounded projection bias are CPU-verified; actual atlas, visible partial fade, expiry and hidden-during-fade cases pass at `021e6223` in run 37003284983. - [ ] Verify rigid receiver motion, LOD changes and repeated scene teardown without detached marks or allocation growth. proof: `node node_modules/vitest/vitest.mjs run --maxWorkers 1 packages/create-threenative/__tests__/shooter-decals-projection.spec.ts packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts` plus the Phase 3 lifecycle scenario. - Receiver/ancestor rigid motion, positive uniform scale, direct and ancestor removal, explicit cleanup, legacy-slot reuse and actual loader/picker LOD independence are CPU-verified. Repeated projected scene resets and GPU residency still require the runtime fixture. - Partial, 2026-10-02: the lifetime regression exercises 20 field construction/teardown cycles, 2,000 pooled placements, clear/reuse, zero-slot teardown and repeated `dispose()`. Real Three disposal events/spies verify owned-resource release and retention of the borrowed map. Rigid receivers, LOD changes, GPU memory and game-scene transitions are not qualified by these unit tests. @@ -140,3 +140,10 @@ Update the phase boxes and this PRD only after the named proof runs. Record actu - Independent review reproduced a proof gap: the retained real hidden-material PNG passed all intermediate-fade visual assertions. A new regression reproduced that acceptance before the fix, then rejected exactly two missing positive-mark regions after it. The original full-opacity disappearance checks remain; two distinct inner regions now require intermediate-luminance mark coverage (left luminance ≤0.65, 1–30%; right ≤0.60, 10–65%). The real empty receiver PNG measures zero at both thresholds. Actual partially faded pixels remain pending the hosted run. - A tenth capture, `hidden-fading`, runs the same atlas/fading scenario but disables material rendering only when the fade input starts. Real opacity progression, geometry, pool and LOD observations stay active; the control must fail exactly the two positive intermediate-pixel assertions and have no unrelated diagnostics. The original four scenario JSON files are byte-identical to `e1566472`. - Fresh scoped checks after this correction: 54/54 tests, package typecheck, changed-fixture lint and Vite fixture build pass; all eight authored scenarios validate. The source atlas/fade arithmetic and ownership were independently reviewed without a blocker. Independent review also reproduced the starter Basis-transcoder failure on unchanged baseline `e1566472`; the temporary test scaffold lacks project-relative Three Basis files. That named gate remains failed; no production change or blanket waiver is made. + +2026-10-02 actual atlas/fade proof with an open residency failure: + +- [Run 37003284983](https://github.com/ThreeNativeHQ/threenative/actions/runs/37003284983), source `021e622380c61695b8c5dc0e7b9ffc604e956b8c`, captured all ten cases at 960 × 540 on Google SwiftShader WebGPU. Artifact `11225280829` SHA-256 `8fad5bcda560bedc2bb9899f747ba6e6e5aa250c50fccade4d89eca02ec586aa` was verified after download. [Raw summary](../../verification/vq11-decals-37003284983/summary.json) retains every verdict and diagnostic. +- [Atlas selection](../../verification/vq11-decals-37003284983/atlas.png) visibly separates the impact and environment-mark cells; [partial fade](../../verification/vq11-decals-37003284983/fading.png) visibly retains translucent marks at measured material opacity 0.5. Positive intermediate pixel coverage is 6.55% left and 43.44% right. Expiry removes all 256 live marks and releases all 321 created buffers; its exact PNG equals the retained [empty receiver frame](../../verification/vq11-decals-36991562324/hidden-decals.png). These real captures were inspected. +- Both hidden controls pass their required rejection checks. Hidden-during-fade retains opacity 0.5 and 256 slots but fails exactly the two positive intermediate-pixel assertions. Positive appearance cases have no error/device-loss diagnostics. The original static/motion/removal/restart cases also pass. Authored appearance is now qualified on this software adapter, not native or hardware performance. +- The overall run fails the new residency case, which remains open: warmed samples alternate 258/259 geometries and 6,522,318/6,522,894 bytes, yielding +1 geometry/+576 bytes; textures stay at 4. The first sample sees only one geometry before its eventual 256-mark frame, demonstrating that 60 simulated update ticks do not guarantee world-render completion. Sampling/render timing and LOD transitions are under investigation. The zero-growth limits remain unchanged; this is not a full run pass. diff --git a/docs/verification/vq11-decals-37003284983/atlas.png b/docs/verification/vq11-decals-37003284983/atlas.png new file mode 100644 index 0000000000000000000000000000000000000000..e8bb3a716919aec83b32a2363f3b705f21fc5bda GIT binary patch literal 34274 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 Date: Fri, 2 Oct 2026 05:13:21 -0700 Subject: [PATCH 08/12] test(vq11): sample residency after completed stable world renders --- .../PRD-VQ-11-bounded-decals.md | 5 ++ .../__tests__/fixtures/bounded-decals/game.ts | 60 ++++++++++++--- .../bounded-decals/lifecycle.playtest.json | 74 ++++++++++++++----- .../__tests__/shooter-decals-fixture.spec.ts | 23 ++++++ 4 files changed, 133 insertions(+), 29 deletions(-) diff --git a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md index e949ff8616..f35e705f7d 100644 --- a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md +++ b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md @@ -147,3 +147,8 @@ Update the phase boxes and this PRD only after the named proof runs. Record actu - [Atlas selection](../../verification/vq11-decals-37003284983/atlas.png) visibly separates the impact and environment-mark cells; [partial fade](../../verification/vq11-decals-37003284983/fading.png) visibly retains translucent marks at measured material opacity 0.5. Positive intermediate pixel coverage is 6.55% left and 43.44% right. Expiry removes all 256 live marks and releases all 321 created buffers; its exact PNG equals the retained [empty receiver frame](../../verification/vq11-decals-36991562324/hidden-decals.png). These real captures were inspected. - Both hidden controls pass their required rejection checks. Hidden-during-fade retains opacity 0.5 and 256 slots but fails exactly the two positive intermediate-pixel assertions. Positive appearance cases have no error/device-loss diagnostics. The original static/motion/removal/restart cases also pass. Authored appearance is now qualified on this software adapter, not native or hardware performance. - The overall run fails the new residency case, which remains open: warmed samples alternate 258/259 geometries and 6,522,318/6,522,894 bytes, yielding +1 geometry/+576 bytes; textures stay at 4. The first sample sees only one geometry before its eventual 256-mark frame, demonstrating that 60 simulated update ticks do not guarantee world-render completion. Sampling/render timing and LOD transitions are under investigation. The zero-growth limits remain unchanged; this is not a full run pass. + +2026-10-02 residency sampling correction, awaiting runtime rerun: + +- Replace the simulated-tick sample with the proven completed-render boundary used by VQ07: observe a changed cumulative `renderer.info.render.calls` value on the next game update, count one interval regardless of internal passes, and require three consecutive stable geometry/texture/byte observations. Only accept the world at its actual projected-mark draw count with the real two-triangle LOD and post-LOD hit present. +- The lifecycle scenario uses bounded 30-second `waitForResource` waits for each of six sampled scene generations, allowing actual presentation between polls. A unit regression rejects 90 simulated ticks with no render-call advance, resets stability when counts change, and rejects missing/nonfinite memory. All 55 focused/adjacent tests, typecheck, fixture build, lint and lifecycle schema validation pass. Production decal code, the original four scenario files and zero-growth ceilings are unchanged. The prior +1/+576 failure is preserved above and is not yet declared fixed by browser evidence. diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts b/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts index 576b0eb17b..4aafbfd3b9 100644 --- a/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts @@ -18,11 +18,32 @@ import { type ICtx, Scene, baseGeometryOf, defineGame } from "../../../../core/d import { playtest } from "../../../../core/dist/playtest.js"; import { DecalField, bulletHoleTexture } from "../../../templates/shooter/src/render/decals.js"; +type ResidencySample = { renderCalls: number; geometries: number; textures: number; bytes: number }; + +/** The framework owns the loop: cumulative render calls, not info.frame or fixed ticks, advance. */ +export function observeResidency( + previous: ResidencySample & { stableFrames: number }, + current: ResidencySample, + ready: boolean, +): void { + if (!Object.values(current).every((value) => Number.isFinite(value) && value >= 0)) + throw new Error("Decal fixture renderer residency is unavailable."); + if (!ready) { + Object.assign(previous, current, { stableFrames: 0 }); + return; + } + if (current.renderCalls === previous.renderCalls) return; + const stable = + current.geometries === previous.geometries && + current.textures === previous.textures && + current.bytes === previous.bytes; + Object.assign(previous, current, { stableFrames: stable ? previous.stableFrames + 1 : 1 }); +} + /** Portable, opt-in runtime proof. Neither the shooter entry nor its authored look is changed. */ export function createDecalFixture(hideDecals = false, atlasFade = false, hideDuringFade = false) { let generation = 0; - const residency: { generation: number; geometries: number; textures: number; bytes: number }[] = - []; + const residency: (ResidencySample & { generation: number; stableFrames: number })[] = []; class DecalRoom extends Scene { #source: Object3D | undefined; #dispose = () => {}; @@ -34,6 +55,7 @@ export function createDecalFixture(hideDecals = false, atlasFade = false, hideDu override enter(ctx: ICtx) { generation += 1; + ctx.state.set({ residencyGeneration: 0 }); const source = this.#source?.getObjectByProperty("isMesh", true); if (!(source instanceof Mesh)) throw new Error("Decal fixture receiver did not load."); const camera = ctx.camera as PerspectiveCamera; @@ -139,6 +161,14 @@ export function createDecalFixture(hideDecals = false, atlasFade = false, hideDu let motionError = 0; let lodHit = false; let maxDrawCalls = 0; + let residencySampled = false; + const residencyObservation = { + renderCalls: (ctx.renderer.raw as WebGPURenderer).info.render.calls, + stableFrames: 0, + geometries: -1, + textures: -1, + bytes: -1, + }; const base = baseGeometryOf(left); if (base.index?.count !== 384) throw new Error("Decal fixture LOD0 must contain 128 triangles."); @@ -176,6 +206,7 @@ export function createDecalFixture(hideDecals = false, atlasFade = false, hideDu averageOpacity: active === 0 ? 0 : opacitySum / active, residency, residencySamples: residency.length, + stableResidencyFrames: residencyObservation.stableFrames, // Compare after the first reset, so one-time renderer startup caches are warmed. ...(baseline === undefined ? {} @@ -246,18 +277,27 @@ export function createDecalFixture(hideDecals = false, atlasFade = false, hideDu lodHit = true; } frames += 1; - if (frames === 60) { - const memory = (ctx.renderer.raw as WebGPURenderer).info.memory; - if (![memory.geometries, memory.textures, memory.total].every(Number.isFinite)) - throw new Error("Decal fixture renderer residency is unavailable."); + const info = (ctx.renderer.raw as WebGPURenderer).info; + observeResidency( + residencyObservation, + { + renderCalls: info.render.calls, + geometries: info.memory.geometries, + textures: info.memory.textures, + bytes: info.memory.total, + }, + lodHit && left.geometry.index?.count === 6 && info.render.drawCalls >= field.capacity, + ); + if (!residencySampled && residencyObservation.stableFrames >= 3) { residency.push({ generation, - geometries: memory.geometries, - textures: memory.textures, - bytes: memory.total, + ...residencyObservation, }); // Retain the warm baseline and the latest samples even during manual repeated resets. if (residency.length > 8) residency.splice(2, 1); + residencySampled = true; + ctx.state.set({ residencyGeneration: generation }); + console.info(`TN_DECAL_FIXTURE:${JSON.stringify(observation())}`); } if (frames % 30 === 0) console.info(`TN_DECAL_FIXTURE:${JSON.stringify(observation())}`); }; @@ -271,7 +311,7 @@ export function createDecalFixture(hideDecals = false, atlasFade = false, hideDu return defineGame({ camera: { far: 100, fov: 48, near: 0.1, projection: "perspective" }, display: { maxFps: 60 }, - initialState: {}, + initialState: { residencyGeneration: 0 }, input: { motion: { keys: ["KeyM"] }, remove: { keys: ["KeyX"] }, diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/lifecycle.playtest.json b/packages/create-threenative/__tests__/fixtures/bounded-decals/lifecycle.playtest.json index 1305603d30..e2c75327e8 100644 --- a/packages/create-threenative/__tests__/fixtures/bounded-decals/lifecycle.playtest.json +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/lifecycle.playtest.json @@ -13,9 +13,14 @@ }, "steps": [ { - "kind": "wait", - "label": "lod-and-pool", - "waitFrames": 90 + "label": "rendered-residency-1", + "waitForResource": { + "id": "state", + "path": "residencyGeneration", + "gte": 1 + }, + "timeoutMs": 30000, + "release": true }, { "kind": "input", @@ -25,9 +30,14 @@ "release": true }, { - "kind": "wait", - "label": "settled-2", - "waitFrames": 90 + "label": "rendered-residency-2", + "waitForResource": { + "id": "state", + "path": "residencyGeneration", + "gte": 2 + }, + "timeoutMs": 30000, + "release": true }, { "kind": "input", @@ -37,9 +47,14 @@ "release": true }, { - "kind": "wait", - "label": "settled-3", - "waitFrames": 90 + "label": "rendered-residency-3", + "waitForResource": { + "id": "state", + "path": "residencyGeneration", + "gte": 3 + }, + "timeoutMs": 30000, + "release": true }, { "kind": "input", @@ -49,9 +64,14 @@ "release": true }, { - "kind": "wait", - "label": "settled-4", - "waitFrames": 90 + "label": "rendered-residency-4", + "waitForResource": { + "id": "state", + "path": "residencyGeneration", + "gte": 4 + }, + "timeoutMs": 30000, + "release": true }, { "kind": "input", @@ -61,9 +81,14 @@ "release": true }, { - "kind": "wait", - "label": "settled-5", - "waitFrames": 90 + "label": "rendered-residency-5", + "waitForResource": { + "id": "state", + "path": "residencyGeneration", + "gte": 5 + }, + "timeoutMs": 30000, + "release": true }, { "kind": "input", @@ -73,9 +98,14 @@ "release": true }, { - "kind": "wait", - "label": "settled-6", - "waitFrames": 90 + "label": "rendered-residency-6", + "waitForResource": { + "id": "state", + "path": "residencyGeneration", + "gte": 6 + }, + "timeoutMs": 30000, + "release": true } ], "assert": { @@ -119,7 +149,7 @@ "entity": "decals", "component": "residencySamples", "equals": 6, - "allowTrivial": "One real renderer memory sample after 60 rendered frames in each scene." + "allowTrivial": "One memory sample per scene after three stable completed world-render intervals and real LOD/cap readiness." }, { "entity": "decals", @@ -138,6 +168,12 @@ "component": "memoryGrowth", "lte": 0, "allowTrivial": "GPU resource counts and bytes must not grow across the five warmed scene instances." + }, + { + "entity": "decals", + "component": "stableResidencyFrames", + "gte": 3, + "allowTrivial": "Fixed simulation ticks cannot substitute for completed render intervals." } ], "visual": [ diff --git a/packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts b/packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts index 57bf115cab..18243411d5 100644 --- a/packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts +++ b/packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts @@ -9,9 +9,32 @@ import { evaluateRichPlaytestAssertions } from "../../playtest/src/assertion-eva import { screenshotObservations } from "../../playtest/src/runner/steps.js"; import { loadPlaytestScenario } from "../../playtest/src/scenario.js"; import { DecalField, bulletHoleTexture } from "../templates/shooter/src/render/decals.js"; +import { observeResidency } from "./fixtures/bounded-decals/game.js"; afterEach(() => vi.unstubAllGlobals()); +it("requires stable completed render intervals rather than simulated ticks for residency", () => { + const previous = { renderCalls: 10, stableFrames: 0, geometries: -1, textures: -1, bytes: -1 }; + const current = { renderCalls: 10, geometries: 258, textures: 4, bytes: 6500000 }; + for (let tick = 0; tick < 90; tick += 1) observeResidency(previous, current, true); + expect(previous.stableFrames).toBe(0); + for (const calls of [14, 18, 22]) { + current.renderCalls = calls; + observeResidency(previous, current, true); + } + expect(previous.stableFrames).toBe(3); // One observation per interval, not four internal passes. + current.renderCalls = 26; + current.geometries += 1; + current.bytes += 576; + observeResidency(previous, current, true); + expect(previous.stableFrames).toBe(1); + observeResidency(previous, current, false); + expect(previous.stableFrames).toBe(0); + expect(() => observeResidency(previous, { ...current, bytes: Number.NaN }, true)).toThrow( + /residency/i, + ); +}); + async function evaluateCapturedPixels( scenarioName: string, captureName: string, From a6d1eaee72fd649b24657fdf99bfb672ad9f3468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Furtado?= <30300459+jonit-dev@users.noreply.github.com> Date: Fri, 2 Oct 2026 05:22:52 -0700 Subject: [PATCH 09/12] docs(vq11): record zero-growth rendered lifecycle and browser qualification --- .../PRD-VQ-11-bounded-decals.md | 13 +- .../vq11-decals-37005420971/summary.json | 1006 +++++++++++++++++ 2 files changed, 1016 insertions(+), 3 deletions(-) create mode 100644 docs/verification/vq11-decals-37005420971/summary.json diff --git a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md index f35e705f7d..e596353105 100644 --- a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md +++ b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md @@ -4,7 +4,7 @@ prd_contract: v1 # PRD-VQ-11 — Impact and environment decals use a bounded portable lifecycle -**Status:** PARTIAL — 2026-10-02. Owned-resource teardown and opt-in bounded receiver projection are CPU-verified. Browser visible-mark/hidden-material discrimination is verified; authored fade/atlas, repeated GPU residency and native gates remain open. +**Status:** PARTIAL — 2026-10-02. Owned-resource teardown and opt-in bounded receiver projection are CPU-verified. Browser atlas/fade/hidden-material discrimination and repeated rendered-scene residency are verified. Linux-native execution and full acceptance remain open. **Batch:** [Visual quality execution batch](https://github.com/ThreeNativeHQ/threenative/blob/docs/visual-quality-batch-2026-10-01/docs/PRDs/batch-2026-10-01-visual-quality/README.md). **Wave:** 2 / high-payoff detail. **Dependencies:** Independent. Share receiver/base-geometry selection with existing LOD and picking paths. @@ -56,8 +56,8 @@ All proof paths below are **planned implementation targets**, not existing passi - [x] Add game-owned decal material/atlas composition with bounded bias and fade controls. proof: `node node_modules/vitest/vitest.mjs run --maxWorkers 1 packages/create-threenative/__tests__/shooter-decals-projection.spec.ts` plus the runtime fixture. - Materials, tints, map, offset, projection depth and size remain game-authored. Optional atlas/fade controls and bounded projection bias are CPU-verified; actual atlas, visible partial fade, expiry and hidden-during-fade cases pass at `021e6223` in run 37003284983. -- [ ] Verify rigid receiver motion, LOD changes and repeated scene teardown without detached marks or allocation growth. proof: `node node_modules/vitest/vitest.mjs run --maxWorkers 1 packages/create-threenative/__tests__/shooter-decals-projection.spec.ts packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts` plus the Phase 3 lifecycle scenario. - - Receiver/ancestor rigid motion, positive uniform scale, direct and ancestor removal, explicit cleanup, legacy-slot reuse and actual loader/picker LOD independence are CPU-verified. Repeated projected scene resets and GPU residency still require the runtime fixture. +- [x] Verify rigid receiver motion, LOD changes and repeated scene teardown without detached marks or allocation growth. proof: `node node_modules/vitest/vitest.mjs run --maxWorkers 1 packages/create-threenative/__tests__/shooter-decals-projection.spec.ts packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts` plus the Phase 3 lifecycle scenario. + - Receiver/ancestor rigid motion, positive uniform scale, direct and ancestor removal, explicit cleanup, legacy-slot reuse and actual loader/picker LOD independence are CPU-verified. [Run 37005420971](https://github.com/ThreeNativeHQ/threenative/actions/runs/37005420971), source `3bb9942c`, passes six actual rendered scene instances with identical 259 geometries, 4 textures and 6,522,894 GPU-tracked bytes; three stable completed-render intervals precede each sample. - Partial, 2026-10-02: the lifetime regression exercises 20 field construction/teardown cycles, 2,000 pooled placements, clear/reuse, zero-slot teardown and repeated `dispose()`. Real Three disposal events/spies verify owned-resource release and retention of the borrowed map. Rigid receivers, LOD changes, GPU memory and game-scene transitions are not qualified by these unit tests. ### Phase 3 — Qualify the visible result @@ -152,3 +152,10 @@ Update the phase boxes and this PRD only after the named proof runs. Record actu - Replace the simulated-tick sample with the proven completed-render boundary used by VQ07: observe a changed cumulative `renderer.info.render.calls` value on the next game update, count one interval regardless of internal passes, and require three consecutive stable geometry/texture/byte observations. Only accept the world at its actual projected-mark draw count with the real two-triangle LOD and post-LOD hit present. - The lifecycle scenario uses bounded 30-second `waitForResource` waits for each of six sampled scene generations, allowing actual presentation between polls. A unit regression rejects 90 simulated ticks with no render-call advance, resets stability when counts change, and rejects missing/nonfinite memory. All 55 focused/adjacent tests, typecheck, fixture build, lint and lifecycle schema validation pass. Production decal code, the original four scenario files and zero-growth ceilings are unchanged. The prior +1/+576 failure is preserved above and is not yet declared fixed by browser evidence. + +2026-10-02 completed-render residency and full browser sequence verified: + +- [Run 37005420971](https://github.com/ThreeNativeHQ/threenative/actions/runs/37005420971) passes at source `3bb9942c41dfd5c4a8690354373ad9c53259d0ee`: eight positive captures and both expected hidden-control rejections. Artifact `11225394658` SHA-256 `9c2811282094efd8229fc341f2a126dea451feefef7587c37882426365488493` was verified. [Raw summary](../../verification/vq11-decals-37005420971/summary.json) retains every capture, counter and diagnostic. +- Across all six scene generations, actual completed-render samples are identical: 259 geometries, 4 textures, 6,522,894 tracked bytes. Cumulative render calls advance 56, 64, 72, 80, 88, 96; every sample has three stable observed intervals. Geometry, texture and byte growth are all zero. The previously reported +1/+576 variation was caused by sampling before stable world rendering; no ceiling changed. This is renderer-reported software-adapter residency over the six observed instances, not a hardware memory benchmark or an unlimited-duration claim. +- All ten actual PNGs are byte-identical to already retained captures, so no duplicate image files were added. The inspected [atlas](../../verification/vq11-decals-37003284983/atlas.png), [partial fade](../../verification/vq11-decals-37003284983/fading.png), [empty/expired](../../verification/vq11-decals-36991562324/hidden-decals.png), [static/restarted lifecycle](../../verification/vq11-decals-36989297734/static.png), [motion](../../verification/vq11-decals-36989297734/motion.png) and [teardown](../../verification/vq11-decals-36989297734/teardown.png) files preserve the exact bytes of this run. Positive captures have no error/device-loss diagnostics; each negative control has only its two intended pixel failures. +- Phases 1 and 2 and browser Phase 3 are verified. Linux-native remains unexecuted; the existing VQ07 hosted ARM desktop route provides a concrete next integration path, so this is not claimed to be blocked solely by the local missing display/GPU. Both full acceptance boxes, required aggregate gates and final promotion review remain open. PR stays draft; no auto-merge is enabled. diff --git a/docs/verification/vq11-decals-37005420971/summary.json b/docs/verification/vq11-decals-37005420971/summary.json new file mode 100644 index 0000000000..eb896571ce --- /dev/null +++ b/docs/verification/vq11-decals-37005420971/summary.json @@ -0,0 +1,1006 @@ +{ + "sourceSha": "3bb9942c41dfd5c4a8690354373ad9c53259d0ee", + "correctnessOnly": true, + "results": [ + { + "name": "static", + "sourceSha": "3bb9942c41dfd5c4a8690354373ad9c53259d0ee", + "qualified": true, + "measurement": { + "generation": 1, + "active": 256, + "capacity": 256, + "created": 321, + "released": 65, + "geometryBytes": 275028, + "maxDrawCalls": 516, + "motionError": 0, + "receiverMotion": 0, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": false, + "atlasFade": false, + "averageOpacity": 1, + "residency": [], + "residencySamples": 0, + "stableResidencyFrames": 1 + }, + "capture": { + "adapter": { + "architecture": "swiftshader", + "vendor": "google", + "features": "bgra8unorm-storage,chromium-experimental-timestamp-query-inside-passes,clip-distances,core-features-and-limits,depth-clip-control,depth32float-stencil8,float32-blendable,float32-filterable,indirect-first-instance,rg11b10ufloat-renderable,subgroup-size-control,subgroups,texture-component-swizzle,texture-compression-astc,texture-compression-astc-sliced-3d,texture-compression-bc,texture-compression-bc-sliced-3d,texture-compression-etc2,timestamp-query", + "limit.maxBindGroups": "4", + "limit.maxTextureDimension2D": "8192", + "limit.maxStorageBufferBindingSize": "1073741824" + }, + "browserArgs": [ + "--ozone-platform=x11", + "--enable-unsafe-webgpu", + "--disable-gpu-sandbox", + "--ignore-gpu-blocklist", + "--enable-features=Vulkan" + ], + "captureMethod": "page.screenshot", + "rendererKind": "webgpu", + "target": "web", + "viewport": { + "height": 540, + "width": 960 + } + }, + "screenshot": "/home/runner/work/threenative/threenative/artifacts/vq11-decals/static/after.png", + "diagnostics": [], + "visual": { + "changedPixelRatio": 0.00017939814814814815, + "comparisonSource": "before-after", + "nonblankRegions": [ + { + "height": 540, + "width": 960, + "x": 0, + "y": 0, + "minNonblankPixelRatio": 0.15, + "darkPixelRatio": 0.5420428240740741, + "nonblankPixelRatio": 0.9999710648148148 + }, + { + "height": 125, + "width": 170, + "x": 270, + "y": 185, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.05341176470588235, + "nonblankPixelRatio": 0.9994823529411765 + }, + { + "height": 90, + "width": 95, + "x": 550, + "y": 190, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.05660818713450293, + "nonblankPixelRatio": 1 + } + ] + } + }, + { + "name": "motion", + "sourceSha": "3bb9942c41dfd5c4a8690354373ad9c53259d0ee", + "qualified": true, + "measurement": { + "generation": 1, + "active": 256, + "capacity": 256, + "created": 321, + "released": 65, + "geometryBytes": 275076, + "maxDrawCalls": 516, + "motionError": 0, + "receiverMotion": 0.8042230500706451, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": false, + "atlasFade": false, + "averageOpacity": 1, + "residency": [], + "residencySamples": 0, + "stableResidencyFrames": 1 + }, + "capture": { + "adapter": { + "architecture": "swiftshader", + "vendor": "google", + "features": "bgra8unorm-storage,chromium-experimental-timestamp-query-inside-passes,clip-distances,core-features-and-limits,depth-clip-control,depth32float-stencil8,float32-blendable,float32-filterable,indirect-first-instance,rg11b10ufloat-renderable,subgroup-size-control,subgroups,texture-component-swizzle,texture-compression-astc,texture-compression-astc-sliced-3d,texture-compression-bc,texture-compression-bc-sliced-3d,texture-compression-etc2,timestamp-query", + "limit.maxBindGroups": "4", + "limit.maxTextureDimension2D": "8192", + "limit.maxStorageBufferBindingSize": "1073741824" + }, + "browserArgs": [ + "--ozone-platform=x11", + "--enable-unsafe-webgpu", + "--disable-gpu-sandbox", + "--ignore-gpu-blocklist", + "--enable-features=Vulkan" + ], + "captureMethod": "page.screenshot", + "rendererKind": "webgpu", + "target": "web", + "viewport": { + "height": 540, + "width": 960 + } + }, + "screenshot": "/home/runner/work/threenative/threenative/artifacts/vq11-decals/motion/after.png", + "diagnostics": [], + "visual": { + "changedPixelRatio": 0.04210648148148148, + "comparisonSource": "before-after", + "nonblankRegions": [ + { + "height": 540, + "width": 960, + "x": 0, + "y": 0, + "minNonblankPixelRatio": 0.15, + "darkPixelRatio": 0.5506944444444445, + "nonblankPixelRatio": 0.9999729938271605 + }, + { + "height": 125, + "width": 170, + "x": 270, + "y": 185, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.05341176470588235, + "nonblankPixelRatio": 0.9994823529411765 + }, + { + "height": 90, + "width": 95, + "x": 550, + "y": 190, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.051812865497076026, + "nonblankPixelRatio": 0.9997660818713451 + } + ] + } + }, + { + "name": "teardown", + "sourceSha": "3bb9942c41dfd5c4a8690354373ad9c53259d0ee", + "qualified": true, + "measurement": { + "generation": 1, + "active": 128, + "capacity": 256, + "created": 321, + "released": 193, + "geometryBytes": 136140, + "maxDrawCalls": 516, + "motionError": 0, + "receiverMotion": 0, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": true, + "atlasFade": false, + "averageOpacity": 1, + "residency": [], + "residencySamples": 0, + "stableResidencyFrames": 1 + }, + "capture": { + "adapter": { + "architecture": "swiftshader", + "vendor": "google", + "features": "bgra8unorm-storage,chromium-experimental-timestamp-query-inside-passes,clip-distances,core-features-and-limits,depth-clip-control,depth32float-stencil8,float32-blendable,float32-filterable,indirect-first-instance,rg11b10ufloat-renderable,subgroup-size-control,subgroups,texture-component-swizzle,texture-compression-astc,texture-compression-astc-sliced-3d,texture-compression-bc,texture-compression-bc-sliced-3d,texture-compression-etc2,timestamp-query", + "limit.maxBindGroups": "4", + "limit.maxTextureDimension2D": "8192", + "limit.maxStorageBufferBindingSize": "1073741824" + }, + "browserArgs": [ + "--ozone-platform=x11", + "--enable-unsafe-webgpu", + "--disable-gpu-sandbox", + "--ignore-gpu-blocklist", + "--enable-features=Vulkan" + ], + "captureMethod": "page.screenshot", + "rendererKind": "webgpu", + "target": "web", + "viewport": { + "height": 540, + "width": 960 + } + }, + "screenshot": "/home/runner/work/threenative/threenative/artifacts/vq11-decals/teardown/after.png", + "diagnostics": [], + "visual": { + "changedPixelRatio": 0.07619598765432099, + "comparisonSource": "before-after", + "nonblankRegions": [ + { + "height": 540, + "width": 960, + "x": 0, + "y": 0, + "minNonblankPixelRatio": 0.15, + "darkPixelRatio": 0.6017264660493827, + "nonblankPixelRatio": 0.9999787808641976 + }, + { + "height": 125, + "width": 170, + "x": 270, + "y": 185, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.05341176470588235, + "nonblankPixelRatio": 0.9994823529411765 + } + ] + } + }, + { + "name": "restart", + "sourceSha": "3bb9942c41dfd5c4a8690354373ad9c53259d0ee", + "qualified": true, + "measurement": { + "generation": 2, + "active": 256, + "capacity": 256, + "created": 321, + "released": 65, + "geometryBytes": 275028, + "maxDrawCalls": 516, + "motionError": 0, + "receiverMotion": 0, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": false, + "atlasFade": false, + "averageOpacity": 1, + "residency": [], + "residencySamples": 0, + "stableResidencyFrames": 1 + }, + "capture": { + "adapter": { + "architecture": "swiftshader", + "vendor": "google", + "features": "bgra8unorm-storage,chromium-experimental-timestamp-query-inside-passes,clip-distances,core-features-and-limits,depth-clip-control,depth32float-stencil8,float32-blendable,float32-filterable,indirect-first-instance,rg11b10ufloat-renderable,subgroup-size-control,subgroups,texture-component-swizzle,texture-compression-astc,texture-compression-astc-sliced-3d,texture-compression-bc,texture-compression-bc-sliced-3d,texture-compression-etc2,timestamp-query", + "limit.maxBindGroups": "4", + "limit.maxTextureDimension2D": "8192", + "limit.maxStorageBufferBindingSize": "1073741824" + }, + "browserArgs": [ + "--ozone-platform=x11", + "--enable-unsafe-webgpu", + "--disable-gpu-sandbox", + "--ignore-gpu-blocklist", + "--enable-features=Vulkan" + ], + "captureMethod": "page.screenshot", + "rendererKind": "webgpu", + "target": "web", + "viewport": { + "height": 540, + "width": 960 + } + }, + "screenshot": "/home/runner/work/threenative/threenative/artifacts/vq11-decals/restart/after.png", + "diagnostics": [], + "visual": { + "changedPixelRatio": 0.00017939814814814815, + "comparisonSource": "before-after", + "nonblankRegions": [ + { + "height": 540, + "width": 960, + "x": 0, + "y": 0, + "minNonblankPixelRatio": 0.15, + "darkPixelRatio": 0.5420428240740741, + "nonblankPixelRatio": 0.9999710648148148 + }, + { + "height": 125, + "width": 170, + "x": 270, + "y": 185, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.05341176470588235, + "nonblankPixelRatio": 0.9994823529411765 + }, + { + "height": 90, + "width": 95, + "x": 550, + "y": 190, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.05660818713450293, + "nonblankPixelRatio": 1 + } + ] + } + }, + { + "name": "atlas", + "sourceSha": "3bb9942c41dfd5c4a8690354373ad9c53259d0ee", + "qualified": true, + "measurement": { + "generation": 1, + "active": 256, + "capacity": 256, + "created": 321, + "released": 65, + "geometryBytes": 275028, + "maxDrawCalls": 516, + "motionError": 0, + "receiverMotion": 0, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": false, + "atlasFade": true, + "averageOpacity": 1, + "residency": [], + "residencySamples": 0, + "stableResidencyFrames": 1 + }, + "capture": { + "adapter": { + "architecture": "swiftshader", + "vendor": "google", + "features": "bgra8unorm-storage,chromium-experimental-timestamp-query-inside-passes,clip-distances,core-features-and-limits,depth-clip-control,depth32float-stencil8,float32-blendable,float32-filterable,indirect-first-instance,rg11b10ufloat-renderable,subgroup-size-control,subgroups,texture-component-swizzle,texture-compression-astc,texture-compression-astc-sliced-3d,texture-compression-bc,texture-compression-bc-sliced-3d,texture-compression-etc2,timestamp-query", + "limit.maxBindGroups": "4", + "limit.maxTextureDimension2D": "8192", + "limit.maxStorageBufferBindingSize": "1073741824" + }, + "browserArgs": [ + "--ozone-platform=x11", + "--enable-unsafe-webgpu", + "--disable-gpu-sandbox", + "--ignore-gpu-blocklist", + "--enable-features=Vulkan" + ], + "captureMethod": "page.screenshot", + "rendererKind": "webgpu", + "target": "web", + "viewport": { + "height": 540, + "width": 960 + } + }, + "screenshot": "/home/runner/work/threenative/threenative/artifacts/vq11-decals/atlas/after.png", + "diagnostics": [], + "visual": { + "changedPixelRatio": 0.00017939814814814815, + "comparisonSource": "before-after", + "nonblankRegions": [ + { + "height": 540, + "width": 960, + "x": 0, + "y": 0, + "minNonblankPixelRatio": 0.15, + "darkPixelRatio": 0.5419116512345679, + "nonblankPixelRatio": 0.9999787808641976 + }, + { + "height": 125, + "width": 170, + "x": 270, + "y": 185, + "maxDarkPixelRatio": 0.1, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.05341176470588235, + "nonblankPixelRatio": 0.9994823529411765 + }, + { + "height": 90, + "width": 95, + "x": 550, + "y": 190, + "maxDarkPixelRatio": 0.65, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.12, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.4545029239766082, + "nonblankPixelRatio": 1 + } + ] + } + }, + { + "name": "fading", + "sourceSha": "3bb9942c41dfd5c4a8690354373ad9c53259d0ee", + "qualified": true, + "measurement": { + "generation": 1, + "active": 256, + "capacity": 256, + "created": 321, + "released": 65, + "geometryBytes": 275076, + "maxDrawCalls": 516, + "motionError": 0, + "receiverMotion": 0, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": false, + "atlasFade": true, + "averageOpacity": 0.49999999999999994, + "residency": [], + "residencySamples": 0, + "stableResidencyFrames": 1 + }, + "capture": { + "adapter": { + "architecture": "swiftshader", + "vendor": "google", + "features": "bgra8unorm-storage,chromium-experimental-timestamp-query-inside-passes,clip-distances,core-features-and-limits,depth-clip-control,depth32float-stencil8,float32-blendable,float32-filterable,indirect-first-instance,rg11b10ufloat-renderable,subgroup-size-control,subgroups,texture-component-swizzle,texture-compression-astc,texture-compression-astc-sliced-3d,texture-compression-bc,texture-compression-bc-sliced-3d,texture-compression-etc2,timestamp-query", + "limit.maxBindGroups": "4", + "limit.maxTextureDimension2D": "8192", + "limit.maxStorageBufferBindingSize": "1073741824" + }, + "browserArgs": [ + "--ozone-platform=x11", + "--enable-unsafe-webgpu", + "--disable-gpu-sandbox", + "--ignore-gpu-blocklist", + "--enable-features=Vulkan" + ], + "captureMethod": "page.screenshot", + "rendererKind": "webgpu", + "target": "web", + "viewport": { + "height": 540, + "width": 960 + } + }, + "screenshot": "/home/runner/work/threenative/threenative/artifacts/vq11-decals/fading/after.png", + "diagnostics": [], + "visual": { + "changedPixelRatio": 0.0277758487654321, + "comparisonSource": "before-after", + "nonblankRegions": [ + { + "height": 540, + "width": 960, + "x": 0, + "y": 0, + "minNonblankPixelRatio": 0.15, + "darkPixelRatio": 0.5417457561728395, + "nonblankPixelRatio": 1 + }, + { + "height": 125, + "width": 170, + "x": 270, + "y": 185, + "maxDarkPixelRatio": 0.001, + "maxLuminance": 0.35, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0, + "nonblankPixelRatio": 1 + }, + { + "height": 90, + "width": 95, + "x": 550, + "y": 190, + "maxDarkPixelRatio": 0.001, + "maxLuminance": 0.35, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0, + "nonblankPixelRatio": 1 + }, + { + "height": 100, + "width": 150, + "x": 280, + "y": 195, + "maxDarkPixelRatio": 0.3, + "maxLuminance": 0.65, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.06553333333333333, + "nonblankPixelRatio": 1 + }, + { + "height": 80, + "width": 85, + "x": 555, + "y": 195, + "maxDarkPixelRatio": 0.65, + "maxLuminance": 0.6, + "minDarkPixelRatio": 0.1, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.43441176470588233, + "nonblankPixelRatio": 1 + } + ] + } + }, + { + "name": "expired", + "sourceSha": "3bb9942c41dfd5c4a8690354373ad9c53259d0ee", + "qualified": true, + "measurement": { + "generation": 1, + "active": 0, + "capacity": 256, + "created": 321, + "released": 321, + "geometryBytes": 0, + "maxDrawCalls": 516, + "motionError": 0, + "receiverMotion": 0, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": false, + "atlasFade": true, + "averageOpacity": 0, + "residency": [], + "residencySamples": 0, + "stableResidencyFrames": 1 + }, + "capture": { + "adapter": { + "architecture": "swiftshader", + "vendor": "google", + "features": "bgra8unorm-storage,chromium-experimental-timestamp-query-inside-passes,clip-distances,core-features-and-limits,depth-clip-control,depth32float-stencil8,float32-blendable,float32-filterable,indirect-first-instance,rg11b10ufloat-renderable,subgroup-size-control,subgroups,texture-component-swizzle,texture-compression-astc,texture-compression-astc-sliced-3d,texture-compression-bc,texture-compression-bc-sliced-3d,texture-compression-etc2,timestamp-query", + "limit.maxBindGroups": "4", + "limit.maxTextureDimension2D": "8192", + "limit.maxStorageBufferBindingSize": "1073741824" + }, + "browserArgs": [ + "--ozone-platform=x11", + "--enable-unsafe-webgpu", + "--disable-gpu-sandbox", + "--ignore-gpu-blocklist", + "--enable-features=Vulkan" + ], + "captureMethod": "page.screenshot", + "rendererKind": "webgpu", + "target": "web", + "viewport": { + "height": 540, + "width": 960 + } + }, + "screenshot": "/home/runner/work/threenative/threenative/artifacts/vq11-decals/expired/after.png", + "diagnostics": [], + "visual": { + "changedPixelRatio": 0.029544753086419753, + "comparisonSource": "before-after", + "nonblankRegions": [ + { + "height": 540, + "width": 960, + "x": 0, + "y": 0, + "minNonblankPixelRatio": 0.15, + "darkPixelRatio": 0.5417457561728395, + "nonblankPixelRatio": 1 + }, + { + "height": 125, + "width": 170, + "x": 270, + "y": 185, + "maxDarkPixelRatio": 0.001, + "maxLuminance": 0.35, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0, + "nonblankPixelRatio": 1 + }, + { + "height": 90, + "width": 95, + "x": 550, + "y": 190, + "maxDarkPixelRatio": 0.001, + "maxLuminance": 0.35, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0, + "nonblankPixelRatio": 1 + } + ] + } + }, + { + "name": "lifecycle", + "sourceSha": "3bb9942c41dfd5c4a8690354373ad9c53259d0ee", + "qualified": true, + "measurement": { + "generation": 6, + "active": 256, + "capacity": 256, + "created": 321, + "released": 65, + "geometryBytes": 275076, + "maxDrawCalls": 516, + "motionError": 0, + "receiverMotion": 0, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": false, + "atlasFade": false, + "averageOpacity": 1, + "residency": [ + { + "generation": 1, + "renderCalls": 56, + "stableFrames": 3, + "geometries": 259, + "textures": 4, + "bytes": 6522894 + }, + { + "generation": 2, + "renderCalls": 64, + "stableFrames": 3, + "geometries": 259, + "textures": 4, + "bytes": 6522894 + }, + { + "generation": 3, + "renderCalls": 72, + "stableFrames": 3, + "geometries": 259, + "textures": 4, + "bytes": 6522894 + }, + { + "generation": 4, + "renderCalls": 80, + "stableFrames": 3, + "geometries": 259, + "textures": 4, + "bytes": 6522894 + }, + { + "generation": 5, + "renderCalls": 88, + "stableFrames": 3, + "geometries": 259, + "textures": 4, + "bytes": 6522894 + }, + { + "generation": 6, + "renderCalls": 96, + "stableFrames": 3, + "geometries": 259, + "textures": 4, + "bytes": 6522894 + } + ], + "residencySamples": 6, + "stableResidencyFrames": 3, + "geometryGrowth": 0, + "textureGrowth": 0, + "memoryGrowth": 0 + }, + "capture": { + "adapter": { + "architecture": "swiftshader", + "vendor": "google", + "features": "bgra8unorm-storage,chromium-experimental-timestamp-query-inside-passes,clip-distances,core-features-and-limits,depth-clip-control,depth32float-stencil8,float32-blendable,float32-filterable,indirect-first-instance,rg11b10ufloat-renderable,subgroup-size-control,subgroups,texture-component-swizzle,texture-compression-astc,texture-compression-astc-sliced-3d,texture-compression-bc,texture-compression-bc-sliced-3d,texture-compression-etc2,timestamp-query", + "limit.maxBindGroups": "4", + "limit.maxTextureDimension2D": "8192", + "limit.maxStorageBufferBindingSize": "1073741824" + }, + "browserArgs": [ + "--ozone-platform=x11", + "--enable-unsafe-webgpu", + "--disable-gpu-sandbox", + "--ignore-gpu-blocklist", + "--enable-features=Vulkan" + ], + "captureMethod": "page.screenshot", + "rendererKind": "webgpu", + "target": "web", + "viewport": { + "height": 540, + "width": 960 + } + }, + "screenshot": "/home/runner/work/threenative/threenative/artifacts/vq11-decals/lifecycle/after.png", + "diagnostics": [], + "visual": { + "changedPixelRatio": 0.00017939814814814815, + "comparisonSource": "before-after", + "nonblankRegions": [ + { + "height": 540, + "width": 960, + "x": 0, + "y": 0, + "minNonblankPixelRatio": 0.15, + "darkPixelRatio": 0.5420428240740741, + "nonblankPixelRatio": 0.9999710648148148 + }, + { + "height": 125, + "width": 170, + "x": 270, + "y": 185, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.05341176470588235, + "nonblankPixelRatio": 0.9994823529411765 + }, + { + "height": 90, + "width": 95, + "x": 550, + "y": 190, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0.05660818713450293, + "nonblankPixelRatio": 1 + } + ] + } + }, + { + "name": "hidden-decals", + "sourceSha": "3bb9942c41dfd5c4a8690354373ad9c53259d0ee", + "qualified": false, + "negativeControlPassed": true, + "measurement": { + "generation": 1, + "active": 256, + "capacity": 256, + "created": 321, + "released": 65, + "geometryBytes": 259794, + "maxDrawCalls": 4, + "motionError": 0, + "receiverMotion": 0, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": false, + "atlasFade": false, + "averageOpacity": 1, + "residency": [], + "residencySamples": 0, + "stableResidencyFrames": 0 + }, + "capture": { + "adapter": { + "architecture": "swiftshader", + "vendor": "google", + "features": "bgra8unorm-storage,chromium-experimental-timestamp-query-inside-passes,clip-distances,core-features-and-limits,depth-clip-control,depth32float-stencil8,float32-blendable,float32-filterable,indirect-first-instance,rg11b10ufloat-renderable,subgroup-size-control,subgroups,texture-component-swizzle,texture-compression-astc,texture-compression-astc-sliced-3d,texture-compression-bc,texture-compression-bc-sliced-3d,texture-compression-etc2,timestamp-query", + "limit.maxBindGroups": "4", + "limit.maxTextureDimension2D": "8192", + "limit.maxStorageBufferBindingSize": "1073741824" + }, + "browserArgs": [ + "--ozone-platform=x11", + "--enable-unsafe-webgpu", + "--disable-gpu-sandbox", + "--ignore-gpu-blocklist", + "--enable-features=Vulkan" + ], + "captureMethod": "page.screenshot", + "rendererKind": "webgpu", + "target": "web", + "viewport": { + "height": 540, + "width": 960 + } + }, + "screenshot": "/home/runner/work/threenative/threenative/artifacts/vq11-decals/hidden-decals/after.png", + "diagnostics": [ + { + "code": "TN_PLAYTEST_REGION_DARK_PIXEL_RATIO_FAILED", + "message": "Screenshot region at (270, 185) contained 0 dark pixels, below required ratio 0.01.", + "severity": "error", + "suggestion": "Check whether the expected foreground silhouette occupies the asserted raster region." + }, + { + "code": "TN_PLAYTEST_REGION_DARK_PIXEL_RATIO_FAILED", + "message": "Screenshot region at (550, 190) contained 0 dark pixels, below required ratio 0.01.", + "severity": "error", + "suggestion": "Check whether the expected foreground silhouette occupies the asserted raster region." + } + ], + "visual": { + "changedPixelRatio": 0, + "comparisonSource": "before-after", + "nonblankRegions": [ + { + "height": 540, + "width": 960, + "x": 0, + "y": 0, + "minNonblankPixelRatio": 0.15, + "darkPixelRatio": 0.5417457561728395, + "nonblankPixelRatio": 1 + }, + { + "height": 125, + "width": 170, + "x": 270, + "y": 185, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0, + "nonblankPixelRatio": 1 + }, + { + "height": 90, + "width": 95, + "x": 550, + "y": 190, + "maxDarkPixelRatio": 0.2, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0, + "nonblankPixelRatio": 1 + } + ] + } + }, + { + "name": "hidden-fading", + "sourceSha": "3bb9942c41dfd5c4a8690354373ad9c53259d0ee", + "qualified": false, + "negativeControlPassed": true, + "measurement": { + "generation": 1, + "active": 256, + "capacity": 256, + "created": 321, + "released": 65, + "geometryBytes": 275076, + "maxDrawCalls": 516, + "motionError": 0, + "receiverMotion": 0, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": false, + "atlasFade": true, + "averageOpacity": 0.49999999999999994, + "residency": [], + "residencySamples": 0, + "stableResidencyFrames": 1 + }, + "capture": { + "adapter": { + "architecture": "swiftshader", + "vendor": "google", + "features": "bgra8unorm-storage,chromium-experimental-timestamp-query-inside-passes,clip-distances,core-features-and-limits,depth-clip-control,depth32float-stencil8,float32-blendable,float32-filterable,indirect-first-instance,rg11b10ufloat-renderable,subgroup-size-control,subgroups,texture-component-swizzle,texture-compression-astc,texture-compression-astc-sliced-3d,texture-compression-bc,texture-compression-bc-sliced-3d,texture-compression-etc2,timestamp-query", + "limit.maxBindGroups": "4", + "limit.maxTextureDimension2D": "8192", + "limit.maxStorageBufferBindingSize": "1073741824" + }, + "browserArgs": [ + "--ozone-platform=x11", + "--enable-unsafe-webgpu", + "--disable-gpu-sandbox", + "--ignore-gpu-blocklist", + "--enable-features=Vulkan" + ], + "captureMethod": "page.screenshot", + "rendererKind": "webgpu", + "target": "web", + "viewport": { + "height": 540, + "width": 960 + } + }, + "screenshot": "/home/runner/work/threenative/threenative/artifacts/vq11-decals/hidden-fading/after.png", + "diagnostics": [ + { + "code": "TN_PLAYTEST_REGION_DARK_PIXEL_RATIO_FAILED", + "message": "Screenshot region at (280, 195) contained 0 dark pixels, below required ratio 0.01.", + "severity": "error", + "suggestion": "Check whether the expected foreground silhouette occupies the asserted raster region." + }, + { + "code": "TN_PLAYTEST_REGION_DARK_PIXEL_RATIO_FAILED", + "message": "Screenshot region at (555, 195) contained 0 dark pixels, below required ratio 0.1.", + "severity": "error", + "suggestion": "Check whether the expected foreground silhouette occupies the asserted raster region." + } + ], + "visual": { + "changedPixelRatio": 0.029544753086419753, + "comparisonSource": "before-after", + "nonblankRegions": [ + { + "height": 540, + "width": 960, + "x": 0, + "y": 0, + "minNonblankPixelRatio": 0.15, + "darkPixelRatio": 0.5417457561728395, + "nonblankPixelRatio": 1 + }, + { + "height": 125, + "width": 170, + "x": 270, + "y": 185, + "maxDarkPixelRatio": 0.001, + "maxLuminance": 0.35, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0, + "nonblankPixelRatio": 1 + }, + { + "height": 90, + "width": 95, + "x": 550, + "y": 190, + "maxDarkPixelRatio": 0.001, + "maxLuminance": 0.35, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0, + "nonblankPixelRatio": 1 + }, + { + "height": 100, + "width": 150, + "x": 280, + "y": 195, + "maxDarkPixelRatio": 0.3, + "maxLuminance": 0.65, + "minDarkPixelRatio": 0.01, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0, + "nonblankPixelRatio": 1 + }, + { + "height": 80, + "width": 85, + "x": 555, + "y": 195, + "maxDarkPixelRatio": 0.65, + "maxLuminance": 0.6, + "minDarkPixelRatio": 0.1, + "minNonblankPixelRatio": 0.9, + "darkPixelRatio": 0, + "nonblankPixelRatio": 1 + } + ] + } + } + ] +} From 563ab23e9448a993013cb0d58c75ab6ab83917c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Furtado?= <30300459+jonit-dev@users.noreply.github.com> Date: Fri, 2 Oct 2026 06:06:52 -0700 Subject: [PATCH 10/12] test(vq11): qualify packaged native decals through supported bridge observations --- .github/workflows/integration-decals.yml | 60 +++++ .../PRD-VQ-11-bounded-decals.md | 13 ++ .../__tests__/fixtures/bounded-decals/game.ts | 2 + .../fixtures/bounded-decals/native-atlas.ts | 2 + .../bounded-decals/native-hidden-fading.ts | 2 + .../fixtures/bounded-decals/native-hidden.ts | 2 + .../fixtures/bounded-decals/native-proof.ts | 147 ++++++++++++ .../fixtures/bounded-decals/package.json | 6 + .../fixtures/bounded-decals/verify-native.ts | 211 ++++++++++++++++++ .../__tests__/shooter-decals-native.spec.ts | 147 ++++++++++++ 10 files changed, 592 insertions(+) create mode 100644 packages/create-threenative/__tests__/fixtures/bounded-decals/native-atlas.ts create mode 100644 packages/create-threenative/__tests__/fixtures/bounded-decals/native-hidden-fading.ts create mode 100644 packages/create-threenative/__tests__/fixtures/bounded-decals/native-hidden.ts create mode 100644 packages/create-threenative/__tests__/fixtures/bounded-decals/native-proof.ts create mode 100644 packages/create-threenative/__tests__/fixtures/bounded-decals/package.json create mode 100644 packages/create-threenative/__tests__/fixtures/bounded-decals/verify-native.ts create mode 100644 packages/create-threenative/__tests__/shooter-decals-native.spec.ts diff --git a/.github/workflows/integration-decals.yml b/.github/workflows/integration-decals.yml index f2f3011bc1..35011a3f2a 100644 --- a/.github/workflows/integration-decals.yml +++ b/.github/workflows/integration-decals.yml @@ -43,3 +43,63 @@ jobs: name: vq11-decals-${{ github.event.pull_request.head.sha || github.sha }} path: artifacts/vq11-decals if-no-files-found: error + native: + name: Linux native decal correctness + needs: [decals] + # Reuse the proven native-platforms / native-assets ARM QuickJS route without changing defaults. + runs-on: ubuntu-24.04-arm + timeout-minutes: 60 + env: + CMAKE_BUILD_PARALLEL_LEVEL: '2' + TN_PLAYTEST_ALLOW_SOFTWARE: '1' + TN_PLAYTEST_NATIVE_SCREENSHOT_TIMEOUT_MS: '30000' + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: ./.github/actions/pnpm + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: '22' + cache: pnpm + - uses: actions/setup-python@v7 + with: + python-version: '3.12' + - run: pnpm install --frozen-lockfile + - name: Build this source's public JavaScript packages + uses: ./.github/actions/workspace-dist + with: + save-bundles: 'false' + pack-archives: 'false' + - name: Verify native preflight and capture rejection controls + run: pnpm exec vitest run packages/create-threenative/__tests__/shooter-decals-native.spec.ts --maxWorkers=1 + - name: Restore checksum-provisioned native dependencies + uses: actions/cache/restore@v4 + with: + path: packages/runtime-native/third_party + key: native-third-party-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('packages/runtime-native/scripts/download-deps.mjs', 'packages/runtime-native/scripts/package-android.mjs') }} + - name: Install the existing native lane's system prerequisites + run: >- + sudo apt-get update && sudo apt-get install -y + libasound2-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev + libpulse-dev libudev-dev libx11-dev libxext-dev libxrandr-dev + libxcursor-dev libxi-dev libxfixes-dev libxss-dev libxkbcommon-dev + libgtk-3-dev libwebkit2gtk-4.1-dev libxcomposite-dev xcompmgr + libvulkan1 mesa-vulkan-drivers vulkan-tools xvfb zlib1g-dev + - name: Build the ordinary ARM desktop host + run: pnpm --filter @threenative/runtime-native native:build + - name: Run the same packaged decal scenarios and unchanged pixel gates + env: + THREENATIVE_RUNTIME_BINARY: ${{ github.workspace }}/packages/runtime-native/build/tn-linux/mystral + run: node --import tsx packages/create-threenative/__tests__/fixtures/bounded-decals/verify-native.ts + - name: Retain native screenshots, host console and executable identities + if: always() + uses: actions/upload-artifact@v7 + with: + name: vq11-decals-native-${{ github.event.pull_request.head.sha || github.sha }} + path: | + artifacts/vq11-decals-native/ + !artifacts/vq11-decals-native/variants/** + retention-days: 30 + if-no-files-found: warn diff --git a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md index e596353105..8a9c56f8ea 100644 --- a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md +++ b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md @@ -34,6 +34,12 @@ Keep this extension in the existing generated `DecalField`. A projected mark may Capability searches inspected the installed atlas rewrite/texel helpers and mesh merger. Those are asset cooking/framework surfaces; generated render source cannot import the framework, and the owned projected buffer needs only a direct UV affine transform. The full-request search also inspected `SpriteAnimator3D`; this is projected surface geometry rather than a sequenced sprite, and the render-source import rule applies. No installed authored fade/lifetime or GPU residency capability matched. CPU proof covers UV isolation, opacity progression, expiry/reuse/removal, malformed input and repeated lifecycle ownership. The existing hosted real fixture has optional atlas/fade and repeated scene-reset observations while retaining its pinned visible-mark and hidden-material gates. Native execution remains a separate unverified platform gate. +### Linux native qualification plan (2026-10-02) + +Use the supported hosted Ubuntu ARM QuickJS build and the built public `runDesktopPlaytest`. Bundle the same factory through explicit standard/atlas/hidden/hidden-during-fade native entries, then use the existing desktop packager to embed the unchanged cooked receiver GLB and manifest. Publish existing measured scene observations into the native-supported state resource seam. Map every authored component predicate to a resource predicate; native readiness uses startup assertions, and the original pixel assertions run unchanged against actual device PNG bytes through the maintained evaluator. Native browser-only assertion families are not advertised as supported. + +Before hosting, test each scenario against the real mailbox capability/preflight implementation using the captured minimal native handshake shape. Reject missing assertions, unready startup, wrong renderer/capture provenance, unnamed adapters, device loss and host console errors. Record source, runtime, bundle, packaged executable and asset hashes and verify them after execution. Software llvmpipe evidence is correctness-only. No native or full acceptance box closes until the actual packaged fixture and its repeated lifecycle have run. + ## Required behavior - A cap of 256 decals in the fixture is enforced with deterministic eviction, and receiver teardown removes its marks. @@ -159,3 +165,10 @@ Update the phase boxes and this PRD only after the named proof runs. Record actu - Across all six scene generations, actual completed-render samples are identical: 259 geometries, 4 textures, 6,522,894 tracked bytes. Cumulative render calls advance 56, 64, 72, 80, 88, 96; every sample has three stable observed intervals. Geometry, texture and byte growth are all zero. The previously reported +1/+576 variation was caused by sampling before stable world rendering; no ceiling changed. This is renderer-reported software-adapter residency over the six observed instances, not a hardware memory benchmark or an unlimited-duration claim. - All ten actual PNGs are byte-identical to already retained captures, so no duplicate image files were added. The inspected [atlas](../../verification/vq11-decals-37003284983/atlas.png), [partial fade](../../verification/vq11-decals-37003284983/fading.png), [empty/expired](../../verification/vq11-decals-36991562324/hidden-decals.png), [static/restarted lifecycle](../../verification/vq11-decals-36989297734/static.png), [motion](../../verification/vq11-decals-36989297734/motion.png) and [teardown](../../verification/vq11-decals-36989297734/teardown.png) files preserve the exact bytes of this run. Positive captures have no error/device-loss diagnostics; each negative control has only its two intended pixel failures. - Phases 1 and 2 and browser Phase 3 are verified. Linux-native remains unexecuted; the existing VQ07 hosted ARM desktop route provides a concrete next integration path, so this is not claimed to be blocked solely by the local missing display/GPU. Both full acceptance boxes, required aggregate gates and final promotion review remain open. PR stays draft; no auto-merge is enabled. + +2026-10-02 native lane local review checkpoint (execution pending): + +- Add an ARM native job after the unchanged browser capture job, reusing the existing ordinary QuickJS host build, dependency cache and package scripts. Four native entry bundles build locally through the maintained bundler. The desktop packager will embed the unchanged receiver GLB/manifest into each debug executable; exact runtime-prefix, runtime/bundle/package/asset hashes are verified. The actual executable's `--version` must identify QuickJS, and the execution process must be Linux ARM64. No unmerged helper from PR396 is required. +- Native receives the same actual scene observations through state resources. Every original component predicate is mapped one-to-one; unsupported browser diagnostics/visual families are replaced by strict native startup/host-console/provenance validation and the unchanged pixel predicates evaluated against actual device PNGs. A crashed host with no `console.json` retains its original report diagnostics. +- Three preflight regressions failed before implementation, then pass: all ten arms pass the maintained mailbox handshake/capability checker using the captured minimal QuickJS handshake and actual shared transport capability set; malformed/unready/unnamed/browser-substitute capture evidence is rejected; actual faded and hidden PNGs preserve the positive/negative visual distinction. These are protocol/unit proofs, not native rendering. +- Fresh validation: 58/58 focused/adjacent tests across seven suites, 141 workflow tests, package typecheck, changed-file lint (four existing fixture complexity warnings), browser fixture build and all four native entry bundle builds pass. Production decals and all authored scenario JSON files are unchanged. Native execution, full acceptance and promotion remain pending. diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts b/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts index 4aafbfd3b9..72d75424d1 100644 --- a/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts @@ -299,6 +299,8 @@ export function createDecalFixture(hideDecals = false, atlasFade = false, hideDu ctx.state.set({ residencyGeneration: generation }); console.info(`TN_DECAL_FIXTURE:${JSON.stringify(observation())}`); } + // Native exposes state resources, not the browser's component assertion family. + ctx.state.set({ ...observation(), residencyGeneration: residencySampled ? generation : 0 }); if (frames % 30 === 0) console.info(`TN_DECAL_FIXTURE:${JSON.stringify(observation())}`); }; } diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/native-atlas.ts b/packages/create-threenative/__tests__/fixtures/bounded-decals/native-atlas.ts new file mode 100644 index 0000000000..dfba335989 --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/native-atlas.ts @@ -0,0 +1,2 @@ +import { createDecalFixture } from "./game.js"; +export default createDecalFixture(false, true); diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/native-hidden-fading.ts b/packages/create-threenative/__tests__/fixtures/bounded-decals/native-hidden-fading.ts new file mode 100644 index 0000000000..5fefa02601 --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/native-hidden-fading.ts @@ -0,0 +1,2 @@ +import { createDecalFixture } from "./game.js"; +export default createDecalFixture(false, true, true); diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/native-hidden.ts b/packages/create-threenative/__tests__/fixtures/bounded-decals/native-hidden.ts new file mode 100644 index 0000000000..9a38758b0c --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/native-hidden.ts @@ -0,0 +1,2 @@ +import { createDecalFixture } from "./game.js"; +export default createDecalFixture(true); diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/native-proof.ts b/packages/create-threenative/__tests__/fixtures/bounded-decals/native-proof.ts new file mode 100644 index 0000000000..4fdb7079fa --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/native-proof.ts @@ -0,0 +1,147 @@ +import assert from "node:assert/strict"; +import { PNG } from "pngjs"; +import { + type IPlaytestScenario, + evaluateRichPlaytestAssertions, +} from "../../../../playtest/dist/index.js"; +import type { IStandalonePlaytestReport } from "../../../../playtest/dist/runner/index.js"; +import { screenshotObservations } from "../../../../playtest/src/runner/steps.js"; + +export const nativeDecalCases = [ + ...["static", "motion", "teardown", "restart", "lifecycle"].map((name) => ({ + name, + scenario: name, + entry: "native.ts", + expectedPixelFailures: [] as string[], + })), + ...["atlas", "fading", "expired"].map((name) => ({ + name, + scenario: name, + entry: "native-atlas.ts", + expectedPixelFailures: [] as string[], + })), + { + name: "hidden-decals", + scenario: "static", + entry: "native-hidden.ts", + expectedPixelFailures: ["visual.1.region.darkPixels", "visual.2.region.darkPixels"], + }, + { + name: "hidden-fading", + scenario: "fading", + entry: "native-hidden-fading.ts", + expectedPixelFailures: ["visual.3.region.darkPixels", "visual.4.region.darkPixels"], + }, +]; + +/** Native supports resources/startup; pixel predicates remain external and unchanged. */ +export function nativeDecalScenario(scenario: IPlaytestScenario): IPlaytestScenario { + assert.ok(scenario.assert?.components?.length, "Native decals require state predicates."); + assert.ok( + scenario.assert.components.every(({ entity }) => entity === "decals"), + "Unmapped native entity.", + ); + assert.ok( + Object.keys(scenario.assert).every((key) => + ["components", "visual", "diagnostics"].includes(key), + ), + "Unmapped native assertion family.", + ); + const { sourcePath: _sourcePath, ...portable } = scenario; + return { + ...portable, + target: "desktop", + artifacts: { screenshots: "after", console: true }, + assert: { + resources: scenario.assert.components.map(({ entity: _entity, component, ...predicate }) => ({ + id: "state", + path: component, + ...predicate, + })), + startup: { maxEnteredMs: 120_000, maxReadyMs: 120_000 }, + }, + }; +} + +export function assertNativeDecalCapture( + report: Pick< + IStandalonePlaytestReport, + "pass" | "runtime" | "target" | "diagnostics" | "capture" | "startup" | "assertionResults" + >, + nativeConsole: unknown, +): void { + assert.equal(report.pass, true, JSON.stringify(report.diagnostics)); + assert.equal(report.runtime, "native"); + assert.equal(report.target, "desktop"); + assert.equal(report.startup?.phase, "ready"); + assert.equal(report.startup.compileSettled, true); + assert.ok(report.assertionResults?.length, "Native assertions are missing."); + assert.ok( + report.assertionResults.every(({ pass }) => pass), + "Native assertion failed.", + ); + const capture = report.capture; + assert.ok(capture, "Native capture provenance is missing."); + assert.equal(capture.target, "desktop"); + assert.equal(capture.captureMethod, "device.screenshot"); + assert.equal(capture.rendererKind, "webgpu"); + assert.deepEqual(capture.viewport, { width: 960, height: 540 }); + assert.ok( + ["vendor", "architecture", "device", "description"].some((key) => { + const value = capture.adapter[key]; + return ( + typeof value === "string" && + value.trim() !== "" && + !/^(unknown|unavailable|0|0x0)$/iu.test(value) + ); + }), + "Native adapter identity is missing.", + ); + for (const diagnostic of report.diagnostics) { + assert.notEqual(diagnostic.severity, "error", diagnostic.code); + assert.doesNotMatch(diagnostic.code, /DEVICE_LOST|BRIDGE_MISSING/u); + } + assert.ok(Array.isArray(nativeConsole) && nativeConsole.length > 0, "Native console is missing."); + for (const entry of nativeConsole) { + assert.ok( + entry && typeof entry.text === "string" && typeof entry.type === "string", + "Malformed native console.", + ); + assert.notEqual(entry.type, "error", entry.text); + assert.doesNotMatch( + entry.text, + /\[FATAL\]|\[WebGPU\].*(?:Device error|Device lost|Failed)|validation error|device(?:[ _-]| was )?lost|(?:Type|Reference|Range|Syntax)Error|TN_(?:NATIVE_START_FAILED|ASSETS_UNRESOLVED|NATIVE_KTX2_UNSUPPORTED|NATIVE_MESH_COMPRESSION_UNSUPPORTED)/iu, + ); + } + assert.ok( + nativeConsole.some((entry) => entry.text.includes("TN_NATIVE_SMOKE_FIRST_FRAME")), + "Native first frame is missing.", + ); +} + +/** Pure PNG measurement using the maintained evaluator, not a native capability claim. */ +export function evaluateDecalPixels(png: Buffer, authored: IPlaytestScenario) { + const image = PNG.sync.read(png); + assert.equal(image.width, authored.viewport.width, "Actual screenshot width"); + assert.equal(image.height, authored.viewport.height, "Actual screenshot height"); + assert.ok(authored.assert?.visual?.length, "Visible decal predicates are missing."); + const scenario = { ...authored, assert: { visual: authored.assert.visual } }; + return evaluateRichPlaytestAssertions({ + scenario, + report: { + diagnostics: [], + distance: 0, + entity: "", + expectMoved: false, + frames: 1, + trivialityOptOuts: [], + observations: { + console: [], + hud: {}, + network: [], + resources: {}, + visual: screenshotObservations(undefined, png, scenario), + }, + }, + }); +} diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/package.json b/packages/create-threenative/__tests__/fixtures/bounded-decals/package.json new file mode 100644 index 0000000000..d221be7a14 --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/package.json @@ -0,0 +1,6 @@ +{ + "name": "vq11-bounded-decals-fixture", + "private": true, + "type": "module", + "threenative": { "nativeEntry": "native.ts" } +} diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/verify-native.ts b/packages/create-threenative/__tests__/fixtures/bounded-decals/verify-native.ts new file mode 100644 index 0000000000..59fef526fb --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/verify-native.ts @@ -0,0 +1,211 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { loadPlaytestScenario } from "../../../../playtest/dist/index.js"; +import { runDesktopPlaytest } from "../../../../playtest/dist/runner/index.js"; +import { + assertNativeDecalCapture, + evaluateDecalPixels, + nativeDecalCases, + nativeDecalScenario, +} from "./native-proof.js"; + +const fixture = dirname(fileURLToPath(import.meta.url)); +const root = resolve(fixture, "../../../../.."); +const artifacts = join(root, "artifacts/vq11-decals-native"); +const bundleOnly = process.argv.includes("--bundle-only"); +const sourceSha = execFileSync("git", ["rev-parse", "HEAD"], { + cwd: root, + encoding: "utf8", +}).trim(); +const hash = (bytes: Buffer) => createHash("sha256").update(bytes).digest("hex"); +const fileHash = async (path: string) => hash(await readFile(path)); +const write = async (path: string, value: unknown) => + writeFile(path, `${JSON.stringify(value, null, 2)}\n`); +const run = (script: string, args: string[]) => + execFileSync(process.execPath, [script, ...args], { cwd: root, stdio: "inherit" }); +await mkdir(artifacts, { recursive: true }); +if (!bundleOnly) + assert.equal( + execFileSync("git", ["status", "--porcelain"], { cwd: root, encoding: "utf8" }).trim(), + "", + "Native capture requires a clean committed source tree.", + ); +const summary: { + sourceSha: string; + pass: boolean; + qualification: string; + results: unknown[]; + identity?: unknown; + error?: string; +} = { + sourceSha, + pass: false, + qualification: + "Packaged Linux ARM QuickJS software-adapter correctness. No mobile or hardware-performance claim.", + results: [], +}; +await write(join(artifacts, "attempt.json"), { + sourceSha, + runId: process.env.GITHUB_RUN_ID, + bundleOnly, +}); +try { + const bundles = new Map(); + for (const entry of new Set(nativeDecalCases.map((item) => item.entry))) { + const bundle = join(artifacts, "variants", entry.replace(".ts", ".js")); + run("packages/runtime-native/scripts/bundle.mjs", [ + "--project", + fixture, + "--entry", + entry, + "--target", + "desktop", + "--native-backend", + "--output", + bundle, + ]); + bundles.set(entry, bundle); + } + if (bundleOnly) { + console.info("VQ11 native bundles built; no native execution or screenshot claim."); + } else { + const runtime = process.env.THREENATIVE_RUNTIME_BINARY; + assert.ok(runtime, "THREENATIVE_RUNTIME_BINARY must name this job's actual native build."); + assert.equal(process.platform, "linux"); + assert.equal(process.arch, "arm64"); + const version = execFileSync(runtime, ["--version"], { encoding: "utf8", timeout: 10_000 }); + assert.match(version, /\+ quickjs build\b/iu, "Expected the actual ARM QuickJS host."); + const capabilities = { engine: "quickjs", version: version.trim() }; + const runtimeBytes = await readFile(runtime); + const assets = ["receiver.glb", "assets.manifest.json"]; + const assetHashes = Object.fromEntries( + await Promise.all( + assets.map(async (name) => [name, await fileHash(join(fixture, "public", name))]), + ), + ); + const config = join(artifacts, "packaging-config.json"); + await write(config, { + app: { id: "com.threenative.vq11", name: "VQ11 bounded decals", version: "1.0.0", build: 1 }, + display: { fullscreen: false, maxFps: 60 }, + window: { width: 960, height: 540, maximized: false, resizable: false }, + renderer: { preferWebGPU: true, resolutionScale: 1 }, + ui: { renderer: "native" }, + }); + const executables = new Map< + string, + { path: string; sha256: string; bundle: string; bundleSha256: string } + >(); + for (const [entry, bundle] of bundles) { + const executable = join(artifacts, "variants", entry.replace(".ts", ""), "decals"); + run("packages/runtime-native/scripts/package-desktop.mjs", [ + "--bundle", + bundle, + "--assets", + join(fixture, "public"), + "--runtime", + runtime, + "--config", + config, + "--mode", + "debug", + "--output", + executable, + ]); + const packaged = await readFile(executable); + assert.equal( + hash(packaged.subarray(0, runtimeBytes.length)), + hash(runtimeBytes), + "Package must carry the exact selected runtime prefix.", + ); + executables.set(entry, { + path: executable, + sha256: hash(packaged), + bundle, + bundleSha256: await fileHash(bundle), + }); + } + summary.identity = { + runtimeSha256: hash(runtimeBytes), + capabilities, + assetHashes, + executables: Object.fromEntries(executables), + }; + await write(join(artifacts, "summary.json"), summary); + for (const item of nativeDecalCases) { + const authored = await loadPlaytestScenario(fixture, `${item.scenario}.playtest.json`); + const directory = join(artifacts, item.name); + await mkdir(directory, { recursive: true }); + const scenarioPath = join(directory, "scenario.playtest.json"); + await write(scenarioPath, nativeDecalScenario(authored)); + await loadPlaytestScenario(root, scenarioPath); + const executable = executables.get(item.entry); + assert.ok(executable, "Native variant was not packaged."); + const report = await runDesktopPlaytest({ + target: "desktop", + projectPath: fixture, + scenarioPath, + artifactDirectory: directory, + desktop: { + executable: executable.path, + hostArgs: ["--windowed", "--width", "960", "--height", "540"], + }, + allowSoftwareAdapter: true, + headless: false, + timeoutMs: 180_000, + trace: false, + url: "", + }); + await write(join(directory, "report.json"), { sourceSha, ...report }); + // A crashed host can omit console.json; preserve/report its real diagnostics first. + const nativeConsole: unknown = await readFile(join(directory, "console.json"), "utf8") + .then(JSON.parse) + .catch(() => undefined); + assertNativeDecalCapture(report, nativeConsole); + const png = await readFile(join(directory, "after.png")); + const pixels = evaluateDecalPixels(png, authored); + const failed = pixels.assertions + .filter(({ pass }) => !pass) + .map(({ id }) => id) + .sort(); + const pass = JSON.stringify(failed) === JSON.stringify(item.expectedPixelFailures); + summary.results.push({ + name: item.name, + pass, + negativeControl: item.expectedPixelFailures.length > 0, + capture: report.capture, + state: report.observations?.resources?.state?.after, + screenshotSha256: hash(png), + pixels, + diagnostics: report.diagnostics, + }); + await write(join(artifacts, "summary.json"), summary); + assert.ok(pass, `Native ${item.name} pixel predicates failed: ${JSON.stringify(failed)}`); + assert.equal( + await fileHash(executable.path), + executable.sha256, + "Packaged executable changed during capture.", + ); + } + assert.equal(await fileHash(runtime), hash(runtimeBytes), "Runtime changed during capture."); + for (const [name, expected] of Object.entries(assetHashes)) + assert.equal( + await fileHash(join(fixture, "public", name)), + expected, + "Receiver assets changed.", + ); + for (const entry of executables.values()) { + assert.equal(await fileHash(entry.path), entry.sha256); + assert.equal(await fileHash(entry.bundle), entry.bundleSha256); + } + summary.pass = true; + } +} catch (error) { + summary.error = error instanceof Error ? (error.stack ?? error.message) : String(error); + throw error; +} finally { + await write(join(artifacts, "summary.json"), summary); +} diff --git a/packages/create-threenative/__tests__/shooter-decals-native.spec.ts b/packages/create-threenative/__tests__/shooter-decals-native.spec.ts new file mode 100644 index 0000000000..5839c16546 --- /dev/null +++ b/packages/create-threenative/__tests__/shooter-decals-native.spec.ts @@ -0,0 +1,147 @@ +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { expect, it } from "vitest"; +import { loadPlaytestScenario, requiredPlaytestCapabilities } from "../../playtest/dist/index.js"; +import { + ANDROID_TRANSPORT_CAPABILITIES, + type IBridgeTransport, + connectPlaytestBridgeTransport, +} from "../../playtest/dist/runner/index.js"; +import { PLAYTEST_PROTOCOL_LIMITS } from "../../playtest/src/protocol.js"; +import { validatePlaytestScenario } from "../../playtest/src/scenario.js"; +import { + assertNativeDecalCapture, + evaluateDecalPixels, + nativeDecalCases, + nativeDecalScenario, +} from "./fixtures/bounded-decals/native-proof.js"; + +const fixture = fileURLToPath(new URL("./fixtures/bounded-decals/", import.meta.url)); +const consoleEvidence = [{ type: "log", text: "TN_NATIVE_SMOKE_FIRST_FRAME" }]; +function report(): Parameters[0] { + return { + pass: true, + runtime: "native", + target: "desktop", + diagnostics: [], + startup: { phase: "ready", progress: 1, compileSettled: true, rule: "compile-settled" }, + assertionResults: [{ id: "resources.0", pass: true }], + capture: { + target: "desktop", + rendererKind: "webgpu", + captureMethod: "device.screenshot", + browserArgs: [], + adapter: { description: "llvmpipe" }, + viewport: { width: 960, height: 540 }, + }, + }; +} + +it("requires genuine native provenance, readiness, nonempty assertions and clean host console", () => { + expect(() => assertNativeDecalCapture(report(), consoleEvidence)).not.toThrow(); + for (const mutation of [ + { pass: false }, + { runtime: "web" }, + { target: "browser" }, + { capture: undefined }, + { startup: undefined }, + { startup: { phase: "ready", progress: 1, compileSettled: false, rule: "compile-settled" } }, + { assertionResults: [] }, + { assertionResults: [{ id: "resources.0", pass: false }] }, + { diagnostics: [{ severity: "warning", code: "TN_PLAYTEST_SOFTWARE_DEVICE_LOST" }] }, + { diagnostics: [{ severity: "error", code: "unrelated" }] }, + ]) + expect(() => + assertNativeDecalCapture( + { ...report(), ...mutation } as Parameters[0], + consoleEvidence, + ), + ).toThrow(); + for (const bad of [ + [], + undefined, + [{ type: "error", text: "unrelated" }], + [{ type: "log", text: "[WebGPU] Device error (Validation): invalid binding" }], + [{ type: "log", text: "TypeError: unavailable API" }], + [{ type: "log", text: "no frame" }], + ]) + expect(() => assertNativeDecalCapture(report(), bad)).toThrow(); + const native = report(); + if (native.capture === undefined) throw new Error("Missing fixture capture"); + native.capture.adapter = { features: "some-feature", description: "unknown" }; + expect(() => assertNativeDecalCapture(native, consoleEvidence)).toThrow(); + native.capture.adapter = { description: "llvmpipe" }; + native.capture.captureMethod = "page.screenshot"; + expect(() => assertNativeDecalCapture(native, consoleEvidence)).toThrow(); + native.capture.captureMethod = "device.screenshot"; + native.capture.viewport.width = 800; + expect(() => assertNativeDecalCapture(native, consoleEvidence)).toThrow(); +}); + +it("preserves all authored predicates and passes actual minimal-native mailbox preflight", async () => { + // Actual shared transport capabilities plus the minimal QuickJS describe shape from PR396. + // This proves protocol admission, not execution on a native renderer. + const transport: IBridgeTransport = { + capabilities: ANDROID_TRANSPORT_CAPABILITIES, + waitForBridge: async () => true, + close: async () => {}, + async call(method: string): Promise { + if (method === "describe") + return { + capabilities: ["runtime.resources", "runtime.fixedStep", "runtime.startup"], + limits: PLAYTEST_PROTOCOL_LIMITS, + name: "@threenative/playtest/three", + protocolVersion: 1, + } as T; + if (method === "ready") return { ready: true } as T; + throw new Error(`Unexpected preflight call ${method}`); + }, + }; + for (const { scenario: name } of nativeDecalCases) { + const authored = await loadPlaytestScenario(fixture, `${name}.playtest.json`); + const native = nativeDecalScenario(authored); + expect(native.assert?.resources).toEqual( + authored.assert?.components?.map(({ entity: _entity, component, ...predicate }) => ({ + id: "state", + path: component, + ...predicate, + })), + ); + expect(native.steps).toEqual(authored.steps); + expect(native.assert?.visual).toBeUndefined(); + expect(requiredPlaytestCapabilities(native, "desktop")).not.toContain("runtime.diagnostics"); + expect(validatePlaytestScenario(native, name).target).toBe("desktop"); + await expect( + connectPlaytestBridgeTransport(transport, native, 1000, "desktop"), + ).resolves.toBeDefined(); + } +}); + +it("uses unchanged visual predicates on actual capture bytes and rejects hidden partial fade", async () => { + const fading = await loadPlaytestScenario(fixture, "fading.playtest.json"); + const actual = await readFile( + new URL("../../../docs/verification/vq11-decals-37003284983/fading.png", import.meta.url), + ); + const hidden = await readFile( + new URL( + "../../../docs/verification/vq11-decals-36991562324/hidden-decals.png", + import.meta.url, + ), + ); + expect(evaluateDecalPixels(actual, fading).assertions.every(({ pass }) => pass)).toBe(true); + expect( + evaluateDecalPixels(hidden, fading) + .assertions.filter(({ pass }) => !pass) + .map(({ id }) => id), + ).toEqual(["visual.3.region.darkPixels", "visual.4.region.darkPixels"]); + expect(() => + evaluateDecalPixels(actual, { ...fading, viewport: { width: 800, height: 540 } }), + ).toThrow(); + expect(() => nativeDecalScenario({ ...fading, assert: {} })).toThrow(); + expect(() => + nativeDecalScenario({ + ...fading, + assert: { ...fading.assert, fps: { min: 60 } }, + } as typeof fading), + ).toThrow(); +}); From 662683fee968fd8668d616b87bfc8173d1d65efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Furtado?= <30300459+jonit-dev@users.noreply.github.com> Date: Fri, 2 Oct 2026 06:31:14 -0700 Subject: [PATCH 11/12] docs(vq11): retain native decal progress and failed residency observation --- .../PRD-VQ-11-bounded-decals.md | 6 + .../lifecycle-report.json | 54 + .../vq11-decals-native-37010879478/motion.png | Bin 0 -> 80429 bytes .../static-console.json | 1234 +++++++++++++++++ .../vq11-decals-native-37010879478/static.png | Bin 0 -> 80893 bytes .../summary.json | 544 ++++++++ .../teardown.png | Bin 0 -> 52974 bytes 7 files changed, 1838 insertions(+) create mode 100644 docs/verification/vq11-decals-native-37010879478/lifecycle-report.json create mode 100644 docs/verification/vq11-decals-native-37010879478/motion.png create mode 100644 docs/verification/vq11-decals-native-37010879478/static-console.json create mode 100644 docs/verification/vq11-decals-native-37010879478/static.png create mode 100644 docs/verification/vq11-decals-native-37010879478/summary.json create mode 100644 docs/verification/vq11-decals-native-37010879478/teardown.png diff --git a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md index 8a9c56f8ea..c75a9e79d6 100644 --- a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md +++ b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md @@ -172,3 +172,9 @@ Update the phase boxes and this PRD only after the named proof runs. Record actu - Native receives the same actual scene observations through state resources. Every original component predicate is mapped one-to-one; unsupported browser diagnostics/visual families are replaced by strict native startup/host-console/provenance validation and the unchanged pixel predicates evaluated against actual device PNGs. A crashed host with no `console.json` retains its original report diagnostics. - Three preflight regressions failed before implementation, then pass: all ten arms pass the maintained mailbox handshake/capability checker using the captured minimal QuickJS handshake and actual shared transport capability set; malformed/unready/unnamed/browser-substitute capture evidence is rejected; actual faded and hidden PNGs preserve the positive/negative visual distinction. These are protocol/unit proofs, not native rendering. - Fresh validation: 58/58 focused/adjacent tests across seven suites, 141 workflow tests, package typecheck, changed-file lint (four existing fixture complexity warnings), browser fixture build and all four native entry bundle builds pass. Production decals and all authored scenario JSON files are unchanged. Native execution, full acceptance and promotion remain pending. + +2026-10-02 first real packaged native progress, incomplete qualification: + +- [Run 37010879478](https://github.com/ThreeNativeHQ/threenative/actions/runs/37010879478), source `563ab23e9448a993013cb0d58c75ab6ab83917c9`, builds the ordinary ARM64 QuickJS/wgpu-native host and packages all four variants with the exact receiver assets. Actual native static, motion, teardown and one-restart cases pass supported resource/startup predicates, strict host console/provenance and every unchanged external pixel predicate at 960 × 540 on named Mesa llvmpipe (LLVM 20.1.2). Artifact `11228900169` SHA-256 `0ee5614f0ec8f859d8a23055062e58792f1c94d2af9c8bfd92b15d0e1f327a55` is verified. +- Exact inspected native images: [static](../../verification/vq11-decals-native-37010879478/static.png), [rigid motion](../../verification/vq11-decals-native-37010879478/motion.png), [receiver teardown](../../verification/vq11-decals-native-37010879478/teardown.png). The native restart PNG is byte-identical to static. [Raw summary and artifact identities](../../verification/vq11-decals-native-37010879478/summary.json) preserve source/runtime/bundle/package/asset hashes and named native adapter/device-screenshot provenance. This is native software correctness progress, not hardware performance. +- Native qualification remains incomplete: the [lifecycle report](../../verification/vq11-decals-native-37010879478/lifecycle-report.json) times out waiting for generation 1 residency readiness. [Actual native console](../../verification/vq11-decals-native-37010879478/static-console.json) shows 256 visible marks/321 placements but `maxDrawCalls=0` at the game-update sample; that update boundary does not retain the browser's per-frame draw counter. The actual render/reset ordering is being traced before changing the observation boundary. No residency limit or pixel predicate is relaxed. Atlas/fade/expiry/hidden native cases were not reached, and all native/full acceptance boxes remain open. diff --git a/docs/verification/vq11-decals-native-37010879478/lifecycle-report.json b/docs/verification/vq11-decals-native-37010879478/lifecycle-report.json new file mode 100644 index 0000000000..647538ed3b --- /dev/null +++ b/docs/verification/vq11-decals-native-37010879478/lifecycle-report.json @@ -0,0 +1,54 @@ +{ + "sourceSha": "563ab23e9448a993013cb0d58c75ab6ab83917c9", + "artifactDirectory": "/home/runner/work/threenative/threenative/artifacts/vq11-decals-native/lifecycle", + "assertionResults": [ + { + "details": { + "consoleErrors": 0, + "networkErrors": 0, + "policy": { + "networkErrorsOptOutReason": "The run target has no network observer; its network observation is hardwired empty, so the default network policy is waived rather than evaluated against nothing.", + "noConsoleErrors": true, + "noNetworkErrors": false, + "noRuntimeDiagnostics": true + }, + "reason": "not-evaluated", + "runtimeDiagnostics": 0 + }, + "id": "diagnostics", + "pass": false + } + ], + "debugColliders": false, + "diagnostics": [ + { + "code": "TN_PLAYTEST_OBSERVATION_UNAVAILABLE", + "fix": { + "instruction": "Increase timeoutMs only when the transition is expected to take longer; otherwise fix the producer or resource path." + }, + "message": "Resource wait for 'state.residencyGeneration' timed out after 30150 ms; predicate {\"gte\":1,\"id\":\"state\",\"path\":\"residencyGeneration\"}, last observation 0.", + "path": "state", + "severity": "error", + "suggestion": "Increase timeoutMs only when the transition is expected to take longer; otherwise fix the producer or resource path." + } + ], + "diagnosticsPolicy": { + "networkErrorsOptOutReason": "The run target has no network observer; its network observation is hardwired empty, so the default network policy is waived rather than evaluated against nothing.", + "noConsoleErrors": true, + "noNetworkErrors": false, + "noRuntimeDiagnostics": true + }, + "distance": 0, + "entity": "", + "expectMoved": false, + "frames": 0, + "input": "", + "movementThreshold": 0, + "pass": false, + "runtime": "native", + "scenario": "vq11-lifecycle", + "target": "desktop", + "trivialityOptOutCount": 0, + "trivialityOptOuts": [], + "url": "/home/runner/work/threenative/threenative/artifacts/vq11-decals-native/variants/native/decals" +} diff --git a/docs/verification/vq11-decals-native-37010879478/motion.png b/docs/verification/vq11-decals-native-37010879478/motion.png new file mode 100644 index 0000000000000000000000000000000000000000..3848ef3886d16b14ddf99b9ec2a0a56db7e078ae GIT binary patch literal 80429 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 F{|DioTK@n5 literal 0 HcmV?d00001 diff --git a/docs/verification/vq11-decals-native-37010879478/summary.json b/docs/verification/vq11-decals-native-37010879478/summary.json new file mode 100644 index 0000000000..6a6291e8dc --- /dev/null +++ b/docs/verification/vq11-decals-native-37010879478/summary.json @@ -0,0 +1,544 @@ +{ + "sourceSha": "563ab23e9448a993013cb0d58c75ab6ab83917c9", + "pass": false, + "qualification": "Packaged Linux ARM QuickJS software-adapter correctness. No mobile or hardware-performance claim.", + "results": [ + { + "name": "static", + "pass": true, + "negativeControl": false, + "capture": { + "adapter": { + "architecture": "unknown", + "description": "Mesa 25.2.8-0ubuntu0.24.04.3 (LLVM 20.1.2)", + "device": "llvmpipe (LLVM 20.1.2, 128 bits)", + "vendor": "llvmpipe" + }, + "browserArgs": [], + "captureMethod": "device.screenshot", + "rendererKind": "webgpu", + "target": "desktop", + "viewport": { + "height": 540, + "width": 960 + } + }, + "state": { + "residencyGeneration": 0, + "generation": 1, + "active": 256, + "capacity": 256, + "created": 321, + "released": 65, + "geometryBytes": 275076, + "maxDrawCalls": 0, + "motionError": 0, + "receiverMotion": 0, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": false, + "atlasFade": false, + "averageOpacity": 1, + "residency": [], + "residencySamples": 0, + "stableResidencyFrames": 0 + }, + "screenshotSha256": "ffb1bc6c5abaf5e4c55c258740c549731e81a8dc9c48c9b449ec7ddd27b95fa8", + "pixels": { + "assertions": [ + { + "id": "visual.0.region", + "pass": true, + "details": { + "after": true, + "expected": { + "equals": true + }, + "minimum": 0.15, + "observed": 0.9999691358024692 + } + }, + { + "id": "visual.1.region", + "pass": true, + "details": { + "after": true, + "expected": { + "equals": true + }, + "minimum": 0.9, + "observed": 0.9995294117647059 + } + }, + { + "id": "visual.1.region.darkPixels", + "pass": true, + "details": { + "maximumLuminance": 0.35, + "minimumDarkPixelRatio": 0.01, + "observedDarkPixelRatio": 0.05331764705882353 + } + }, + { + "id": "visual.1.region.maxDarkPixels", + "pass": true, + "details": { + "maximumDarkPixelRatio": 0.2, + "observedDarkPixelRatio": 0.05331764705882353 + } + }, + { + "id": "visual.2.region", + "pass": true, + "details": { + "after": true, + "expected": { + "equals": true + }, + "minimum": 0.9, + "observed": 0.9997660818713451 + } + }, + { + "id": "visual.2.region.darkPixels", + "pass": true, + "details": { + "maximumLuminance": 0.35, + "minimumDarkPixelRatio": 0.01, + "observedDarkPixelRatio": 0.05719298245614035 + } + }, + { + "id": "visual.2.region.maxDarkPixels", + "pass": true, + "details": { + "maximumDarkPixelRatio": 0.2, + "observedDarkPixelRatio": 0.05719298245614035 + } + }, + { + "details": { + "consoleErrors": 0, + "networkErrors": 0, + "policy": { + "noConsoleErrors": true, + "noNetworkErrors": true, + "noRuntimeDiagnostics": true + }, + "runtimeDiagnostics": 0 + }, + "id": "diagnostics", + "pass": true + } + ], + "diagnostics": [] + }, + "diagnostics": [] + }, + { + "name": "motion", + "pass": true, + "negativeControl": false, + "capture": { + "adapter": { + "architecture": "unknown", + "description": "Mesa 25.2.8-0ubuntu0.24.04.3 (LLVM 20.1.2)", + "device": "llvmpipe (LLVM 20.1.2, 128 bits)", + "vendor": "llvmpipe" + }, + "browserArgs": [], + "captureMethod": "device.screenshot", + "rendererKind": "webgpu", + "target": "desktop", + "viewport": { + "height": 540, + "width": 960 + } + }, + "state": { + "residencyGeneration": 0, + "generation": 1, + "active": 256, + "capacity": 256, + "created": 321, + "released": 65, + "geometryBytes": 275076, + "maxDrawCalls": 0, + "motionError": 0, + "receiverMotion": 0.7549778411745116, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": false, + "atlasFade": false, + "averageOpacity": 1, + "residency": [], + "residencySamples": 0, + "stableResidencyFrames": 0 + }, + "screenshotSha256": "a80813b0c2ebdff2c817664a8780b59396bbec4844bacfba5ac25998fe001c99", + "pixels": { + "assertions": [ + { + "id": "visual.0.region", + "pass": true, + "details": { + "after": true, + "expected": { + "equals": true + }, + "minimum": 0.15, + "observed": 0.9999729938271605 + } + }, + { + "id": "visual.1.region", + "pass": true, + "details": { + "after": true, + "expected": { + "equals": true + }, + "minimum": 0.9, + "observed": 0.9995294117647059 + } + }, + { + "id": "visual.1.region.darkPixels", + "pass": true, + "details": { + "maximumLuminance": 0.35, + "minimumDarkPixelRatio": 0.01, + "observedDarkPixelRatio": 0.05331764705882353 + } + }, + { + "id": "visual.1.region.maxDarkPixels", + "pass": true, + "details": { + "maximumDarkPixelRatio": 0.2, + "observedDarkPixelRatio": 0.05331764705882353 + } + }, + { + "id": "visual.2.region", + "pass": true, + "details": { + "after": true, + "expected": { + "equals": true + }, + "minimum": 0.9, + "observed": 0.9996491228070176 + } + }, + { + "id": "visual.2.region.darkPixels", + "pass": true, + "details": { + "maximumLuminance": 0.35, + "minimumDarkPixelRatio": 0.01, + "observedDarkPixelRatio": 0.05309941520467836 + } + }, + { + "id": "visual.2.region.maxDarkPixels", + "pass": true, + "details": { + "maximumDarkPixelRatio": 0.2, + "observedDarkPixelRatio": 0.05309941520467836 + } + }, + { + "details": { + "consoleErrors": 0, + "networkErrors": 0, + "policy": { + "noConsoleErrors": true, + "noNetworkErrors": true, + "noRuntimeDiagnostics": true + }, + "runtimeDiagnostics": 0 + }, + "id": "diagnostics", + "pass": true + } + ], + "diagnostics": [] + }, + "diagnostics": [] + }, + { + "name": "teardown", + "pass": true, + "negativeControl": false, + "capture": { + "adapter": { + "architecture": "unknown", + "description": "Mesa 25.2.8-0ubuntu0.24.04.3 (LLVM 20.1.2)", + "device": "llvmpipe (LLVM 20.1.2, 128 bits)", + "vendor": "llvmpipe" + }, + "browserArgs": [], + "captureMethod": "device.screenshot", + "rendererKind": "webgpu", + "target": "desktop", + "viewport": { + "height": 540, + "width": 960 + } + }, + "state": { + "residencyGeneration": 0, + "generation": 1, + "active": 128, + "capacity": 256, + "created": 321, + "released": 193, + "geometryBytes": 136188, + "maxDrawCalls": 0, + "motionError": 0, + "receiverMotion": 0, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": true, + "atlasFade": false, + "averageOpacity": 1, + "residency": [], + "residencySamples": 0, + "stableResidencyFrames": 0 + }, + "screenshotSha256": "2a85f1cb78de0bd06f79534704b44170397c6a299d9b5709dd76c560c08f6880", + "pixels": { + "assertions": [ + { + "id": "visual.0.region", + "pass": true, + "details": { + "after": true, + "expected": { + "equals": true + }, + "minimum": 0.15, + "observed": 0.9999807098765432 + } + }, + { + "id": "visual.1.region", + "pass": true, + "details": { + "after": true, + "expected": { + "equals": true + }, + "minimum": 0.9, + "observed": 0.9995294117647059 + } + }, + { + "id": "visual.1.region.darkPixels", + "pass": true, + "details": { + "maximumLuminance": 0.35, + "minimumDarkPixelRatio": 0.01, + "observedDarkPixelRatio": 0.05331764705882353 + } + }, + { + "id": "visual.1.region.maxDarkPixels", + "pass": true, + "details": { + "maximumDarkPixelRatio": 0.2, + "observedDarkPixelRatio": 0.05331764705882353 + } + }, + { + "details": { + "consoleErrors": 0, + "networkErrors": 0, + "policy": { + "noConsoleErrors": true, + "noNetworkErrors": true, + "noRuntimeDiagnostics": true + }, + "runtimeDiagnostics": 0 + }, + "id": "diagnostics", + "pass": true + } + ], + "diagnostics": [] + }, + "diagnostics": [] + }, + { + "name": "restart", + "pass": true, + "negativeControl": false, + "capture": { + "adapter": { + "architecture": "unknown", + "description": "Mesa 25.2.8-0ubuntu0.24.04.3 (LLVM 20.1.2)", + "device": "llvmpipe (LLVM 20.1.2, 128 bits)", + "vendor": "llvmpipe" + }, + "browserArgs": [], + "captureMethod": "device.screenshot", + "rendererKind": "webgpu", + "target": "desktop", + "viewport": { + "height": 540, + "width": 960 + } + }, + "state": { + "residencyGeneration": 0, + "generation": 2, + "active": 256, + "capacity": 256, + "created": 321, + "released": 65, + "geometryBytes": 275076, + "maxDrawCalls": 0, + "motionError": 0, + "receiverMotion": 0, + "lodHit": true, + "renderTriangles": 2, + "receiverRemoved": false, + "atlasFade": false, + "averageOpacity": 1, + "residency": [], + "residencySamples": 0, + "stableResidencyFrames": 0 + }, + "screenshotSha256": "ffb1bc6c5abaf5e4c55c258740c549731e81a8dc9c48c9b449ec7ddd27b95fa8", + "pixels": { + "assertions": [ + { + "id": "visual.0.region", + "pass": true, + "details": { + "after": true, + "expected": { + "equals": true + }, + "minimum": 0.15, + "observed": 0.9999691358024692 + } + }, + { + "id": "visual.1.region", + "pass": true, + "details": { + "after": true, + "expected": { + "equals": true + }, + "minimum": 0.9, + "observed": 0.9995294117647059 + } + }, + { + "id": "visual.1.region.darkPixels", + "pass": true, + "details": { + "maximumLuminance": 0.35, + "minimumDarkPixelRatio": 0.01, + "observedDarkPixelRatio": 0.05331764705882353 + } + }, + { + "id": "visual.1.region.maxDarkPixels", + "pass": true, + "details": { + "maximumDarkPixelRatio": 0.2, + "observedDarkPixelRatio": 0.05331764705882353 + } + }, + { + "id": "visual.2.region", + "pass": true, + "details": { + "after": true, + "expected": { + "equals": true + }, + "minimum": 0.9, + "observed": 0.9997660818713451 + } + }, + { + "id": "visual.2.region.darkPixels", + "pass": true, + "details": { + "maximumLuminance": 0.35, + "minimumDarkPixelRatio": 0.01, + "observedDarkPixelRatio": 0.05719298245614035 + } + }, + { + "id": "visual.2.region.maxDarkPixels", + "pass": true, + "details": { + "maximumDarkPixelRatio": 0.2, + "observedDarkPixelRatio": 0.05719298245614035 + } + }, + { + "details": { + "consoleErrors": 0, + "networkErrors": 0, + "policy": { + "noConsoleErrors": true, + "noNetworkErrors": true, + "noRuntimeDiagnostics": true + }, + "runtimeDiagnostics": 0 + }, + "id": "diagnostics", + "pass": true + } + ], + "diagnostics": [] + }, + "diagnostics": [] + } + ], + "identity": { + "runtimeSha256": "da16138cc37938a14ce6831476aa58972a0d469ba2172b6a06faf6d1d56c6042", + "capabilities": { + "engine": "quickjs", + "version": "TN_COLD_START:{\"segment\":\"process\",\"atMs\":0.000}\nMystral Native Runtime v0.3.4\nNative WebGPU JS runtime - wgpu-native + quickjs build" + }, + "assetHashes": { + "receiver.glb": "7ed6179e17f75f3f60576f72968140773c7f65cb1b8d760240576ac4092c05c3", + "assets.manifest.json": "3b833a9fc96c4f3ef939840f2154f3e40aa95d4beb6cda965833b54f43cde1dc" + }, + "executables": { + "native.ts": { + "path": "/home/runner/work/threenative/threenative/artifacts/vq11-decals-native/variants/native/decals", + "sha256": "623b916c245470157308c7203472cffaa7a8860b88093697e16305577af95ca7", + "bundle": "/home/runner/work/threenative/threenative/artifacts/vq11-decals-native/variants/native.js", + "bundleSha256": "69b662ca548cd31bd6848acdfd086befd22a612bbb396caf9e8c9e5da27d33ee" + }, + "native-atlas.ts": { + "path": "/home/runner/work/threenative/threenative/artifacts/vq11-decals-native/variants/native-atlas/decals", + "sha256": "681760e85faa4cc1e0d18b67f1f05bb0b6cf65d4910d8a79a9ec9d96630426aa", + "bundle": "/home/runner/work/threenative/threenative/artifacts/vq11-decals-native/variants/native-atlas.js", + "bundleSha256": "ebe516e667d7d9da27f08cd2964a4a8a520cc849b2251261f781f026de47a27b" + }, + "native-hidden.ts": { + "path": "/home/runner/work/threenative/threenative/artifacts/vq11-decals-native/variants/native-hidden/decals", + "sha256": "165d1ce5cf07db055c92947644f840efaca58e4b75bf4e8e24fc4967f61eed28", + "bundle": "/home/runner/work/threenative/threenative/artifacts/vq11-decals-native/variants/native-hidden.js", + "bundleSha256": "43f5d19cf13531736384038fa9e372f90c45f7d2558335fa717070412acac08c" + }, + "native-hidden-fading.ts": { + "path": "/home/runner/work/threenative/threenative/artifacts/vq11-decals-native/variants/native-hidden-fading/decals", + "sha256": "6bcf02f0fd25ccc8ef2cebfc2210db64ad88b7328f8107f6ea685bd36bd97527", + "bundle": "/home/runner/work/threenative/threenative/artifacts/vq11-decals-native/variants/native-hidden-fading.js", + "bundleSha256": "b451d8a9c4982b9e22c7f82c40d02d879037dc78f8dadecb459c60d82022f7c5" + } + } + }, + "error": "AssertionError [ERR_ASSERTION]: [{\"code\":\"TN_PLAYTEST_OBSERVATION_UNAVAILABLE\",\"fix\":{\"instruction\":\"Increase timeoutMs only when the transition is expected to take longer; otherwise fix the producer or resource path.\"},\"message\":\"Resource wait for 'state.residencyGeneration' timed out after 30150 ms; predicate {\\\"gte\\\":1,\\\"id\\\":\\\"state\\\",\\\"path\\\":\\\"residencyGeneration\\\"}, last observation 0.\",\"path\":\"state\",\"severity\":\"error\",\"suggestion\":\"Increase timeoutMs only when the transition is expected to take longer; otherwise fix the producer or resource path.\"}]\n\nfalse !== true\n\n at assertNativeDecalCapture (/home/runner/work/threenative/threenative/packages/create-threenative/__tests__/fixtures/bounded-decals/native-proof.ts:73:10)\n at (/home/runner/work/threenative/threenative/packages/create-threenative/__tests__/fixtures/bounded-decals/verify-native.ts:167:7)" +} diff --git a/docs/verification/vq11-decals-native-37010879478/teardown.png b/docs/verification/vq11-decals-native-37010879478/teardown.png new file mode 100644 index 0000000000000000000000000000000000000000..1823155528e7d8e51565e6d4b47b5c13f4a13ea9 GIT binary patch literal 52974 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

@c9@2#xq=ZS_6 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 rF{ueQjI~%9@Su Date: Fri, 2 Oct 2026 06:47:38 -0700 Subject: [PATCH 12/12] test(vq11): observe completed native decal renders across counter resets Sample draw counters after the ordinary world render and consume each snapshot once on the next game update. Preserve the original 256-draw, stable-interval and zero-growth gates. Includes a real Three Info.reset regression and strict missing/repeated snapshot controls. Independently reviewed at local 185b1ef7; actual repaired native qualification remains pending. --- .../PRD-VQ-11-bounded-decals.md | 8 ++ .../__tests__/fixtures/bounded-decals/game.ts | 45 ++++++--- .../__tests__/shooter-decals-fixture.spec.ts | 96 ++++++++++++++++++- 3 files changed, 132 insertions(+), 17 deletions(-) diff --git a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md index c75a9e79d6..72c669a526 100644 --- a/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md +++ b/docs/PRDs/batch-2026-10-01-visual-quality/PRD-VQ-11-bounded-decals.md @@ -178,3 +178,11 @@ Update the phase boxes and this PRD only after the named proof runs. Record actu - [Run 37010879478](https://github.com/ThreeNativeHQ/threenative/actions/runs/37010879478), source `563ab23e9448a993013cb0d58c75ab6ab83917c9`, builds the ordinary ARM64 QuickJS/wgpu-native host and packages all four variants with the exact receiver assets. Actual native static, motion, teardown and one-restart cases pass supported resource/startup predicates, strict host console/provenance and every unchanged external pixel predicate at 960 × 540 on named Mesa llvmpipe (LLVM 20.1.2). Artifact `11228900169` SHA-256 `0ee5614f0ec8f859d8a23055062e58792f1c94d2af9c8bfd92b15d0e1f327a55` is verified. - Exact inspected native images: [static](../../verification/vq11-decals-native-37010879478/static.png), [rigid motion](../../verification/vq11-decals-native-37010879478/motion.png), [receiver teardown](../../verification/vq11-decals-native-37010879478/teardown.png). The native restart PNG is byte-identical to static. [Raw summary and artifact identities](../../verification/vq11-decals-native-37010879478/summary.json) preserve source/runtime/bundle/package/asset hashes and named native adapter/device-screenshot provenance. This is native software correctness progress, not hardware performance. - Native qualification remains incomplete: the [lifecycle report](../../verification/vq11-decals-native-37010879478/lifecycle-report.json) times out waiting for generation 1 residency readiness. [Actual native console](../../verification/vq11-decals-native-37010879478/static-console.json) shows 256 visible marks/321 placements but `maxDrawCalls=0` at the game-update sample; that update boundary does not retain the browser's per-frame draw counter. The actual render/reset ordering is being traced before changing the observation boundary. No residency limit or pixel predicate is relaxed. Atlas/fade/expiry/hidden native cases were not reached, and all native/full acceptance boxes remain open. + +### Native residency observation correction — 2026-10-02 + +The first native run's generation-1 timeout is an observation defect, not evidence of a missing visible render: all four completed native captures passed the authored pixel gates. Three's `Info.reset()` zeros `render.drawCalls` but retains cumulative `render.calls`; its internal Animation RAF can reset the former before the fixture's update. Core also resets metrics before the world pass. This ordinary fixture's existing `Scene.render` hook runs after the world render returns. The native host then replays/submits/presents the RAF command stream in `endDawnFrame` before its next timer/mailbox/update iteration (`packages/runtime-native/src/runtime.cpp`). + +The fixture now snapshots the real renderer counters and resource tuple in that post-world hook, then consumes the snapshot once on the next update. Missing snapshots, repeated hooks without new render calls and simulation ticks alone cannot advance residency. The original post-LOD hit, 2-triangle receiver, at-least-256-draw requirement, three stable intervals and all zero-growth ceilings are unchanged. Scene exit removes the capture closure. This is a fixture observation correction; no engine loop, renderer counter or production decal behavior changes. + +The actual fixture scene regression uses the real loader/picker/LOD path and Three `Info.update/reset`, with CPU-supplied memory measurements (no GPU claim). It reproduces the reset-before-update failure before the fix, then proves 256 draws qualify while 0/255, missing snapshots and repeated snapshots do not. Focused decal/lifetime/native-proof and adjacent template runtime suites pass 57 tests; the package typecheck, four native bundles and browser fixture build pass. Biome has no errors and retains the four existing fixture complexity warnings. The root `pnpm test` attempt still fails before test execution at tsx's Unix IPC listener with `EPERM`; this does not establish a full-suite pass. Native and full acceptance remain open until the corrected exact source runs all native cases, including unchanged visible-effect controls and repeated residency. diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts b/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts index 72d75424d1..a93d66e366 100644 --- a/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts @@ -47,6 +47,7 @@ export function createDecalFixture(hideDecals = false, atlasFade = false, hideDu class DecalRoom extends Scene { #source: Object3D | undefined; #dispose = () => {}; + #observeRenderedFrame = () => {}; override async load(ctx: ICtx): Promise { const model = await ctx.assets.model<{ scene: Object3D }>("receiver.glb"); @@ -169,6 +170,22 @@ export function createDecalFixture(hideDecals = false, atlasFade = false, hideDu textures: -1, bytes: -1, }; + let rendered: (ResidencySample & { drawCalls: number; ready: boolean }) | undefined; + // This fixture has no depth-coupled output: Scene.render runs after its world draw. + // Capture before Three's Animation callback can reset per-frame draw counters again. + this.#observeRenderedFrame = () => { + const info = (ctx.renderer.raw as WebGPURenderer).info; + if (info.render.calls === residencyObservation.renderCalls) return; + rendered = { + renderCalls: info.render.calls, + drawCalls: info.render.drawCalls, + geometries: info.memory.geometries, + textures: info.memory.textures, + bytes: info.memory.total, + ready: + lodHit && left.geometry.index?.count === 6 && info.render.drawCalls >= field.capacity, + }; + }; const base = baseGeometryOf(left); if (base.index?.count !== 384) throw new Error("Decal fixture LOD0 must contain 128 triangles."); @@ -234,10 +251,14 @@ export function createDecalFixture(hideDecals = false, atlasFade = false, hideDu for (const material of materials) material.dispose(); }; return (frame: ICtx, dt: number) => { - maxDrawCalls = Math.max( - maxDrawCalls, - (ctx.renderer.raw as WebGPURenderer).info.render.drawCalls, - ); + // Consume only on the next update. Native replays/submits the previous RAF stream + // before its next timer/mailbox/update iteration; fixed ticks alone cannot advance this. + if (rendered !== undefined) { + const { drawCalls, ready, ...sample } = rendered; + maxDrawCalls = Math.max(maxDrawCalls, drawCalls); + observeResidency(residencyObservation, sample, ready); + rendered = undefined; + } if (frame.input.justPressed("motion")) moving = !moving; if (frame.input.justPressed("fade")) { fading = true; @@ -277,17 +298,6 @@ export function createDecalFixture(hideDecals = false, atlasFade = false, hideDu lodHit = true; } frames += 1; - const info = (ctx.renderer.raw as WebGPURenderer).info; - observeResidency( - residencyObservation, - { - renderCalls: info.render.calls, - geometries: info.memory.geometries, - textures: info.memory.textures, - bytes: info.memory.total, - }, - lodHit && left.geometry.index?.count === 6 && info.render.drawCalls >= field.capacity, - ); if (!residencySampled && residencyObservation.stableFrames >= 3) { residency.push({ generation, @@ -305,7 +315,12 @@ export function createDecalFixture(hideDecals = false, atlasFade = false, hideDu }; } + override render(): void { + this.#observeRenderedFrame(); + } + override exit(): void { + this.#observeRenderedFrame = () => {}; this.#dispose(); this.#dispose = () => {}; } diff --git a/packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts b/packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts index 18243411d5..a607a9ec55 100644 --- a/packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts +++ b/packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts @@ -1,7 +1,9 @@ import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { Mesh, PerspectiveCamera, Scene, Vector2, Vector3 } from "three"; +import Info from "three/src/renderers/common/Info.js"; import { afterEach, expect, it, vi } from "vitest"; +import * as fixtureCore from "../../core/dist/index.js"; import { createAssetLoader } from "../../core/src/assets.js"; import { baseGeometryOf, updateModelLods } from "../../core/src/model-lod.js"; import { ScenePicker } from "../../core/src/picking.js"; @@ -9,9 +11,12 @@ import { evaluateRichPlaytestAssertions } from "../../playtest/src/assertion-eva import { screenshotObservations } from "../../playtest/src/runner/steps.js"; import { loadPlaytestScenario } from "../../playtest/src/scenario.js"; import { DecalField, bulletHoleTexture } from "../templates/shooter/src/render/decals.js"; -import { observeResidency } from "./fixtures/bounded-decals/game.js"; +import { createDecalFixture, observeResidency } from "./fixtures/bounded-decals/game.js"; -afterEach(() => vi.unstubAllGlobals()); +afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); +}); it("requires stable completed render intervals rather than simulated ticks for residency", () => { const previous = { renderCalls: 10, stableFrames: 0, geometries: -1, textures: -1, bytes: -1 }; @@ -35,6 +40,93 @@ it("requires stable completed render intervals rather than simulated ticks for r ); }); +it.each([0, 255, 256])( + "observes the real fixture render before Three resets counters, requiring 256 draws (%i)", + async (draws) => { + const expectedGeneration = draws >= 256 ? 1 : 0; + let config: Parameters[0] | undefined; + vi.spyOn(fixtureCore, "defineGame").mockImplementation((value) => { + config = value; + return {} as never; + }); + createDecalFixture(); + const Room = config?.scenes.room; + if (Room === undefined) throw new Error("Missing actual fixture scene"); + const room = new Room(); + const glb = await readFile( + new URL("./fixtures/bounded-decals/public/receiver.glb", import.meta.url), + ); + const manifest = await readFile( + new URL("./fixtures/bounded-decals/public/assets.manifest.json", import.meta.url), + "utf8", + ); + vi.stubGlobal("fetch", async (url: string) => + url.endsWith("assets.manifest.json") + ? new Response(manifest, { headers: { "content-type": "application/json" } }) + : new Response(glb, { headers: { "content-type": "model/gltf-binary" } }), + ); + const scene = new Scene(); + const camera = new PerspectiveCamera(48, 16 / 9, 0.1, 100); + const info = new Info(); + Object.assign(info.memory, { geometries: 259, textures: 4, total: 6500000 }); + const state: Record = {}; + const picker = new ScenePicker({ + camera, + pointer: () => new Vector2(), + scene, + viewport: {} as never, + }); + const ctx = { + assets: fixtureCore.createAssetLoader({ basePath: "https://fixture.invalid" }), + camera, + scene, + add: (object: Mesh) => scene.add(object), + entities: { add: () => {} }, + input: { justPressed: () => false }, + raycast: picker.raycast.bind(picker), + renderer: { raw: { info } }, + state: { set: (value: Record) => Object.assign(state, value) }, + } as unknown as fixtureCore.ICtx; + vi.spyOn(console, "info").mockImplementation(() => {}); + await room.load(ctx); + const update = room.enter(ctx); + if (typeof update !== "function") throw new Error("Missing actual fixture update"); + try { + camera.updateMatrixWorld(true); + fixtureCore.updateModelLods(scene, camera, 540); + update(ctx, 0); // The actual post-LOD hit must exist before residency can qualify. + expect(state.lodHit).toBe(true); + const receiver = scene.children.find((object) => object instanceof Mesh); + if (!(receiver instanceof Mesh)) throw new Error("Missing actual receiver"); + info.render.calls = 1; + for (let draw = 0; draw < 256; draw += 1) info.update(receiver, 6, 1); + for (let tick = 0; tick < 10; tick += 1) update(ctx, 0); + expect(state.stableResidencyFrames).toBe(0); // Live counters alone are not a world snapshot. + for (let interval = 1; interval <= 3; interval += 1) { + info.reset(); + info.render.calls += 1; + for (let draw = 0; draw < draws; draw += 1) info.update(receiver, 6, 1); + room.render(ctx); // The framework invokes this only after this ordinary world render. + expect(state.residencyGeneration).toBe(0); // Do not publish before the next update. + info.reset(); // The native RAF ordering can erase per-frame draws before that update. + expect(info.render.drawCalls).toBe(0); + expect(info.render.calls).toBe(interval + 1); + for (let tick = 0; tick < 10; tick += 1) update(ctx, 0); + expect(state.stableResidencyFrames).toBe(expectedGeneration * interval); + room.render(ctx); // A repeated hook without another render call cannot add an interval. + update(ctx, 0); + expect(state.stableResidencyFrames).toBe(expectedGeneration * interval); + } + expect(state.maxDrawCalls).toBe(draws); + expect(state.residencyGeneration).toBe(expectedGeneration); + expect(state.residencySamples).toBe(expectedGeneration); + } finally { + room.exit(ctx); + picker.dispose(); + } + }, +); + async function evaluateCapturedPixels( scenarioName: string, captureName: string,