diff --git a/.github/workflows/integration-decals.yml b/.github/workflows/integration-decals.yml new file mode 100644 index 0000000000..35011a3f2a --- /dev/null +++ b/.github/workflows/integration-decals.yml @@ -0,0 +1,105 @@ +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 + 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/done/PRD-VQ-11-bounded-decals.md b/docs/PRDs/done/PRD-VQ-11-bounded-decals.md new file mode 100644 index 0000000000..d3d576922e --- /dev/null +++ b/docs/PRDs/done/PRD-VQ-11-bounded-decals.md @@ -0,0 +1,208 @@ +--- +prd_contract: v1 +--- + +# PRD-VQ-11 — Impact and environment decals use a bounded portable lifecycle + +**Status:** COMPLETE — 2026-10-02 at `8f85fff19913c955a44b7b85d2cc117b972bd19c`. All six phase items and both acceptance criteria are verified by CPU ownership tests plus actual browser WebGPU and packaged Linux ARM64 QuickJS/wgpu captures. The merge with develop `924b92f8` changes the playtest runner and is undergoing the same browser/native qualification and required CI before promotion. Software-adapter correctness only; no mobile or hardware-performance claim. +**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. + +### 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. + +### Authored atlas/fade continuation (2026-10-02, CPU-verified; runtime pending) + +Keep this extension in the existing generated `DecalField`. A projected mark may provide `uvRect: [u0, v0, u1, v1]`, normalized and ordered inside the borrowed atlas, plus `fade: { duration, opacity(progress) }`. Map only the owned projected UV buffer; do not clone or rewrite the borrowed texture. The optional duration is positive finite seconds; progress runs from 0 toward 1 through `update(dt)`, and the authored curve must return finite opacity in [0, 1]. At expiry, release only the projected geometry/listener and recycle the slot. Reuse resets opacity; omitted controls keep the existing persistent, full-map appearance. Projection bias must be nonnegative and at most half the supplied projector depth, rejecting invalid input before evicting a live mark. + +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. +- 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 + +- [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 + +- [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. +- [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 + +- [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. +- [x] 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:?}` — [run 37015640669](https://github.com/ThreeNativeHQ/threenative/actions/runs/37015640669) passes all ten native cases at `8f85fff19`, 960 × 540 Mesa llvmpipe, through the built public desktop runner and packaged QuickJS/wgpu host. State predicates, original external pixel gates, both hidden-material controls and strict startup/console/provenance guards pass. + +## Acceptance criteria + +- [x] The supported receiver cases retain their marks under motion and teardown, and resource use is bounded independently of total shots fired. proof: [run 37015640669](https://github.com/ThreeNativeHQ/threenative/actions/runs/37015640669), source `8f85fff19`, passes the complete bounded-decals browser/native sequence; six native generations retain 259 geometries, 4 textures and 6,522,894 tracked bytes with all growth measures zero. The CPU cap regression fires 1,024 impacts through 256 slots and proves exact eviction/final disposal. Rigid transform and removal tests plus actual motion/teardown PNGs verify receiver attachment. +- [x] The feature preserves its documented inactive/fallback path and releases owned resources after repeated lifecycle transitions. proof: 57 focused decal/lifetime/native-proof and adjacent template tests pass, including legacy persistent `place()`, unsupported-receiver refusal, fade expiry/reuse and exact borrower/owner teardown; [run 37015640669](https://github.com/ThreeNativeHQ/threenative/actions/runs/37015640669) verifies scene restart, expiry to zero marks/bytes and six repeated native lifecycles. The default shooter appearance remains unchanged. + +## 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. + +## Qualification limits + +Skinned/deforming receivers remain an explicit v1 refusal and require a separate demand-backed extension. Hosted Chromium SwiftShader and packaged Linux ARM64 QuickJS/wgpu on Mesa llvmpipe qualify the requested browser/native correctness. Mobile and hardware performance are outside this proof. The local container cannot provide GPU/display or tsx Unix IPC execution; hosted exact-source gates supply the actual runtime evidence. Promotion still requires the combined develop-merge run and mandatory CI. + +## Completion record + +All original phase and acceptance items are verified at `8f85fff19`; archived individually under the batch filing rule while sibling PRDs remain active. The dated records below preserve earlier partial and failed attempts as history, not current status. The current qualification record follows them. Promotion gates are tracked separately and are not waived by archival. + +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. + +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. + +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. + +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. 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 [original raw report](https://github.com/ThreeNativeHQ/threenative/blob/e1566472ee439650c74109a92e7fabae1770f71a/docs/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. + +2026-10-02 atlas/fade local review checkpoint (not published or runtime-qualified yet): + +- Thirteen new projection tests failed on the prior implementation and pass with the controls. A further real Registry test spans 20 lifecycles/240 projected buffers; every eviction/expiry/removal/teardown buffer disposal occurs exactly once, and borrowed receiver geometry is retained. The projection suite now has 33 tests. The six focused/adjacent suites pass 53/53 after a non-force merge of develop `416ffd7c`. +- Package typecheck and real Vite fixture build pass. Changed-file lint passes with six complexity warnings (existing texture generator, projection method, and fixture callbacks); no lint rule is relaxed. Eight runtime scenario files validate through the maintained schema. The original visible-mark and hidden-material bounds are unchanged. New atlas/fading/expiry cases use an authored two-cell atlas and a two-second linear curve; six scene instances sample actual renderer geometry/texture counts and total GPU-tracked bytes, requiring no growth across the five warmed instances. These new runtime observations have not run yet. +- The actual combined scaffold test first failed only the shooter hash after merging develop; it measured `c035066c49f50a3c8ff894604a17ca6db3729179d6112e8470cb83f43d6eccc0`, now recorded. The full scaffold/looks/playtest/doctor group has 238 passes and one failure: the starter mobile-assets case cannot resolve Three's Basis transcoder from its temporary scaffold (`TN_ASSETS_TRANSCODER_MISSING`). This is a reported failed gate, not an aggregate pass; the byte-stability test itself passes. +- Fresh `pnpm test` was attempted and fails before test execution at the `tsx` Unix IPC socket with `EPERM`. No root aggregate pass is claimed. The local actual browser run builds, then refuses capture because Xvfb/display is absent; doctor exits with missing `adb`. Current capability checks also find no `/dev/dri`, `/dev/kvm`, `Xvfb` or native host on PATH. +- Native continuation: use the maintained desktop builder with `nativeEntry` pointing to the existing portable `native.ts`, stage the cooked receiver GLB and manifest from `public`, and execute the same cases through the built playtest runner against the resulting host. Native packaging/runtime has not been executed. No platform or residency box is ticked from CPU tests or older browser frames. + +2026-10-02 review correction before publishing atlas/fade: + +- 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. + +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. + +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. + +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. + +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. + +### 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. + + +### Complete browser/native qualification — 2026-10-02 + +[Run 37015640669](https://github.com/ThreeNativeHQ/threenative/actions/runs/37015640669), exact source `8f85fff19913c955a44b7b85d2cc117b972bd19c`, passed both browser and Linux native jobs. All ten native cases pass the maintained guards: static marks, rigid motion, removal, restart, six-generation residency, atlas selection, visible intermediate fade, expiry, hidden marks and hidden-during-fade rejection. Each negative control fails only its two intended authored pixel predicates while its state, startup, console and capture identity remain valid. Actual native partial opacity is approximately 0.475; expiry releases all 321 created projected buffers and retains zero active marks/geometry bytes. + +Six measured native generations each have 259 geometries, 4 textures and 6,522,894 renderer-tracked bytes after three stable completed-render intervals; geometry, texture and byte growth are exactly zero. At the cap the fixture retains 256 marks / 275,076 owned projected bytes and observes a maximum 516 world draw calls. This reports measured cost without a hardware-performance or single-draw claim. Native captures use packaged ARM64 QuickJS/wgpu, Mesa llvmpipe (LLVM 20.1.2, 128 bits), `device.screenshot`, 960 × 540. + +[Minimal provenance](../../verification/vq11-decals-native-37015640669/provenance.json) retains source/build/runtime/asset/PNG hashes and scoped results. Artifact `11231490134` ZIP SHA-256 is `07925a63d4091f2a53d06ef7b178619d775ef9185890949afb4645c83e9d519f`; full raw reports and host logs remain in that workflow artifact. The new [atlas](../../verification/vq11-decals-native-37015640669/atlas.png), [partial fade](../../verification/vq11-decals-native-37015640669/fading.png) and [expired](../../verification/vq11-decals-native-37015640669/expired.png) PNGs are copied unchanged and visually inspected. The other seven captures reuse byte-identical prior/empty frames recorded in provenance. An independent whole-PR review reran 57 tests, all ten native report/console/pixel evaluations and adversarial empty-image controls, and cleared the original implementation and acceptance scope. + +### Combined develop promotion preparation — 2026-10-02 + +Merged develop `924b92f825d602485ef4e682f434b4757a825cd7` without rewriting ancestry. The changed playtest runner was rebuilt with its validator-generation check, tsup/declarations and publint; browser and all four native fixture bundles build successfully. Root `pnpm typecheck` passes serially across the workspace. Root `pnpm lint` passes with existing warnings after formatting evidence JSON. The historical raw report is preserved in its original immutable commit and workflow artifact; the current-tree file contains minimal provenance, as described below. Documentation links (2,429), all 22 generated agent mirrors and the staged evidence budget pass. + +The expanded affected suite has 463 passes and one failure: `create-threenative > keeps the starter's shipped assets mobile-shippable`, `TN_ASSETS_TRANSCODER_MISSING`, because its temporary scaffold cannot resolve the project-relative Three Basis transcoder. Independent review already reproduced that exact fixture failure on unchanged `e1566472`; it remains reported, not skipped or treated as green. The focused decal tests and changed playtest/tone, schema and workflow tests all pass. Root `pnpm build` and `pnpm test` were each attempted and stop at the environment's tsx Unix IPC `EPERM`; the affected package/fixture builds were executed through Node's loader instead. This is not a full-suite or hosted-CI pass. The combined source's actual browser/native rerun and mandatory CI must pass before promotion. + +Additional template/platformer/package/API/docs/mirror drift checks report 99 passes and two initial failures. The pristine-scaffold typecheck passed on rerun with the shared pnpm tool/cache environment (its first attempt used an unavailable default pnpm home); the remaining Xvfb-wrapper test cannot run because Xvfb is not installed and the wrapper correctly refuses blind execution. These environment limits are retained for comparison with the ordinary hosted checks. + +The exact Basis-transcoder test subsequently passes with `NODE_PATH` pointing to this checkout's existing `packages/core/node_modules`, which lets its isolated temporary project resolve the already installed Three transcoder. No production code, test, dependency or expected result changes. Both resolver and pristine-scaffold failures are thus reproduced environment-setup failures with passing scoped reruns; the aggregate IPC and absent-display limits remain. + +Publication privacy: the historical hidden-control report in the current tree is now minimal source/run/artifact/assertion provenance; its original raw report remains available in the immutable earlier commit and original workflow artifact. All original assertion outcomes and screenshots remain preserved. The current proof adds only minimal native provenance and actual PNG bytes. diff --git a/docs/verification/vq11-decals-36989297734/motion.png b/docs/verification/vq11-decals-36989297734/motion.png new file mode 100644 index 0000000000..d2af756406 Binary files /dev/null and b/docs/verification/vq11-decals-36989297734/motion.png differ diff --git a/docs/verification/vq11-decals-36989297734/static.png b/docs/verification/vq11-decals-36989297734/static.png new file mode 100644 index 0000000000..c15d6d481e Binary files /dev/null and b/docs/verification/vq11-decals-36989297734/static.png differ diff --git a/docs/verification/vq11-decals-36989297734/summary.json b/docs/verification/vq11-decals-36989297734/summary.json new file mode 100644 index 0000000000..82172ab59a --- /dev/null +++ b/docs/verification/vq11-decals-36989297734/summary.json @@ -0,0 +1,186 @@ +{ + "sourceSha": "7396738b33a2b3018a2e6235a0e59ca07620a324", + "correctnessOnly": true, + "results": [ + { + "name": "static", + "sourceSha": "7396738b33a2b3018a2e6235a0e59ca07620a324", + "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 + }, + "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": [] + }, + { + "name": "motion", + "sourceSha": "7396738b33a2b3018a2e6235a0e59ca07620a324", + "qualified": true, + "measurement": { + "generation": 1, + "active": 256, + "capacity": 256, + "created": 321, + "released": 65, + "geometryBytes": 275028, + "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": [] + }, + { + "name": "teardown", + "sourceSha": "7396738b33a2b3018a2e6235a0e59ca07620a324", + "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": [] + }, + { + "name": "restart", + "sourceSha": "7396738b33a2b3018a2e6235a0e59ca07620a324", + "qualified": true, + "measurement": { + "generation": 2, + "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/restart/after.png", + "diagnostics": [] + } + ] +} diff --git a/docs/verification/vq11-decals-36989297734/teardown.png b/docs/verification/vq11-decals-36989297734/teardown.png new file mode 100644 index 0000000000..f10178395e Binary files /dev/null and b/docs/verification/vq11-decals-36989297734/teardown.png differ 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 0000000000..6c43259ac7 Binary files /dev/null and b/docs/verification/vq11-decals-36991562324/hidden-decals.png differ 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..5bebf69176 --- /dev/null +++ b/docs/verification/vq11-decals-36991562324/hidden-report.json @@ -0,0 +1,65 @@ +{ + "sourceSha": "4cfc3c2024da1cb37533dc67fd07679e364c27f6", + "runUrl": "https://github.com/ThreeNativeHQ/threenative/actions/runs/36991562324", + "artifactId": 11218994493, + "artifactSha256": "a84190c40b3cdd6679ff9fea858dd6cfe2373646f090192ee531411fbaba1070", + "kind": "historical hidden-material control provenance", + "pass": false, + "expectedFailureIds": ["visual.1.region.darkPixels", "visual.2.region.darkPixels"], + "assertions": [ + { + "id": "visual.0.region", + "pass": true + }, + { + "id": "visual.1.region", + "pass": true + }, + { + "id": "visual.1.region.darkPixels", + "pass": false + }, + { + "id": "visual.1.region.maxDarkPixels", + "pass": true + }, + { + "id": "visual.2.region", + "pass": true + }, + { + "id": "visual.2.region.darkPixels", + "pass": false + }, + { + "id": "visual.2.region.maxDarkPixels", + "pass": true + }, + { + "id": "component.decals.active.value", + "pass": true + }, + { + "id": "component.decals.created.value", + "pass": true + }, + { + "id": "component.decals.geometryBytes.value", + "pass": true + }, + { + "id": "component.decals.renderTriangles.value", + "pass": true + }, + { + "id": "component.decals.generation.value", + "pass": true + }, + { + "id": "diagnostics", + "pass": true + } + ], + "originalReportUrl": "https://github.com/ThreeNativeHQ/threenative/blob/e1566472ee439650c74109a92e7fabae1770f71a/docs/verification/vq11-decals-36991562324/hidden-report.json", + "note": "Minimal public provenance. Full original report remains in the workflow artifact and the immutable historical commit; no environment or diagnostic payload is republished." +} 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 + } + ] + } + } + ] +} diff --git a/docs/verification/vq11-decals-37003284983/atlas.png b/docs/verification/vq11-decals-37003284983/atlas.png new file mode 100644 index 0000000000..e8bb3a7169 Binary files /dev/null and b/docs/verification/vq11-decals-37003284983/atlas.png differ diff --git a/docs/verification/vq11-decals-37003284983/fading.png b/docs/verification/vq11-decals-37003284983/fading.png new file mode 100644 index 0000000000..58991affee Binary files /dev/null and b/docs/verification/vq11-decals-37003284983/fading.png differ diff --git a/docs/verification/vq11-decals-37003284983/summary.json b/docs/verification/vq11-decals-37003284983/summary.json new file mode 100644 index 0000000000..0b91e2cedd --- /dev/null +++ b/docs/verification/vq11-decals-37003284983/summary.json @@ -0,0 +1,1071 @@ +{ + "sourceSha": "021e622380c61695b8c5dc0e7b9ffc604e956b8c", + "correctnessOnly": true, + "results": [ + { + "name": "static", + "sourceSha": "021e622380c61695b8c5dc0e7b9ffc604e956b8c", + "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": false, + "averageOpacity": 1, + "residency": [ + { + "generation": 1, + "geometries": 1, + "textures": 2, + "bytes": 6223938 + } + ], + "residencySamples": 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": "021e622380c61695b8c5dc0e7b9ffc604e956b8c", + "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": [ + { + "generation": 1, + "geometries": 1, + "textures": 2, + "bytes": 6223938 + } + ], + "residencySamples": 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": "021e622380c61695b8c5dc0e7b9ffc604e956b8c", + "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": [ + { + "generation": 1, + "geometries": 1, + "textures": 2, + "bytes": 6223938 + } + ], + "residencySamples": 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": "021e622380c61695b8c5dc0e7b9ffc604e956b8c", + "qualified": true, + "measurement": { + "generation": 2, + "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, + "geometries": 1, + "textures": 2, + "bytes": 6223938 + }, + { + "generation": 2, + "geometries": 259, + "textures": 4, + "bytes": 6522894 + } + ], + "residencySamples": 2, + "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/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": "021e622380c61695b8c5dc0e7b9ffc604e956b8c", + "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": [ + { + "generation": 1, + "geometries": 1, + "textures": 2, + "bytes": 6223938 + } + ], + "residencySamples": 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": "021e622380c61695b8c5dc0e7b9ffc604e956b8c", + "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": [ + { + "generation": 1, + "geometries": 1, + "textures": 2, + "bytes": 6223938 + } + ], + "residencySamples": 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": "021e622380c61695b8c5dc0e7b9ffc604e956b8c", + "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": [ + { + "generation": 1, + "geometries": 1, + "textures": 2, + "bytes": 6223938 + } + ], + "residencySamples": 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": "021e622380c61695b8c5dc0e7b9ffc604e956b8c", + "qualified": false, + "measurement": { + "generation": 6, + "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": [ + { + "generation": 1, + "geometries": 1, + "textures": 2, + "bytes": 6223938 + }, + { + "generation": 2, + "geometries": 258, + "textures": 4, + "bytes": 6522318 + }, + { + "generation": 3, + "geometries": 258, + "textures": 4, + "bytes": 6522318 + }, + { + "generation": 4, + "geometries": 258, + "textures": 4, + "bytes": 6522318 + }, + { + "generation": 5, + "geometries": 259, + "textures": 4, + "bytes": 6522894 + }, + { + "generation": 6, + "geometries": 258, + "textures": 4, + "bytes": 6522318 + } + ], + "residencySamples": 6, + "geometryGrowth": 1, + "textureGrowth": 0, + "memoryGrowth": 576 + }, + "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": [ + { + "code": "TN_PLAYTEST_COMPONENT_ASSERTION_FAILED", + "message": "Component 'geometryGrowth' on entity 'decals' did not satisfy the assertion.", + "observedRuntimePath": "observations.json/components/decals/geometryGrowth", + "severity": "error", + "suggestion": "Expected {\"component\":\"geometryGrowth\",\"entity\":\"decals\",\"allowTrivial\":\"GPU resource counts and bytes must not grow across the five warmed scene instances.\",\"lte\":0}, observed before=undefined after=1. Check the owning script and runtime component synchronization." + }, + { + "code": "TN_PLAYTEST_COMPONENT_ASSERTION_FAILED", + "message": "Component 'memoryGrowth' on entity 'decals' did not satisfy the assertion.", + "observedRuntimePath": "observations.json/components/decals/memoryGrowth", + "severity": "error", + "suggestion": "Expected {\"component\":\"memoryGrowth\",\"entity\":\"decals\",\"allowTrivial\":\"GPU resource counts and bytes must not grow across the five warmed scene instances.\",\"lte\":0}, observed before=undefined after=576. Check the owning script and runtime component synchronization." + } + ], + "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": "021e622380c61695b8c5dc0e7b9ffc604e956b8c", + "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": [ + { + "generation": 1, + "geometries": 1, + "textures": 2, + "bytes": 6223938 + } + ], + "residencySamples": 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-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": "021e622380c61695b8c5dc0e7b9ffc604e956b8c", + "qualified": false, + "negativeControlPassed": 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": 0.49999999999999994, + "residency": [ + { + "generation": 1, + "geometries": 1, + "textures": 2, + "bytes": 6223938 + } + ], + "residencySamples": 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 + } + ] + } + } + ] +} 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 + } + ] + } + } + ] +} 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 0000000000..3848ef3886 Binary files /dev/null and b/docs/verification/vq11-decals-native-37010879478/motion.png differ diff --git a/docs/verification/vq11-decals-native-37010879478/static-console.json b/docs/verification/vq11-decals-native-37010879478/static-console.json new file mode 100644 index 0000000000..9590b4daaf --- /dev/null +++ b/docs/verification/vq11-decals-native-37010879478/static-console.json @@ -0,0 +1,1234 @@ +[ + { + "text": "TN_COLD_START:{\"segment\":\"process\",\"atMs\":0.000}", + "type": "log" + }, + { + "text": "=== ThreeNative Native Runtime ===", + "type": "log" + }, + { + "text": "Version: 0.3.4", + "type": "log" + }, + { + "text": "Script: .threenative/game.js", + "type": "log" + }, + { + "text": "Window: 960x540", + "type": "log" + }, + { + "text": "[Mystral] Initializing runtime...", + "type": "log" + }, + { + "text": "[Mystral] Window: 960x540", + "type": "log" + }, + { + "text": "[Mystral] Presentation cap: 60 fps", + "type": "log" + }, + { + "text": "[Window] Creating window: ThreeNative (960x540)", + "type": "log" + }, + { + "text": "[Window] SDL initialized", + "type": "log" + }, + { + "text": "[Window] Running in hidden mode (MYSTRAL_HEADLESS=1)", + "type": "log" + }, + { + "text": "[Window] Brand icon unavailable in embedded bundle; using compositor default", + "type": "log" + }, + { + "text": "[Window] Actual window size: 960x540", + "type": "log" + }, + { + "text": "[Window] Window created successfully", + "type": "log" + }, + { + "text": "[Mystral] Lifecycle watch installed; backgroundMode=pause", + "type": "log" + }, + { + "text": "[Mystral] Actual window size: 960x540", + "type": "log" + }, + { + "text": "[WebGPU] Initializing...", + "type": "log" + }, + { + "text": "[WebGPU] Instance created", + "type": "log" + }, + { + "text": "[Mystral] Using X11 display: 0xaae1a72e9610 window: 4194350", + "type": "log" + }, + { + "text": "[WebGPU] Creating surface for platform type 4 with display pointer", + "type": "log" + }, + { + "text": "[WebGPU] Creating X11 surface with display: 0xaae1a72e9610 window: 0x40002e", + "type": "log" + }, + { + "text": "[WebGPU] Surface created", + "type": "log" + }, + { + "text": "[WebGPU] Adapter acquired successfully", + "type": "log" + }, + { + "text": "[WebGPU] Adapter: llvmpipe (LLVM 20.1.2, 128 bits)", + "type": "log" + }, + { + "text": "[WebGPU] Vendor: llvmpipe", + "type": "log" + }, + { + "text": "[WebGPU] Backend: Vulkan", + "type": "log" + }, + { + "text": "[WebGPU] adapter feature probe indirect-first-instance: yes", + "type": "log" + }, + { + "text": "[WebGPU] Requesting IndirectFirstInstance feature (supported)", + "type": "log" + }, + { + "text": "[WebGPU] adapter feature probe texture-compression-bc: yes", + "type": "log" + }, + { + "text": "[WebGPU] adapter feature probe texture-compression-etc2: no", + "type": "log" + }, + { + "text": "[WebGPU] adapter feature probe texture-compression-astc: no", + "type": "log" + }, + { + "text": "[WebGPU] adapter feature probe timestamp-query: yes", + "type": "log" + }, + { + "text": "[WebGPU] adapter feature probe rg11b10ufloat-renderable: yes", + "type": "log" + }, + { + "text": "[WebGPU] Device acquired successfully", + "type": "log" + }, + { + "text": "[WebGPU] TN_WEBGPU_FEATURES:{\"timestamp-query\":true,\"texture-compression-bc\":true,\"texture-compression-etc2\":false,\"texture-compression-astc\":false,\"indirect-first-instance\":true,\"rg11b10ufloat-renderable\":true}", + "type": "log" + }, + { + "text": "[WebGPU] Queue acquired", + "type": "log" + }, + { + "text": "[WebGPU] configureSurface called: 960x540", + "type": "log" + }, + { + "text": "[WebGPU] surface_=0xaae1a7351e20, device_=0xaae1a74719d0, adapter_=0xaae1a74553c0", + "type": "log" + }, + { + "text": "[WebGPU] Getting surface capabilities...", + "type": "log" + }, + { + "text": "[WebGPU] Got capabilities: formatCount=2", + "type": "log" + }, + { + "text": "[WebGPU] Available surface formats:", + "type": "log" + }, + { + "text": " [0] = 24", + "type": "log" + }, + { + "text": " [1] = 23", + "type": "log" + }, + { + "text": "[WebGPU] Using surface format: 23", + "type": "log" + }, + { + "text": "[WebGPU] Surface configured: 960x540", + "type": "log" + }, + { + "text": "[WebGPU] Present mode: fifo (vsync=true)", + "type": "log" + }, + { + "text": "[JS] Creating QuickJS engine (fallback)", + "type": "log" + }, + { + "text": "[QuickJS] Creating engine...", + "type": "log" + }, + { + "text": "[QuickJS] Engine created successfully", + "type": "log" + }, + { + "text": "[Mystral] Using JS engine: QuickJS", + "type": "log" + }, + { + "text": "[Mystral] Fetch API initialized (file://, http://, https://)", + "type": "log" + }, + { + "text": "[Mystral] Web Streams API initialized (ReadableStream/WritableStream/TransformStream)", + "type": "log" + }, + { + "text": "[WorkerRegistry] Initialized", + "type": "log" + }, + { + "text": "[Mystral] URL and native Worker facade initialized", + "type": "log" + }, + { + "text": "[DOM] Canvas element created with addEventListener, style, etc.", + "type": "log" + }, + { + "text": "[log] [Mystral] WebP format support: NO", + "type": "log" + }, + { + "text": "[Mystral] DOM event system initialized", + "type": "log" + }, + { + "text": "[Mystral] localStorage initialized: /home/runner/.local/share/mystral/storage/bounded-decals.json", + "type": "log" + }, + { + "text": "[Audio] Web Audio API bindings initialized", + "type": "log" + }, + { + "text": "TN_PIPELINE_CAPTURE:{\"version\":1,\"build\":{\"identity\":\"mystral-native@0.3.4\"},\"adapter\":{\"identity\":\"native:vulkan/llvmpipe/llvmpipe (LLVM 20.1.2, 128 bits)/Mesa 25.2.8-0ubuntu0.24.04.3 (LLVM 20.1.2)\",\"thermal\":\"unavailable\"},\"clock\":{\"source\":\"steady\",\"originMs\":0}}", + "type": "log" + }, + { + "text": "TN_PIPELINE_CACHE:{\"version\":1,\"phase\":\"device\",\"mode\":\"unsupported\",\"featureGranted\":false,\"renderAttached\":0,\"computeAttached\":0,\"emptyBytes\":0,\"serializedBytes\":0,\"load\":\"unavailable\",\"loadedBytes\":0,\"loadMs\":0,\"store\":\"not-attempted\",\"storedBytes\":0,\"storeMs\":0,\"snapshotMs\":0,\"reason\":\"this build's WebGPU backend declares no pipeline cache API\"}", + "type": "log" + }, + { + "text": "TN_SURFACE_FORMAT:{\"native\":\"bgra8unorm\",\"render\":\"bgra8unorm\",\"bridge\":false,\"present\":\"fifo\"}", + "type": "log" + }, + { + "text": "[MystralRT] No hardware RT available, using stub backend", + "type": "log" + }, + { + "text": "[MystralRT] Bindings initialized (backend: none)", + "type": "log" + }, + { + "text": "[EventLoop] libuv 1.51.0 initialized", + "type": "log" + }, + { + "text": "[AsyncHttp] Initialized with curl_multi + libuv", + "type": "log" + }, + { + "text": "[AsyncFile] Initialized with libuv thread pool", + "type": "log" + }, + { + "text": "[FileWatcher] Initialized with libuv fs_event", + "type": "log" + }, + { + "text": "[Mystral] Runtime initialized", + "type": "log" + }, + { + "text": "TN_COLD_START:{\"segment\":\"runtime_created\",\"atMs\":580.324}", + "type": "log" + }, + { + "text": "[Mystral] Evaluating script: threenative-playtest-mailbox.js (246 bytes)", + "type": "log" + }, + { + "text": "TN_COLD_START:{\"segment\":\"compile_begin\",\"atMs\":580.344}", + "type": "log" + }, + { + "text": "TN_COLD_START:{\"segment\":\"compile_complete\",\"atMs\":580.397}", + "type": "log" + }, + { + "text": "TN_COLD_START:{\"segment\":\"execute_begin\",\"atMs\":580.407}", + "type": "log" + }, + { + "text": "TN_COLD_START:{\"segment\":\"execute_complete\",\"atMs\":580.412}", + "type": "log" + }, + { + "text": "[Mystral] Desktop playtest mailbox configured", + "type": "log" + }, + { + "text": "TN_COLD_START:{\"segment\":\"game_eval_begin\",\"atMs\":580.415}", + "type": "log" + }, + { + "text": "[Mystral] Loading script: .threenative/game.js", + "type": "log" + }, + { + "text": "[Fetch] Read 212 bytes from bundle: assets.manifest.json", + "type": "log" + }, + { + "text": "[Mystral] Starting main loop...", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"cf01b5890b1b6101\",\"bytes\":28,\"requestId\":\"ready\",\"requestMethod\":\"\",\"requestOrder\":-1,\"pump\":{\"observed\":true,\"pumpCount\":1,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":853.834,\"maxGapMs\":0.000,\"maxGapAtMs\":-1.000,\"trailingGapMs\":0.988,\"atMs\":854.823,\"longGaps\":[],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "[Fetch] Read 4712 bytes from bundle: receiver.glb", + "type": "log" + }, + { + "text": "[log] TN_NATIVE_SMOKE_READY:webgpu", + "type": "log" + }, + { + "text": "[log] TN_NATIVE_SMOKE_FIRST_FRAME", + "type": "log" + }, + { + "text": "[log] 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": "log" + }, + { + "text": "TN_PIPELINE_EVENT:{\"version\":1,\"eventId\":1,\"pipelineIdentity\":\"native-render-1\",\"programIdentity\":\"00fc0a8ad03630cc/458120a84b226496\",\"kind\":\"render\",\"pass\":\"unknown\",\"mode\":\"sync\",\"status\":\"created\",\"startedMs\":0.00027999999999999998,\"settledMs\":6.401694,\"queueMs\":0,\"serviceMs\":6.4014139999999999,\"wallMs\":6.4014139999999999,\"provenance\":{\"unknown\":true},\"vertex\":{\"hash\":\"00fc0a8ad03630cc\",\"bytes\":1299},\"fragment\":{\"hash\":\"458120a84b226496\",\"bytes\":1595},\"cache\":\"unsupported\",\"label\":\"renderPipeline_outputColorTransform_18\"}", + "type": "log" + }, + { + "text": "[warn] THREE.WebGPURenderer: Timestamp tracking is disabled.", + "type": "warning" + }, + { + "text": "TN_SLOW_FRAME_OPS:{\"replayMs\":421.24257799999998,\"ops\":18,\"top\":[{\"op\":\"submit\",\"n\":2,\"ms\":421.13237700000002},{\"op\":\"render.end\",\"n\":2,\"ms\":0.075402999999999998},{\"op\":\"beginRenderPass\",\"n\":2,\"ms\":0.016678999999999999},{\"op\":\"createCommandEncoder\",\"n\":2,\"ms\":0.008175},{\"op\":\"finish\",\"n\":2,\"ms\":0.0021359999999999999},{\"op\":\"render.setBindGroup\",\"n\":2,\"ms\":0.001872}]}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":1}", + "type": "log" + }, + { + "text": "TN_PIPELINE_FIRST_PRESENT:{\"version\":1,\"boundaryMs\":540.37911299999996}", + "type": "log" + }, + { + "text": "TN_COLD_START:{\"segment\":\"first_frame\",\"atMs\":1411.164}", + "type": "log" + }, + { + "text": "TN_PUMP_SILENCE:{\"observed\":true,\"pumpCount\":1,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":853.834,\"maxGapMs\":0.000,\"maxGapAtMs\":-1.000,\"trailingGapMs\":557.333,\"longGaps\":[],\"droppedLongGaps\":0}", + "type": "log" + }, + { + "text": "TN_STALL_SEGMENTS:{\"toFirstFrameMs\":1411.178,\"frameBeganAtMs\":857.006,\"gapMs\":554.172,\"segments\":{\"pipelineCompile\":{\"ms\":6.468,\"calls\":1},\"shaderCompile\":{\"ms\":0.445,\"calls\":2},\"textureUpload\":{\"ms\":0.183,\"calls\":4},\"bufferUpload\":{\"ms\":0.183,\"calls\":3},\"queueSubmit\":{\"ms\":0.000,\"calls\":0}},\"attributedMs\":7.279,\"residualMs\":546.893,\"attributedShare\":0.0131}", + "type": "log" + }, + { + "text": "TN_PIPELINE_CHECKPOINT:{\"version\":1,\"present\":1,\"requested\":1,\"emitted\":1,\"outstanding\":0}", + "type": "log" + }, + { + "text": "TN_SLOW_END_FRAME:{\"totalMs\":533.67494299999998,\"drainMs\":0.029454000000000001,\"replayMs\":421.29363799999999,\"uiMs\":0.00078399999999999997,\"presentMs\":112.30627699999999,\"pollMs\":0.043206000000000001,\"gpuDrainMs\":0,\"otherMs\":0.0015839999999999999}", + "type": "log" + }, + { + "text": "TN_SLOW_PHASE:{\"phase\":\"endDawnFrame\",\"ms\":533.68100000000004,\"atMs\":1411.2539899999999}", + "type": "log" + }, + { + "text": "TN_SLOW_PHASE:{\"phase\":\"pollEvents\",\"ms\":557.68200000000002,\"atMs\":1411.518525}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"d51d0ecd79920230\",\"bytes\":596,\"requestId\":\"1\",\"requestMethod\":\"describe\",\"requestOrder\":1,\"pump\":{\"observed\":true,\"pumpCount\":2,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":1411.531,\"maxGapMs\":557.697,\"maxGapAtMs\":853.834,\"trailingGapMs\":1122.184,\"atMs\":2533.715,\"longGaps\":[557.697],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "[log] 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,\"atlasFade\":false,\"averageOpacity\":1,\"residency\":[],\"residencySamples\":0,\"stableResidencyFrames\":0}", + "type": "log" + }, + { + "text": "[log] 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,\"atlasFade\":false,\"averageOpacity\":1,\"residency\":[],\"residencySamples\":0,\"stableResidencyFrames\":0}", + "type": "log" + }, + { + "text": "TN_PIPELINE_EVENT:{\"version\":1,\"eventId\":2,\"pipelineIdentity\":\"native-render-2\",\"programIdentity\":\"2eb07d0bdc0da0c6/e792f11cd4d28ee0\",\"kind\":\"render\",\"pass\":\"unknown\",\"mode\":\"sync\",\"status\":\"created\",\"startedMs\":1827.715976,\"settledMs\":1831.635959,\"queueMs\":0,\"serviceMs\":3.919983000000002,\"wallMs\":3.919983000000002,\"provenance\":{\"unknown\":true},\"vertex\":{\"hash\":\"2eb07d0bdc0da0c6\",\"bytes\":1953},\"fragment\":{\"hash\":\"e792f11cd4d28ee0\",\"bytes\":14203},\"cache\":\"unsupported\",\"label\":\"renderPipeline_MeshStandardMaterial_19\"}", + "type": "log" + }, + { + "text": "TN_PIPELINE_EVENT:{\"version\":1,\"eventId\":3,\"pipelineIdentity\":\"native-render-3\",\"programIdentity\":\"2eb07d0bdc0da0c6/e792f11cd4d28ee0\",\"kind\":\"render\",\"pass\":\"unknown\",\"mode\":\"sync\",\"status\":\"created\",\"startedMs\":1869.3319839999999,\"settledMs\":1872.7200909999999,\"queueMs\":0,\"serviceMs\":3.3881069999999909,\"wallMs\":3.3881069999999909,\"provenance\":{\"unknown\":true},\"vertex\":{\"hash\":\"2eb07d0bdc0da0c6\",\"bytes\":1953},\"fragment\":{\"hash\":\"e792f11cd4d28ee0\",\"bytes\":14203},\"cache\":\"unsupported\",\"label\":\"renderPipeline_MeshStandardMaterial_21\"}", + "type": "log" + }, + { + "text": "TN_PIPELINE_EVENT:{\"version\":1,\"eventId\":4,\"pipelineIdentity\":\"native-render-4\",\"programIdentity\":\"effe698ea684daf6/957687f9bd1af7aa\",\"kind\":\"render\",\"pass\":\"unknown\",\"mode\":\"sync\",\"status\":\"created\",\"startedMs\":1884.9268770000001,\"settledMs\":1886.2407189999999,\"queueMs\":0,\"serviceMs\":1.313841999999795,\"wallMs\":1.313841999999795,\"provenance\":{\"unknown\":true},\"vertex\":{\"hash\":\"effe698ea684daf6\",\"bytes\":1482},\"fragment\":{\"hash\":\"957687f9bd1af7aa\",\"bytes\":3294},\"cache\":\"unsupported\",\"label\":\"renderPipeline_MeshBasicMaterial_89\"}", + "type": "log" + }, + { + "text": "TN_PIPELINE_EVENT:{\"version\":1,\"eventId\":5,\"pipelineIdentity\":\"native-render-5\",\"programIdentity\":\"effe698ea684daf6/957687f9bd1af7aa\",\"kind\":\"render\",\"pass\":\"unknown\",\"mode\":\"sync\",\"status\":\"created\",\"startedMs\":1894.9154160000001,\"settledMs\":1896.334564,\"queueMs\":0,\"serviceMs\":1.4191479999999501,\"wallMs\":1.4191479999999501,\"provenance\":{\"unknown\":true},\"vertex\":{\"hash\":\"effe698ea684daf6\",\"bytes\":1482},\"fragment\":{\"hash\":\"957687f9bd1af7aa\",\"bytes\":3294},\"cache\":\"unsupported\",\"label\":\"renderPipeline_MeshBasicMaterial_89\"}", + "type": "log" + }, + { + "text": "TN_SLOW_PHASE:{\"phase\":\"animationFrames\",\"ms\":1754.6659999999999,\"atMs\":3166.772508}", + "type": "log" + }, + { + "text": "TN_SLOW_FRAME_OPS:{\"replayMs\":577.880135,\"ops\":3889,\"top\":[{\"op\":\"submit\",\"n\":2,\"ms\":574.93500800000004},{\"op\":\"render.end\",\"n\":2,\"ms\":1.5463789999999999},{\"op\":\"render.setBindGroup\",\"n\":519,\"ms\":0.363147},{\"op\":\"writeBuffer\",\"n\":1552,\"ms\":0.26949899999999999},{\"op\":\"writeTexture\",\"n\":2,\"ms\":0.23605699999999999},{\"op\":\"render.setVertexBuffer\",\"n\":517,\"ms\":0.16612399999999999}]}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":2}", + "type": "log" + }, + { + "text": "TN_PIPELINE_CHECKPOINT:{\"version\":1,\"present\":2,\"requested\":5,\"emitted\":5,\"outstanding\":0}", + "type": "log" + }, + { + "text": "TN_SLOW_END_FRAME:{\"totalMs\":583.17345999999998,\"drainMs\":0.084194000000000005,\"replayMs\":577.93781100000001,\"uiMs\":0.00084800000000000001,\"presentMs\":5.1016579999999996,\"pollMs\":0.047364999999999997,\"gpuDrainMs\":0,\"otherMs\":0.0015839999999999999}", + "type": "log" + }, + { + "text": "TN_SLOW_PHASE:{\"phase\":\"endDawnFrame\",\"ms\":583.18899999999996,\"atMs\":3749.9956529999999}", + "type": "log" + }, + { + "text": "TN_SLOW_PHASE:{\"phase\":\"pollEvents\",\"ms\":2343.7020000000002,\"atMs\":3755.2437260000002}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"19bbb4273bc7b242\",\"bytes\":168,\"requestId\":\"2\",\"requestMethod\":\"ready\",\"requestOrder\":2,\"pump\":{\"observed\":true,\"pumpCount\":3,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":3755.271,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.611,\"atMs\":3759.882,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "[log] TN_FRAME_HITCH:{\"gapMs\":2343.24,\"uptimeMs\":3823.79,\"wallClock\":1790946973685}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":3}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"78b95576888a52b7\",\"bytes\":72,\"requestId\":\"3\",\"requestMethod\":\"advance\",\"requestOrder\":3,\"pump\":{\"observed\":true,\"pumpCount\":4,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":3858.159,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":21.716,\"atMs\":3879.875,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":4}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"9c7a1dd6c70ad0e0\",\"bytes\":168,\"requestId\":\"4\",\"requestMethod\":\"ready\",\"requestOrder\":4,\"pump\":{\"observed\":true,\"pumpCount\":5,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":3965.014,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.618,\"atMs\":3966.632,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":5}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"af6e700c4e3f1fca\",\"bytes\":71,\"requestId\":\"5\",\"requestMethod\":\"advance\",\"requestOrder\":5,\"pump\":{\"observed\":true,\"pumpCount\":6,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":4051.185,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.065,\"atMs\":4055.251,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":6}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"adcce64027efbb36\",\"bytes\":168,\"requestId\":\"6\",\"requestMethod\":\"ready\",\"requestOrder\":6,\"pump\":{\"observed\":true,\"pumpCount\":7,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":4140.260,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.689,\"atMs\":4141.950,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":7}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"8f2dad1d0d1a33ab\",\"bytes\":71,\"requestId\":\"7\",\"requestMethod\":\"advance\",\"requestOrder\":7,\"pump\":{\"observed\":true,\"pumpCount\":8,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":4238.374,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":3.149,\"atMs\":4241.524,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":8}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"07870d7be2b04b74\",\"bytes\":168,\"requestId\":\"8\",\"requestMethod\":\"ready\",\"requestOrder\":8,\"pump\":{\"observed\":true,\"pumpCount\":9,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":4327.281,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.556,\"atMs\":4328.837,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":9}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"89debabef68245d0\",\"bytes\":71,\"requestId\":\"9\",\"requestMethod\":\"advance\",\"requestOrder\":9,\"pump\":{\"observed\":true,\"pumpCount\":10,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":4414.522,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":3.995,\"atMs\":4418.518,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":10}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"2aad096ed3467ddf\",\"bytes\":169,\"requestId\":\"10\",\"requestMethod\":\"ready\",\"requestOrder\":10,\"pump\":{\"observed\":true,\"pumpCount\":11,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":4504.263,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.653,\"atMs\":4505.915,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":11}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"cdf6d448b2ecb5ae\",\"bytes\":72,\"requestId\":\"11\",\"requestMethod\":\"advance\",\"requestOrder\":11,\"pump\":{\"observed\":true,\"pumpCount\":12,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":4591.308,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":2.871,\"atMs\":4594.180,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":12}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"58699643da11af6d\",\"bytes\":169,\"requestId\":\"12\",\"requestMethod\":\"ready\",\"requestOrder\":12,\"pump\":{\"observed\":true,\"pumpCount\":13,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":4680.403,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.745,\"atMs\":4682.148,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":13}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"dd22e9348bd1a591\",\"bytes\":72,\"requestId\":\"13\",\"requestMethod\":\"advance\",\"requestOrder\":13,\"pump\":{\"observed\":true,\"pumpCount\":14,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":4769.194,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.191,\"atMs\":4773.384,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":14}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"7ce7cd2a47a1920b\",\"bytes\":169,\"requestId\":\"14\",\"requestMethod\":\"ready\",\"requestOrder\":14,\"pump\":{\"observed\":true,\"pumpCount\":15,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":4859.995,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.748,\"atMs\":4861.744,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":15}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"c7dbbaecf9618974\",\"bytes\":72,\"requestId\":\"15\",\"requestMethod\":\"advance\",\"requestOrder\":15,\"pump\":{\"observed\":true,\"pumpCount\":16,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":4949.326,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":3.267,\"atMs\":4952.594,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":16}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"9655fa46e0d42b79\",\"bytes\":169,\"requestId\":\"16\",\"requestMethod\":\"ready\",\"requestOrder\":16,\"pump\":{\"observed\":true,\"pumpCount\":17,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":5038.885,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.641,\"atMs\":5040.526,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":17}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"cad80a66f9a59b2b\",\"bytes\":72,\"requestId\":\"17\",\"requestMethod\":\"advance\",\"requestOrder\":17,\"pump\":{\"observed\":true,\"pumpCount\":18,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":5125.853,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.031,\"atMs\":5129.885,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":18}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"b4268cb69df16ee7\",\"bytes\":169,\"requestId\":\"18\",\"requestMethod\":\"ready\",\"requestOrder\":18,\"pump\":{\"observed\":true,\"pumpCount\":19,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":5217.293,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.795,\"atMs\":5219.087,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":19}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"1dd915307902036a\",\"bytes\":72,\"requestId\":\"19\",\"requestMethod\":\"advance\",\"requestOrder\":19,\"pump\":{\"observed\":true,\"pumpCount\":20,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":5306.854,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":2.911,\"atMs\":5309.765,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":20}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"216b29bcb8e05b1c\",\"bytes\":169,\"requestId\":\"20\",\"requestMethod\":\"ready\",\"requestOrder\":20,\"pump\":{\"observed\":true,\"pumpCount\":21,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":5395.859,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.656,\"atMs\":5397.516,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":21}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"279fed16396dcd86\",\"bytes\":72,\"requestId\":\"21\",\"requestMethod\":\"advance\",\"requestOrder\":21,\"pump\":{\"observed\":true,\"pumpCount\":22,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":5484.510,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.170,\"atMs\":5488.680,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":22}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"ce289c8a103aa272\",\"bytes\":169,\"requestId\":\"22\",\"requestMethod\":\"ready\",\"requestOrder\":22,\"pump\":{\"observed\":true,\"pumpCount\":23,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":5573.880,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.578,\"atMs\":5575.458,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":23}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"72275fb20f5bcf70\",\"bytes\":72,\"requestId\":\"23\",\"requestMethod\":\"advance\",\"requestOrder\":23,\"pump\":{\"observed\":true,\"pumpCount\":24,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":5664.399,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":3.315,\"atMs\":5667.714,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":24}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"93e3cef21e462b50\",\"bytes\":169,\"requestId\":\"24\",\"requestMethod\":\"ready\",\"requestOrder\":24,\"pump\":{\"observed\":true,\"pumpCount\":25,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":5754.409,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.773,\"atMs\":5756.182,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":25}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"279ca0192310c4c7\",\"bytes\":72,\"requestId\":\"25\",\"requestMethod\":\"advance\",\"requestOrder\":25,\"pump\":{\"observed\":true,\"pumpCount\":26,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":5842.468,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.228,\"atMs\":5846.696,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":26}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"8d4bb9970b4ae526\",\"bytes\":169,\"requestId\":\"26\",\"requestMethod\":\"ready\",\"requestOrder\":26,\"pump\":{\"observed\":true,\"pumpCount\":27,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":5932.559,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.921,\"atMs\":5934.481,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":27}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"198f6d5cdea59b3a\",\"bytes\":72,\"requestId\":\"27\",\"requestMethod\":\"advance\",\"requestOrder\":27,\"pump\":{\"observed\":true,\"pumpCount\":28,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":6021.134,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.081,\"atMs\":6025.214,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":28}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"f25763a78bc64424\",\"bytes\":169,\"requestId\":\"28\",\"requestMethod\":\"ready\",\"requestOrder\":28,\"pump\":{\"observed\":true,\"pumpCount\":29,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":6110.901,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.614,\"atMs\":6112.515,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":29}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"c83dac86f371d28d\",\"bytes\":72,\"requestId\":\"29\",\"requestMethod\":\"advance\",\"requestOrder\":29,\"pump\":{\"observed\":true,\"pumpCount\":30,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":6199.084,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.100,\"atMs\":6203.184,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":30}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"47b9e49bdf111499\",\"bytes\":169,\"requestId\":\"30\",\"requestMethod\":\"ready\",\"requestOrder\":30,\"pump\":{\"observed\":true,\"pumpCount\":31,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":6291.960,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.703,\"atMs\":6293.663,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":31}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"823675271fc907f5\",\"bytes\":72,\"requestId\":\"31\",\"requestMethod\":\"advance\",\"requestOrder\":31,\"pump\":{\"observed\":true,\"pumpCount\":32,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":6379.675,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":3.192,\"atMs\":6382.866,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":32}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"31c93cedd3583d2b\",\"bytes\":169,\"requestId\":\"32\",\"requestMethod\":\"ready\",\"requestOrder\":32,\"pump\":{\"observed\":true,\"pumpCount\":33,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":6466.923,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.620,\"atMs\":6468.543,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":33}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"640a7c0fe169f9f6\",\"bytes\":72,\"requestId\":\"33\",\"requestMethod\":\"advance\",\"requestOrder\":33,\"pump\":{\"observed\":true,\"pumpCount\":34,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":6553.725,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.086,\"atMs\":6557.811,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":34}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"dd3f5c7ad916b98d\",\"bytes\":169,\"requestId\":\"34\",\"requestMethod\":\"ready\",\"requestOrder\":34,\"pump\":{\"observed\":true,\"pumpCount\":35,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":6642.950,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.688,\"atMs\":6644.638,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":35}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"93e707672ccc448f\",\"bytes\":72,\"requestId\":\"35\",\"requestMethod\":\"advance\",\"requestOrder\":35,\"pump\":{\"observed\":true,\"pumpCount\":36,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":6729.996,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":2.838,\"atMs\":6732.834,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":36}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"0b65d747bac6607f\",\"bytes\":169,\"requestId\":\"36\",\"requestMethod\":\"ready\",\"requestOrder\":36,\"pump\":{\"observed\":true,\"pumpCount\":37,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":6817.951,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.770,\"atMs\":6819.720,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":37}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"f8f056c81673203c\",\"bytes\":72,\"requestId\":\"37\",\"requestMethod\":\"advance\",\"requestOrder\":37,\"pump\":{\"observed\":true,\"pumpCount\":38,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":6904.066,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.081,\"atMs\":6908.147,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":38}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"ef2e395a3bdfb4a1\",\"bytes\":169,\"requestId\":\"38\",\"requestMethod\":\"ready\",\"requestOrder\":38,\"pump\":{\"observed\":true,\"pumpCount\":39,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":6993.401,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.537,\"atMs\":6994.937,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":39}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"e526ad18bf498959\",\"bytes\":72,\"requestId\":\"39\",\"requestMethod\":\"advance\",\"requestOrder\":39,\"pump\":{\"observed\":true,\"pumpCount\":40,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":7079.625,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":3.197,\"atMs\":7082.822,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":40}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"81f6430656d4a536\",\"bytes\":169,\"requestId\":\"40\",\"requestMethod\":\"ready\",\"requestOrder\":40,\"pump\":{\"observed\":true,\"pumpCount\":41,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":7165.846,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.456,\"atMs\":7167.301,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":41}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"117b49d130b50c15\",\"bytes\":72,\"requestId\":\"41\",\"requestMethod\":\"advance\",\"requestOrder\":41,\"pump\":{\"observed\":true,\"pumpCount\":42,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":7251.308,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":3.776,\"atMs\":7255.084,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":42}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"70a37a9cf5efbae0\",\"bytes\":169,\"requestId\":\"42\",\"requestMethod\":\"ready\",\"requestOrder\":42,\"pump\":{\"observed\":true,\"pumpCount\":43,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":7348.435,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.657,\"atMs\":7350.092,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":43}", + "type": "log" + }, + { + "text": "[log] TN_DECAL_FIXTURE:{\"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}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"e31f767984275c39\",\"bytes\":72,\"requestId\":\"43\",\"requestMethod\":\"advance\",\"requestOrder\":43,\"pump\":{\"observed\":true,\"pumpCount\":44,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":7433.785,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.883,\"atMs\":7438.669,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":44}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"ede510ed6aac9c42\",\"bytes\":169,\"requestId\":\"44\",\"requestMethod\":\"ready\",\"requestOrder\":44,\"pump\":{\"observed\":true,\"pumpCount\":45,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":7522.708,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.542,\"atMs\":7524.250,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":45}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"477caedf27ed1792\",\"bytes\":72,\"requestId\":\"45\",\"requestMethod\":\"advance\",\"requestOrder\":45,\"pump\":{\"observed\":true,\"pumpCount\":46,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":7607.341,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.009,\"atMs\":7611.349,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":46}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"347d2fcdb8d947ac\",\"bytes\":169,\"requestId\":\"46\",\"requestMethod\":\"ready\",\"requestOrder\":46,\"pump\":{\"observed\":true,\"pumpCount\":47,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":7695.968,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.564,\"atMs\":7697.533,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":47}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"c458ea380df770a3\",\"bytes\":72,\"requestId\":\"47\",\"requestMethod\":\"advance\",\"requestOrder\":47,\"pump\":{\"observed\":true,\"pumpCount\":48,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":7793.298,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":3.162,\"atMs\":7796.460,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":48}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"3e262161873c549e\",\"bytes\":169,\"requestId\":\"48\",\"requestMethod\":\"ready\",\"requestOrder\":48,\"pump\":{\"observed\":true,\"pumpCount\":49,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":7881.718,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.601,\"atMs\":7883.319,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":49}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"fdb55682e517f658\",\"bytes\":72,\"requestId\":\"49\",\"requestMethod\":\"advance\",\"requestOrder\":49,\"pump\":{\"observed\":true,\"pumpCount\":50,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":7968.599,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.097,\"atMs\":7972.695,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":50}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"1d9b12d3ab425c3b\",\"bytes\":169,\"requestId\":\"50\",\"requestMethod\":\"ready\",\"requestOrder\":50,\"pump\":{\"observed\":true,\"pumpCount\":51,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":8058.345,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.634,\"atMs\":8059.980,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":51}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"7782b2b6cb71c8dc\",\"bytes\":72,\"requestId\":\"51\",\"requestMethod\":\"advance\",\"requestOrder\":51,\"pump\":{\"observed\":true,\"pumpCount\":52,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":8150.988,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.655,\"atMs\":8155.643,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":52}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"b6a103b889890fa9\",\"bytes\":169,\"requestId\":\"52\",\"requestMethod\":\"ready\",\"requestOrder\":52,\"pump\":{\"observed\":true,\"pumpCount\":53,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":8247.187,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":2.347,\"atMs\":8249.533,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":53}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"9d71a81816380853\",\"bytes\":72,\"requestId\":\"53\",\"requestMethod\":\"advance\",\"requestOrder\":53,\"pump\":{\"observed\":true,\"pumpCount\":54,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":8342.500,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.664,\"atMs\":8347.164,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":54}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"6f98cda10b366c4f\",\"bytes\":169,\"requestId\":\"54\",\"requestMethod\":\"ready\",\"requestOrder\":54,\"pump\":{\"observed\":true,\"pumpCount\":55,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":8438.992,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.736,\"atMs\":8440.728,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":55}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"337ebeaa86ffbade\",\"bytes\":72,\"requestId\":\"55\",\"requestMethod\":\"advance\",\"requestOrder\":55,\"pump\":{\"observed\":true,\"pumpCount\":56,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":8526.854,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":3.155,\"atMs\":8530.009,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":56}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"16cf2f9ca6fd47dd\",\"bytes\":169,\"requestId\":\"56\",\"requestMethod\":\"ready\",\"requestOrder\":56,\"pump\":{\"observed\":true,\"pumpCount\":57,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":8613.976,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.586,\"atMs\":8615.562,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":57}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"da4470c0057f3901\",\"bytes\":72,\"requestId\":\"57\",\"requestMethod\":\"advance\",\"requestOrder\":57,\"pump\":{\"observed\":true,\"pumpCount\":58,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":8699.938,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.049,\"atMs\":8703.987,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":58}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"271183c751d68ce3\",\"bytes\":169,\"requestId\":\"58\",\"requestMethod\":\"ready\",\"requestOrder\":58,\"pump\":{\"observed\":true,\"pumpCount\":59,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":8788.066,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.644,\"atMs\":8789.710,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":59}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"de33de02ae45f990\",\"bytes\":72,\"requestId\":\"59\",\"requestMethod\":\"advance\",\"requestOrder\":59,\"pump\":{\"observed\":true,\"pumpCount\":60,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":8874.535,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.077,\"atMs\":8878.612,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":60}", + "type": "log" + }, + { + "text": "TN_PRESENTS_TICK:{\"frames\":60,\"presents\":60,\"textureMB\":29,\"textures\":6,\"bufferMB\":0,\"capHz\":60}", + "type": "log" + }, + { + "text": "TN_GPU_TEXTURES:{\"totalMB\":29,\"count\":6,\"buckets\":[{\"k\":\"960x540 rgba16float msaa4\",\"n\":1,\"mb\":15},{\"k\":\"960x540 depth24plus msaa4\",\"n\":1,\"mb\":7},{\"k\":\"960x540 rgba16float\",\"n\":1,\"mb\":3},{\"k\":\"960x540 depth24plus\",\"n\":1,\"mb\":1},{\"k\":\"64x64 rgba8unorm\",\"n\":1,\"mb\":0},{\"k\":\"16x16 rg16float\",\"n\":1,\"mb\":0}],\"bucketsTotal\":6}", + "type": "log" + }, + { + "text": "TN_GPU_BUFFERS:{\"totalMB\":0,\"count\":1296,\"buckets\":[{\"k\":\"vertex|copysrc|copydst\",\"n\":517,\"mb\":0},{\"k\":\"uniform|copydst\",\"n\":519,\"mb\":0},{\"k\":\"index|copysrc|copydst\",\"n\":258,\"mb\":0},{\"k\":\"copydst|mapread\",\"n\":1,\"mb\":0},{\"k\":\"copysrc\",\"n\":1,\"mb\":0}]}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"18b2ef1e76e5bce8\",\"bytes\":169,\"requestId\":\"60\",\"requestMethod\":\"ready\",\"requestOrder\":60,\"pump\":{\"observed\":true,\"pumpCount\":61,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":8965.798,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.626,\"atMs\":8967.424,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":61}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"09d5f00b59a5a2e0\",\"bytes\":72,\"requestId\":\"61\",\"requestMethod\":\"advance\",\"requestOrder\":61,\"pump\":{\"observed\":true,\"pumpCount\":62,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":9052.530,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.171,\"atMs\":9056.701,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":62}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"c2911562e037c03e\",\"bytes\":169,\"requestId\":\"62\",\"requestMethod\":\"ready\",\"requestOrder\":62,\"pump\":{\"observed\":true,\"pumpCount\":63,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":9143.789,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.601,\"atMs\":9145.390,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":63}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"0e85742f46a37379\",\"bytes\":73,\"requestId\":\"63\",\"requestMethod\":\"advance\",\"requestOrder\":63,\"pump\":{\"observed\":true,\"pumpCount\":64,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":9233.151,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":3.170,\"atMs\":9236.321,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":64}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"9baadaa99db7dd94\",\"bytes\":169,\"requestId\":\"64\",\"requestMethod\":\"ready\",\"requestOrder\":64,\"pump\":{\"observed\":true,\"pumpCount\":65,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":9322.870,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.543,\"atMs\":9324.414,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":65}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"9966ef2d4e6708b2\",\"bytes\":73,\"requestId\":\"65\",\"requestMethod\":\"advance\",\"requestOrder\":65,\"pump\":{\"observed\":true,\"pumpCount\":66,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":9409.915,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.106,\"atMs\":9414.020,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":66}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"69d6de25bf7a182a\",\"bytes\":169,\"requestId\":\"66\",\"requestMethod\":\"ready\",\"requestOrder\":66,\"pump\":{\"observed\":true,\"pumpCount\":67,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":9500.117,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.627,\"atMs\":9501.744,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":67}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"f6598f5c20cca33b\",\"bytes\":73,\"requestId\":\"67\",\"requestMethod\":\"advance\",\"requestOrder\":67,\"pump\":{\"observed\":true,\"pumpCount\":68,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":9590.236,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":2.862,\"atMs\":9593.097,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":68}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"bf9d77b1364a0e80\",\"bytes\":169,\"requestId\":\"68\",\"requestMethod\":\"ready\",\"requestOrder\":68,\"pump\":{\"observed\":true,\"pumpCount\":69,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":9679.332,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.679,\"atMs\":9681.011,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":69}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"af051c6ddb763330\",\"bytes\":73,\"requestId\":\"69\",\"requestMethod\":\"advance\",\"requestOrder\":69,\"pump\":{\"observed\":true,\"pumpCount\":70,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":9765.724,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.146,\"atMs\":9769.869,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":70}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"3cdb38250d95b1e5\",\"bytes\":169,\"requestId\":\"70\",\"requestMethod\":\"ready\",\"requestOrder\":70,\"pump\":{\"observed\":true,\"pumpCount\":71,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":9854.890,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.591,\"atMs\":9856.481,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":71}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"c7c209b160e92ec6\",\"bytes\":73,\"requestId\":\"71\",\"requestMethod\":\"advance\",\"requestOrder\":71,\"pump\":{\"observed\":true,\"pumpCount\":72,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":9941.189,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":3.153,\"atMs\":9944.342,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":72}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"520e454b3c904f77\",\"bytes\":169,\"requestId\":\"72\",\"requestMethod\":\"ready\",\"requestOrder\":72,\"pump\":{\"observed\":true,\"pumpCount\":73,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":10028.046,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.620,\"atMs\":10029.666,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":73}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"52b64084ebb918ed\",\"bytes\":73,\"requestId\":\"73\",\"requestMethod\":\"advance\",\"requestOrder\":73,\"pump\":{\"observed\":true,\"pumpCount\":74,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":10114.864,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":3.924,\"atMs\":10118.787,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":74}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"6380c227074b8691\",\"bytes\":169,\"requestId\":\"74\",\"requestMethod\":\"ready\",\"requestOrder\":74,\"pump\":{\"observed\":true,\"pumpCount\":75,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":10201.862,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.617,\"atMs\":10203.479,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":75}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"be3dc3260f36fe3c\",\"bytes\":73,\"requestId\":\"75\",\"requestMethod\":\"advance\",\"requestOrder\":75,\"pump\":{\"observed\":true,\"pumpCount\":76,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":10288.000,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.102,\"atMs\":10292.102,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":76}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"1f677084102d9b83\",\"bytes\":169,\"requestId\":\"76\",\"requestMethod\":\"ready\",\"requestOrder\":76,\"pump\":{\"observed\":true,\"pumpCount\":77,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":10378.999,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.538,\"atMs\":10380.537,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":77}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"c4d75c1cfeb82787\",\"bytes\":73,\"requestId\":\"77\",\"requestMethod\":\"advance\",\"requestOrder\":77,\"pump\":{\"observed\":true,\"pumpCount\":78,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":10464.486,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":3.966,\"atMs\":10468.452,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":78}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"94350233bec333fd\",\"bytes\":169,\"requestId\":\"78\",\"requestMethod\":\"ready\",\"requestOrder\":78,\"pump\":{\"observed\":true,\"pumpCount\":79,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":10551.853,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.557,\"atMs\":10553.410,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":79}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"d07ed5024424a452\",\"bytes\":73,\"requestId\":\"79\",\"requestMethod\":\"advance\",\"requestOrder\":79,\"pump\":{\"observed\":true,\"pumpCount\":80,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":10636.856,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":3.068,\"atMs\":10639.924,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":80}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"89741c4f3d5d2262\",\"bytes\":169,\"requestId\":\"80\",\"requestMethod\":\"ready\",\"requestOrder\":80,\"pump\":{\"observed\":true,\"pumpCount\":81,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":10724.017,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.691,\"atMs\":10725.708,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":81}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"301571c6a38e0434\",\"bytes\":73,\"requestId\":\"81\",\"requestMethod\":\"advance\",\"requestOrder\":81,\"pump\":{\"observed\":true,\"pumpCount\":82,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":10809.833,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.308,\"atMs\":10814.141,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":82}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"90fedf35eedd2fcc\",\"bytes\":169,\"requestId\":\"82\",\"requestMethod\":\"ready\",\"requestOrder\":82,\"pump\":{\"observed\":true,\"pumpCount\":83,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":10906.953,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.531,\"atMs\":10908.483,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":83}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"036f4c51138a9c58\",\"bytes\":73,\"requestId\":\"83\",\"requestMethod\":\"advance\",\"requestOrder\":83,\"pump\":{\"observed\":true,\"pumpCount\":84,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":10991.263,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":3.801,\"atMs\":10995.064,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":84}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"14a16ced8cf51356\",\"bytes\":169,\"requestId\":\"84\",\"requestMethod\":\"ready\",\"requestOrder\":84,\"pump\":{\"observed\":true,\"pumpCount\":85,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":11077.669,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.637,\"atMs\":11079.306,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":85}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"a3ee970b2a64450f\",\"bytes\":73,\"requestId\":\"85\",\"requestMethod\":\"advance\",\"requestOrder\":85,\"pump\":{\"observed\":true,\"pumpCount\":86,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":11162.818,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":4.012,\"atMs\":11166.830,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":86}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"64b8e924b0b5ab80\",\"bytes\":169,\"requestId\":\"86\",\"requestMethod\":\"ready\",\"requestOrder\":86,\"pump\":{\"observed\":true,\"pumpCount\":87,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":11253.412,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.620,\"atMs\":11255.032,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":87}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"a323689e1d42692a\",\"bytes\":73,\"requestId\":\"87\",\"requestMethod\":\"advance\",\"requestOrder\":87,\"pump\":{\"observed\":true,\"pumpCount\":88,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":11352.263,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":3.304,\"atMs\":11355.567,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":88}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"795a5d9268e0ad67\",\"bytes\":249,\"requestId\":\"88\",\"requestMethod\":\"ready\",\"requestOrder\":88,\"pump\":{\"observed\":true,\"pumpCount\":89,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":11439.123,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":1.671,\"atMs\":11440.794,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":89}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"9224ca862071dfa0\",\"bytes\":6165,\"requestId\":\"89\",\"requestMethod\":\"sample\",\"requestOrder\":89,\"pump\":{\"observed\":true,\"pumpCount\":90,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":11525.048,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":23.764,\"atMs\":11548.812,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":90}", + "type": "log" + }, + { + "text": "TN_COLD_START:{\"segment\":\"first_playable\",\"atMs\":11633.833}", + "type": "log" + }, + { + "text": "[log] TN_DECAL_FIXTURE:{\"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}", + "type": "log" + }, + { + "text": "[log] TN_DECAL_FIXTURE:{\"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}", + "type": "log" + }, + { + "text": "[log] TN_DECAL_FIXTURE:{\"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}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"6878ee373e5f987e\",\"bytes\":74,\"requestId\":\"90\",\"requestMethod\":\"advance\",\"requestOrder\":90,\"pump\":{\"observed\":true,\"pumpCount\":91,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":11633.919,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":132.181,\"atMs\":11766.100,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":91}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"8bd550e42e078f4d\",\"bytes\":6165,\"requestId\":\"91\",\"requestMethod\":\"sample\",\"requestOrder\":91,\"pump\":{\"observed\":true,\"pumpCount\":92,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":11856.224,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":14.205,\"atMs\":11870.429,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":92}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"8e69a825d07469a4\",\"bytes\":6165,\"requestId\":\"92\",\"requestMethod\":\"sample\",\"requestOrder\":92,\"pump\":{\"observed\":true,\"pumpCount\":93,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":11961.372,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":14.562,\"atMs\":11975.935,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":93}", + "type": "log" + }, + { + "text": "TN_PUMP_ENDPOINT:{\"path\":\"/tmp/threenative-playtest-desktop-b6Dgxs/tn-playtest-response.json\",\"stored\":true,\"payloadHash\":\"9534902c1c41adeb\",\"bytes\":6165,\"requestId\":\"93\",\"requestMethod\":\"sample\",\"requestOrder\":93,\"pump\":{\"observed\":true,\"pumpCount\":94,\"firstPumpAtMs\":853.834,\"lastPumpAtMs\":12065.898,\"maxGapMs\":2343.740,\"maxGapAtMs\":1411.531,\"trailingGapMs\":14.339,\"atMs\":12080.237,\"longGaps\":[557.697,2343.740],\"droppedLongGaps\":0}}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":94}", + "type": "log" + }, + { + "text": "TN_SURFACE_FRAME:{\"view\":true,\"present\":95}", + "type": "log" + }, + { + "text": "[Screenshot] First 16 bytes (BGRA raw): 47 35 23 255 47 35 23 255 47 35 23 255 47 35 23 255 ", + "type": "log" + }, + { + "text": "[Screenshot] Middle bytes (BGRA raw): 47 35 23 255 47 35 23 255 47 35 23 255 47 35 23 255 ", + "type": "log" + }, + { + "text": "[Screenshot] Saved: /home/runner/work/threenative/threenative/artifacts/vq11-decals-native/static/after.png (960x540)", + "type": "log" + } +] diff --git a/docs/verification/vq11-decals-native-37010879478/static.png b/docs/verification/vq11-decals-native-37010879478/static.png new file mode 100644 index 0000000000..da3ebd877a Binary files /dev/null and b/docs/verification/vq11-decals-native-37010879478/static.png differ 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 0000000000..1823155528 Binary files /dev/null and b/docs/verification/vq11-decals-native-37010879478/teardown.png differ diff --git a/docs/verification/vq11-decals-native-37015640669/atlas.png b/docs/verification/vq11-decals-native-37015640669/atlas.png new file mode 100644 index 0000000000..82992fcef5 Binary files /dev/null and b/docs/verification/vq11-decals-native-37015640669/atlas.png differ diff --git a/docs/verification/vq11-decals-native-37015640669/expired.png b/docs/verification/vq11-decals-native-37015640669/expired.png new file mode 100644 index 0000000000..5351a90ffb Binary files /dev/null and b/docs/verification/vq11-decals-native-37015640669/expired.png differ diff --git a/docs/verification/vq11-decals-native-37015640669/fading.png b/docs/verification/vq11-decals-native-37015640669/fading.png new file mode 100644 index 0000000000..a4d602f580 Binary files /dev/null and b/docs/verification/vq11-decals-native-37015640669/fading.png differ diff --git a/docs/verification/vq11-decals-native-37015640669/provenance.json b/docs/verification/vq11-decals-native-37015640669/provenance.json new file mode 100644 index 0000000000..795d8cb902 --- /dev/null +++ b/docs/verification/vq11-decals-native-37015640669/provenance.json @@ -0,0 +1,245 @@ +{ + "sourceSha": "8f85fff19913c955a44b7b85d2cc117b972bd19c", + "runUrl": "https://github.com/ThreeNativeHQ/threenative/actions/runs/37015640669", + "artifactId": 11231490134, + "artifactSha256": "07925a63d4091f2a53d06ef7b178619d775ef9185890949afb4645c83e9d519f", + "qualification": "Packaged Linux ARM QuickJS software-adapter correctness. No mobile or hardware-performance claim.", + "pass": true, + "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 + } + }, + "runtimeSha256": "da16138cc37938a14ce6831476aa58972a0d469ba2172b6a06faf6d1d56c6042", + "runtimeVersion": "Mystral Native Runtime v0.3.4; wgpu-native + QuickJS", + "assetHashes": { + "receiver.glb": "7ed6179e17f75f3f60576f72968140773c7f65cb1b8d760240576ac4092c05c3", + "assets.manifest.json": "3b833a9fc96c4f3ef939840f2154f3e40aa95d4beb6cda965833b54f43cde1dc" + }, + "builds": { + "native.ts": { + "executableSha256": "6d53ae54426ee3226a06373978fa1ea081c02d1667ceafa191bf70a1657a7fa7", + "bundleSha256": "391f98f6391b915798aa10053b71a03a652d1b07a72062d29448309435ac051a" + }, + "native-atlas.ts": { + "executableSha256": "a8bda56b2a103f0ae6f1330c5f35f56dfb2c7dc0d4b2ce672b2424f5eff05b26", + "bundleSha256": "683bc8d337886e7f9ca803c09d0ea89393e5914d17c7c58cfe2536fbe2db49d5" + }, + "native-hidden.ts": { + "executableSha256": "a404951c4b9f9e73d44589b307253383590b185dcc33aae1cad4bdc83fcfc4d5", + "bundleSha256": "54e4a82de7e6920b74d87beb66c995dd40bbf3012efcaf06989ec14a04e26abe" + }, + "native-hidden-fading.ts": { + "executableSha256": "511bfb911a6909a09043151fdfc65ea124b1ced21559b80c27c93144c8d6c7cd", + "bundleSha256": "5f538d5bdde31205935bf18c22214b641748e98a8636a7ba73e37ea06ee2d329" + } + }, + "results": [ + { + "name": "static", + "pass": true, + "negativeControl": false, + "png": "../vq11-decals-native-37010879478/static.png", + "screenshotSha256": "ffb1bc6c5abaf5e4c55c258740c549731e81a8dc9c48c9b449ec7ddd27b95fa8", + "active": 256, + "created": 321, + "released": 65, + "geometryBytes": 275076, + "maxDrawCalls": 516, + "averageOpacity": 1, + "failedPixelAssertions": [] + }, + { + "name": "motion", + "pass": true, + "negativeControl": false, + "png": "../vq11-decals-native-37010879478/motion.png", + "screenshotSha256": "a80813b0c2ebdff2c817664a8780b59396bbec4844bacfba5ac25998fe001c99", + "active": 256, + "created": 321, + "released": 65, + "geometryBytes": 275076, + "maxDrawCalls": 516, + "averageOpacity": 1, + "failedPixelAssertions": [] + }, + { + "name": "teardown", + "pass": true, + "negativeControl": false, + "png": "../vq11-decals-native-37010879478/teardown.png", + "screenshotSha256": "2a85f1cb78de0bd06f79534704b44170397c6a299d9b5709dd76c560c08f6880", + "active": 128, + "created": 321, + "released": 193, + "geometryBytes": 136188, + "maxDrawCalls": 516, + "averageOpacity": 1, + "failedPixelAssertions": [] + }, + { + "name": "restart", + "pass": true, + "negativeControl": false, + "png": "../vq11-decals-native-37010879478/static.png", + "screenshotSha256": "ffb1bc6c5abaf5e4c55c258740c549731e81a8dc9c48c9b449ec7ddd27b95fa8", + "active": 256, + "created": 321, + "released": 65, + "geometryBytes": 275076, + "maxDrawCalls": 516, + "averageOpacity": 1, + "failedPixelAssertions": [] + }, + { + "name": "lifecycle", + "pass": true, + "negativeControl": false, + "png": "../vq11-decals-native-37010879478/static.png", + "screenshotSha256": "ffb1bc6c5abaf5e4c55c258740c549731e81a8dc9c48c9b449ec7ddd27b95fa8", + "active": 256, + "created": 321, + "released": 65, + "geometryBytes": 275076, + "maxDrawCalls": 516, + "averageOpacity": 1, + "failedPixelAssertions": [], + "residency": [ + { + "generation": 1, + "renderCalls": 18, + "stableFrames": 3, + "geometries": 259, + "textures": 4, + "bytes": 6522894 + }, + { + "generation": 2, + "renderCalls": 230, + "stableFrames": 3, + "geometries": 259, + "textures": 4, + "bytes": 6522894 + }, + { + "generation": 3, + "renderCalls": 258, + "stableFrames": 3, + "geometries": 259, + "textures": 4, + "bytes": 6522894 + }, + { + "generation": 4, + "renderCalls": 286, + "stableFrames": 3, + "geometries": 259, + "textures": 4, + "bytes": 6522894 + }, + { + "generation": 5, + "renderCalls": 314, + "stableFrames": 3, + "geometries": 259, + "textures": 4, + "bytes": 6522894 + }, + { + "generation": 6, + "renderCalls": 342, + "stableFrames": 3, + "geometries": 259, + "textures": 4, + "bytes": 6522894 + } + ], + "growth": { + "geometryGrowth": 0, + "textureGrowth": 0, + "memoryGrowth": 0 + } + }, + { + "name": "atlas", + "pass": true, + "negativeControl": false, + "png": "atlas.png", + "screenshotSha256": "4ecb9a664104a750608b1326bf18bbed367f03d0ed88c021b19a94f9d91fcd0f", + "active": 256, + "created": 321, + "released": 65, + "geometryBytes": 275076, + "maxDrawCalls": 516, + "averageOpacity": 1, + "failedPixelAssertions": [] + }, + { + "name": "fading", + "pass": true, + "negativeControl": false, + "png": "fading.png", + "screenshotSha256": "4740d99ae7b3ba976a3ea9a49c218021443a7e870c9dbc4da72bc5479f55a554", + "active": 256, + "created": 321, + "released": 65, + "geometryBytes": 275076, + "maxDrawCalls": 516, + "averageOpacity": 0.4749999999999973, + "failedPixelAssertions": [] + }, + { + "name": "expired", + "pass": true, + "negativeControl": false, + "png": "expired.png", + "screenshotSha256": "12304589dcd60b2f774176717c8910ddddc7cdef39d5a75fe6e526ec5caef56e", + "active": 0, + "created": 321, + "released": 321, + "geometryBytes": 0, + "maxDrawCalls": 516, + "averageOpacity": 0, + "failedPixelAssertions": [] + }, + { + "name": "hidden-decals", + "pass": true, + "negativeControl": true, + "png": "expired.png", + "screenshotSha256": "12304589dcd60b2f774176717c8910ddddc7cdef39d5a75fe6e526ec5caef56e", + "active": 256, + "created": 321, + "released": 65, + "geometryBytes": 259794, + "maxDrawCalls": 4, + "averageOpacity": 1, + "failedPixelAssertions": ["visual.1.region.darkPixels", "visual.2.region.darkPixels"] + }, + { + "name": "hidden-fading", + "pass": true, + "negativeControl": true, + "png": "expired.png", + "screenshotSha256": "12304589dcd60b2f774176717c8910ddddc7cdef39d5a75fe6e526ec5caef56e", + "active": 256, + "created": 321, + "released": 65, + "geometryBytes": 275076, + "maxDrawCalls": 516, + "averageOpacity": 0.4749999999999973, + "failedPixelAssertions": ["visual.3.region.darkPixels", "visual.4.region.darkPixels"] + } + ] +} diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/atlas.playtest.json b/packages/create-threenative/__tests__/fixtures/bounded-decals/atlas.playtest.json new file mode 100644 index 0000000000..c48f47c7ea --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/atlas.playtest.json @@ -0,0 +1,107 @@ +{ + "name": "vq11-atlas", + "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." + }, + { + "entity": "decals", + "component": "atlasFade", + "equals": true, + "allowTrivial": "The same atlas is sampled through two distinct authored UV rectangles." + }, + { + "entity": "decals", + "component": "averageOpacity", + "equals": 1, + "allowTrivial": "Observe actual cloned material opacity and exact expiry, not a timer counter." + } + ], + "visual": [ + { + "region": { + "x": 0, + "y": 0, + "width": 960, + "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.1 + } + }, + { + "region": { + "x": 550, + "y": 190, + "width": 95, + "height": 90, + "minNonblankPixelRatio": 0.9, + "maxLuminance": 0.35, + "minDarkPixelRatio": 0.12, + "maxDarkPixelRatio": 0.65 + } + } + ] + } +} diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/expired.playtest.json b/packages/create-threenative/__tests__/fixtures/bounded-decals/expired.playtest.json new file mode 100644 index 0000000000..2d4ac86c00 --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/expired.playtest.json @@ -0,0 +1,123 @@ +{ + "name": "vq11-expired", + "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 + }, + { + "kind": "input", + "label": "start-fade", + "press": "KeyF", + "holdTicks": 2, + "release": true + }, + { + "kind": "wait", + "label": "expired", + "waitFrames": 180 + } + ], + "assert": { + "diagnostics": { + "runtimeReady": true, + "noConsoleErrors": true, + "noNetworkErrors": true + }, + "components": [ + { + "entity": "decals", + "component": "active", + "equals": 0, + "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.", + "equals": 0 + }, + { + "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." + }, + { + "entity": "decals", + "component": "atlasFade", + "equals": true, + "allowTrivial": "The same atlas is sampled through two distinct authored UV rectangles." + }, + { + "entity": "decals", + "component": "averageOpacity", + "equals": 0, + "allowTrivial": "Observe actual cloned material opacity and exact expiry, not a timer counter." + }, + { + "entity": "decals", + "component": "released", + "equals": 321, + "allowTrivial": "Every projected buffer is released by eviction or authored expiry." + } + ], + "visual": [ + { + "region": { + "x": 0, + "y": 0, + "width": 960, + "height": 540, + "minNonblankPixelRatio": 0.15 + } + }, + { + "region": { + "x": 270, + "y": 185, + "width": 170, + "height": 125, + "minNonblankPixelRatio": 0.9, + "maxLuminance": 0.35, + "maxDarkPixelRatio": 0.001 + } + }, + { + "region": { + "x": 550, + "y": 190, + "width": 95, + "height": 90, + "minNonblankPixelRatio": 0.9, + "maxLuminance": 0.35, + "maxDarkPixelRatio": 0.001 + } + } + ] + } +} diff --git a/packages/create-threenative/__tests__/fixtures/bounded-decals/fading.playtest.json b/packages/create-threenative/__tests__/fixtures/bounded-decals/fading.playtest.json new file mode 100644 index 0000000000..b1c68a617a --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/fading.playtest.json @@ -0,0 +1,142 @@ +{ + "name": "vq11-fading", + "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 + }, + { + "kind": "input", + "label": "start-fade", + "press": "KeyF", + "holdTicks": 2, + "release": true + }, + { + "kind": "wait", + "label": "fading", + "waitFrames": 60 + } + ], + "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." + }, + { + "entity": "decals", + "component": "atlasFade", + "equals": true, + "allowTrivial": "The same atlas is sampled through two distinct authored UV rectangles." + }, + { + "entity": "decals", + "component": "averageOpacity", + "gte": 0.4, + "lte": 0.6, + "allowTrivial": "Observe actual cloned material opacity and exact expiry, not a timer counter." + } + ], + "visual": [ + { + "region": { + "x": 0, + "y": 0, + "width": 960, + "height": 540, + "minNonblankPixelRatio": 0.15 + } + }, + { + "region": { + "x": 270, + "y": 185, + "width": 170, + "height": 125, + "minNonblankPixelRatio": 0.9, + "maxLuminance": 0.35, + "maxDarkPixelRatio": 0.001 + } + }, + { + "region": { + "x": 550, + "y": 190, + "width": 95, + "height": 90, + "minNonblankPixelRatio": 0.9, + "maxLuminance": 0.35, + "maxDarkPixelRatio": 0.001 + } + }, + { + "region": { + "x": 280, + "y": 195, + "width": 150, + "height": 100, + "minNonblankPixelRatio": 0.9, + "maxLuminance": 0.65, + "minDarkPixelRatio": 0.01, + "maxDarkPixelRatio": 0.3 + } + }, + { + "region": { + "x": 555, + "y": 195, + "width": 85, + "height": 80, + "minNonblankPixelRatio": 0.9, + "maxLuminance": 0.6, + "minDarkPixelRatio": 0.1, + "maxDarkPixelRatio": 0.65 + } + } + ] + } +} 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..a93d66e366 --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/game.ts @@ -0,0 +1,344 @@ +import { + AmbientLight, + BoxGeometry, + type BufferGeometry, + Color, + DataTexture, + DirectionalLight, + Mesh, + MeshStandardMaterial, + type Object3D, + type PerspectiveCamera, + RGBAFormat, + UnsignedByteType, + 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"; + +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: (ResidencySample & { generation: number; stableFrames: number })[] = []; + 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"); + this.#source = model.scene; + } + + 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; + 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)); + let map = bulletHoleTexture(64) as DataTexture; + if (atlasFade) { + // A two-cell authored atlas: impact on the left, a solid environment mark on the right. + // Their different pixel coverage makes selecting the wrong atlas region observable. + const data = new Uint8Array(128 * 64 * 4); + const impact = map.image.data; + if (impact === null) throw new Error("Decal fixture impact pixels are unavailable."); + for (let y = 0; y < 64; y += 1) { + for (let x = 0; x < 64; x += 1) { + const target = (y * 128 + x) * 4; + data.set(impact.subarray((y * 64 + x) * 4, (y * 64 + x + 1) * 4), target); + data.set([20, 20, 20, x >= 3 && x < 61 && y >= 3 && y < 61 ? 255 : 0], target + 256); + } + } + map.dispose(); + map = new DataTexture(data, 128, 64, RGBAFormat, UnsignedByteType); + map.needsUpdate = true; + } + 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, + ...(atlasFade + ? { + uvRect: receiver === left ? ([0, 0, 0.5, 1] as const) : ([0.5, 0, 1, 1] as const), + fade: { duration: 2, opacity: (progress: number) => 1 - progress }, + } + : {}), + }) + ) { + 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."); + // 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; + }); + }; + 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 fading = false; + let elapsed = 0; + let frames = 0; + 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, + }; + 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."); + const observation = () => { + const baseline = residency[1]; + let geometryBytes = 0; + let active = 0; + let opacitySum = 0; + for (const receiver of [left, right]) { + for (const child of receiver.children) { + if (!(child instanceof Mesh)) continue; + active += 1; + opacitySum += child.material.opacity; + 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, + atlasFade, + 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 + ? {} + : { + geometryGrowth: Math.max( + 0, + ...residency.slice(1).map((sample) => sample.geometries - baseline.geometries), + ), + textureGrowth: Math.max( + 0, + ...residency.slice(1).map((sample) => sample.textures - baseline.textures), + ), + memoryGrowth: Math.max( + 0, + ...residency.slice(1).map((sample) => sample.bytes - baseline.bytes), + ), + }), + }; + }; + 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) => { + // 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; + // Suppress rendering only after the control has shown its real marks. The opacity + // curve and owned geometry continue normally, so only positive fade pixels can reject it. + if (hideDuringFade) + for (const receiver of [left, right]) { + for (const child of receiver.children) + if (child instanceof Mesh) child.material.visible = false; + } + } + 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(fading ? dt : 0); + 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 (!residencySampled && residencyObservation.stableFrames >= 3) { + residency.push({ + generation, + ...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())}`); + } + // 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())}`); + }; + } + + override render(): void { + this.#observeRenderedFrame(); + } + + override exit(): void { + this.#observeRenderedFrame = () => {}; + this.#dispose(); + this.#dispose = () => {}; + } + } + return defineGame({ + camera: { far: 100, fov: 48, near: 0.1, projection: "perspective" }, + display: { maxFps: 60 }, + initialState: { residencyGeneration: 0 }, + input: { + motion: { keys: ["KeyM"] }, + remove: { keys: ["KeyX"] }, + reset: { keys: ["KeyR"] }, + fade: { keys: ["KeyF"] }, + }, + 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/lifecycle.playtest.json b/packages/create-threenative/__tests__/fixtures/bounded-decals/lifecycle.playtest.json new file mode 100644 index 0000000000..e2c75327e8 --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/lifecycle.playtest.json @@ -0,0 +1,215 @@ +{ + "name": "vq11-lifecycle", + "schemaVersion": 1, + "target": "web", + "viewport": { + "width": 960, + "height": 540 + }, + "warmupFrames": 10, + "artifacts": { + "screenshots": true, + "console": true + }, + "steps": [ + { + "label": "rendered-residency-1", + "waitForResource": { + "id": "state", + "path": "residencyGeneration", + "gte": 1 + }, + "timeoutMs": 30000, + "release": true + }, + { + "kind": "input", + "label": "reset-1", + "press": "KeyR", + "holdTicks": 2, + "release": true + }, + { + "label": "rendered-residency-2", + "waitForResource": { + "id": "state", + "path": "residencyGeneration", + "gte": 2 + }, + "timeoutMs": 30000, + "release": true + }, + { + "kind": "input", + "label": "reset-2", + "press": "KeyR", + "holdTicks": 2, + "release": true + }, + { + "label": "rendered-residency-3", + "waitForResource": { + "id": "state", + "path": "residencyGeneration", + "gte": 3 + }, + "timeoutMs": 30000, + "release": true + }, + { + "kind": "input", + "label": "reset-3", + "press": "KeyR", + "holdTicks": 2, + "release": true + }, + { + "label": "rendered-residency-4", + "waitForResource": { + "id": "state", + "path": "residencyGeneration", + "gte": 4 + }, + "timeoutMs": 30000, + "release": true + }, + { + "kind": "input", + "label": "reset-4", + "press": "KeyR", + "holdTicks": 2, + "release": true + }, + { + "label": "rendered-residency-5", + "waitForResource": { + "id": "state", + "path": "residencyGeneration", + "gte": 5 + }, + "timeoutMs": 30000, + "release": true + }, + { + "kind": "input", + "label": "reset-5", + "press": "KeyR", + "holdTicks": 2, + "release": true + }, + { + "label": "rendered-residency-6", + "waitForResource": { + "id": "state", + "path": "residencyGeneration", + "gte": 6 + }, + "timeoutMs": 30000, + "release": true + } + ], + "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": 6, + "allowTrivial": "The restart input must construct a new scene; unchanged initial state is not evidence." + }, + { + "entity": "decals", + "component": "residencySamples", + "equals": 6, + "allowTrivial": "One memory sample per scene after three stable completed world-render intervals and real LOD/cap readiness." + }, + { + "entity": "decals", + "component": "geometryGrowth", + "lte": 0, + "allowTrivial": "GPU resource counts and bytes must not grow across the five warmed scene instances." + }, + { + "entity": "decals", + "component": "textureGrowth", + "lte": 0, + "allowTrivial": "GPU resource counts and bytes must not grow across the five warmed scene instances." + }, + { + "entity": "decals", + "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": [ + { + "region": { + "x": 0, + "y": 0, + "width": 960, + "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/main.ts b/packages/create-threenative/__tests__/fixtures/bounded-decals/main.ts new file mode 100644 index 0000000000..42e132d891 --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/main.ts @@ -0,0 +1,17 @@ +import { createDecalFixture } from "./game.js"; +const params = new URLSearchParams(location.search); +const game = createDecalFixture( + params.get("hideDecals") === "1", + params.get("atlasFade") === "1", + params.get("hideFading") === "1", +); +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..969878b789 --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/motion.playtest.json @@ -0,0 +1,119 @@ +{ + "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 + } + }, + { + "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/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/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/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/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 0000000000..7dcc89a384 Binary files /dev/null and b/packages/create-threenative/__tests__/fixtures/bounded-decals/public/receiver.glb differ 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..4b79543e1c --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/restart.playtest.json @@ -0,0 +1,107 @@ +{ + "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 + } + }, + { + "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 new file mode 100644 index 0000000000..b070b1904c --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/static.playtest.json @@ -0,0 +1,95 @@ +{ + "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 + } + }, + { + "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 new file mode 100644 index 0000000000..ba4e255b27 --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/teardown.playtest.json @@ -0,0 +1,101 @@ +{ + "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 + } + }, + { + "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-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__/fixtures/bounded-decals/verify.ts b/packages/create-threenative/__tests__/fixtures/bounded-decals/verify.ts new file mode 100644 index 0000000000..7b8d5d4140 --- /dev/null +++ b/packages/create-threenative/__tests__/fixtures/bounded-decals/verify.ts @@ -0,0 +1,145 @@ +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", + "atlas", + "fading", + "expired", + "lifecycle", + "hidden-decals", + "hidden-fading", + ]) { + const hiddenFading = name === "hidden-fading"; + const hidden = name === "hidden-decals" || hiddenFading; + const atlasFade = ["atlas", "fading", "expired", "hidden-fading"].includes(name); + const expired = name === "expired"; + const directory = join(artifacts, name); + const report = await runStandalonePlaytest({ + artifactDirectory: directory, + projectPath: fixture, + scenarioPath: join( + fixture, + `${hiddenFading ? "fading" : hidden ? "static" : name}.playtest.json`, + ), + url: `http://127.0.0.1:4173/${hiddenFading ? "?atlasFade=1&hideFading=1" : hidden ? "?hideDecals=1" : atlasFade ? "?atlasFade=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)}`, + 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 observed = + report.capture?.rendererKind === "webgpu" && + Object.values(report.capture.adapter).some((value) => value.length > 0) && + disallowedDiagnostics.length === 0 && + screenshotPresent && + measurement !== undefined && + measurement.active === (expired ? 0 : name === "teardown" ? 128 : 256) && + measurement.capacity === 256 && + measurement.created >= 321 && + measurement.lodHit === true && + measurement.renderTriangles === 2 && + Number.isFinite(measurement.geometryBytes) && + (expired ? measurement.geometryBytes === 0 : measurement.geometryBytes > 0) && + Number.isFinite(measurement.maxDrawCalls) && + 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( + hiddenFading + ? ["visual.3.region.darkPixels", "visual.4.region.darkPixels"] + : ["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: !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.name.startsWith("hidden-") ? !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, hiddenDecalsRejected: true, hiddenFadingRejected: true, correctnessOnly: true })}`, + ); +} diff --git a/packages/create-threenative/__tests__/scaffold.spec.ts b/packages/create-threenative/__tests__/scaffold.spec.ts index eb1a669c3e..e59c853653 100644 --- a/packages/create-threenative/__tests__/scaffold.spec.ts +++ b/packages/create-threenative/__tests__/scaffold.spec.ts @@ -193,7 +193,8 @@ const PRD_201_PARENT_SCAFFOLD_HASHES: Readonly> = { rts: "45a4cd67b5c4e6dbba60ea6813636effc6ecaa294acc9f5ae49262fc64141b1c", runner: "3348101ae79785e5e286df38628f13e747b91f1ea6527fa71acd9d62f767d3a3", sailing: "69b54c3c07cf4fe65f4cd06f1ce315750d198332129425b031853f1022d2e99b", - shooter: "386f6bca602b4939bf9d860d5cadba0ede803adef95222faedbdc8e33d205590", + // VQ11 after merge of develop 416ffd7: measured through createProject, only shooter changed. + shooter: "c035066c49f50a3c8ff894604a17ca6db3729179d6112e8470cb83f43d6eccc0", snow: "f6555492e02901960b8d733568f4f9c07ae1e11baa03e95b9b498d24c3eee1c6", starter: "064de9e02c8b09cd378fdf42186b499580e474d323717eecbe4edb59964b94f6", "tower-defense": "2967805234d96f52d79a398463ddb980658a53efafecf4b7a8ecf33ad1a5679f", 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..a607a9ec55 --- /dev/null +++ b/packages/create-threenative/__tests__/shooter-decals-fixture.spec.ts @@ -0,0 +1,264 @@ +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"; +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"; +import { createDecalFixture, observeResidency } from "./fixtures/bounded-decals/game.js"; + +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 }; + 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, + ); +}); + +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, + run = "36989297734", +) { + 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-${run}/${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 fully hidden decals as intermediate fade evidence despite nonblank receivers", async () => { + const assertions = await evaluateCapturedPixels("fading", "hidden-decals", "36991562324"); + expect(assertions.filter(({ pass }) => !pass).map(({ id }) => id)).toEqual([ + "visual.3.region.darkPixels", + "visual.4.region.darkPixels", + ]); +}); + +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), + ); + 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-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/__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(); +}); 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..5a33e4de08 --- /dev/null +++ b/packages/create-threenative/__tests__/shooter-decals-projection.spec.ts @@ -0,0 +1,496 @@ +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 { Registry } from "../../core/src/entities.js"; +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("releases fading projected buffers exactly once across 20 registry lifecycles", () => { + let created = 0; + let disposed = 0; + for (let cycle = 0; cycle < 20; cycle += 1) { + const { receiver, field, slots, map } = fixture(4); + const registry = new Registry(); + registry.add("decals", field); + registry.add("map", map); + const borrowedDisposed = vi.fn(); + receiver.geometry.addEventListener("dispose", borrowedDisposed); + for (let shot = 0; shot < 12; shot += 1) { + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { + depth: 0.2, + fade: { duration: 1, opacity: (p) => 1 - p }, + }); + created += 1; + slots[shot % 4]?.geometry.addEventListener("dispose", () => { + disposed += 1; + }); + field.update(0.1); + } + field.update(0.5); + receiver.removeFromParent(); + field.update(1); + registry.clear(); + registry.clear(); + expect(receiver.children).toHaveLength(0); + expect(borrowedDisposed).not.toHaveBeenCalled(); + expect(disposed).toBe(created); + } + expect(created).toBe(240); + }); + it("maps only owned projected UVs into an authored atlas rectangle", () => { + const a = fixture(1); + const b = fixture(1); + const borrowedUvs = Array.from(a.receiver.geometry.getAttribute("uv").array); + a.field.project(a.receiver, a.receiver.geometry, new Vector3(), normal, "stone", { + depth: 0.2, + uvRect: [0.5, 0.25, 1, 0.75], + }); + b.field.project(b.receiver, b.receiver.geometry, new Vector3(), normal, "stone", { + depth: 0.2, + }); + const atlas = a.slots[0]?.geometry.getAttribute("uv"); + const full = b.slots[0]?.geometry.getAttribute("uv"); + if (atlas === undefined || full === undefined) throw new Error("Missing atlas UVs"); + for (let i = 0; i < full.count; i += 1) { + expect(atlas.getX(i)).toBeCloseTo(0.5 + full.getX(i) * 0.5, 6); + expect(atlas.getY(i)).toBeCloseTo(0.25 + full.getY(i) * 0.5, 6); + } + expect(Array.from(a.receiver.geometry.getAttribute("uv").array)).toEqual(borrowedUvs); + expect(a.slots[0]?.material.map).toBe(a.map); + expect(a.map.offset.toArray()).toEqual([0, 0]); + expect(a.map.repeat.toArray()).toEqual([1, 1]); + a.field.dispose(); + b.field.dispose(); + }); + + it("uses the authored opacity curve and expires owned geometry once without retiring the slot", () => { + const { root, receiver, field, slots, map } = fixture(1); + const slot = slots[0]; + if (slot === undefined) throw new Error("Missing slot"); + const plane = slot.geometry; + const materialDisposed = vi.fn(); + const mapDisposed = vi.fn(); + slot.material.addEventListener("dispose", materialDisposed); + map.addEventListener("dispose", mapDisposed); + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { + depth: 0.2, + fade: { duration: 2, opacity: (progress: number) => (1 - progress) ** 2 }, + }); + const geometryDisposed = vi.fn(); + slot.geometry.addEventListener("dispose", geometryDisposed); + field.update(0.5); + expect(slot.material.opacity).toBe(0.5625); + field.update(0.5); + expect(slot.material.opacity).toBe(0.25); + field.update(1); + field.update(10); + expect(geometryDisposed).toHaveBeenCalledTimes(1); + expect(receiver.children).toHaveLength(0); + expect(slot.parent).toBe(root); + expect(slot.geometry).toBe(plane); + expect(slot.visible).toBe(false); + expect(slot.material.opacity).toBe(1); + expect(materialDisposed).not.toHaveBeenCalled(); + expect(mapDisposed).not.toHaveBeenCalled(); + field.dispose(); + field.dispose(); + expect(geometryDisposed).toHaveBeenCalledTimes(1); + expect(materialDisposed).toHaveBeenCalledTimes(1); + expect(mapDisposed).not.toHaveBeenCalled(); + }); + + it("resets fade on projected or legacy reuse and keeps unconfigured marks persistent", () => { + const { receiver, field, slots } = fixture(1); + const fading = { depth: 0.2, fade: { duration: 2, opacity: (p: number) => 1 - p } }; + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", fading); + field.update(1); + expect(slots[0]?.material.opacity).toBe(0.5); + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { depth: 0.2 }); + field.update(1000); + expect(receiver.children).toHaveLength(1); + expect(slots[0]?.material.opacity).toBe(1); + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", fading); + field.update(1); + field.place(new Vector3(), normal, "stone"); + field.update(1000); + expect(slots[0]?.material.opacity).toBe(1); + expect(slots[0]?.visible).toBe(true); + field.dispose(); + }); + + it.each([ + { uvRect: [-0.1, 0, 1, 1] }, + { uvRect: [0.5, 0, 0.5, 1] }, + { uvRect: [0, 0, 1.1, 1] }, + { uvRect: [0, 0, 1, Number.NaN] }, + { fade: { duration: 0, opacity: () => 1 } }, + { fade: { duration: Number.POSITIVE_INFINITY, opacity: () => 1 } }, + { fade: { duration: 1, opacity: () => Number.NaN } }, + { fade: { duration: 1, opacity: () => 1.1 } }, + ])("refuses malformed authored controls without evicting a live mark: %j", (controls) => { + const { receiver, field, slots } = fixture(1); + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { depth: 0.2 }); + const geometry = slots[0]?.geometry; + expect(() => + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { + depth: 0.2, + ...controls, + } as Parameters[5]), + ).toThrow(/decal/i); + expect(slots[0]?.geometry).toBe(geometry); + expect(field.placed).toBe(1); + field.dispose(); + }); + + it("rejects invalid time or curve values without advancing a live fade", () => { + const { receiver, field, slots } = fixture(1); + let invalid = false; + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { + depth: 0.2, + fade: { duration: 2, opacity: (p: number) => (invalid ? Number.NaN : 1 - p) }, + }); + for (const dt of [-1, Number.NaN, Number.POSITIVE_INFINITY]) + expect(() => field.update(dt)).toThrow(/decal/i); + field.update(0.5); + expect(slots[0]?.material.opacity).toBe(0.75); + invalid = true; + expect(() => field.update(0.5)).toThrow(/decal/i); + expect(slots[0]?.material.opacity).toBe(0.75); + invalid = false; + field.update(0.5); + expect(slots[0]?.material.opacity).toBe(0.5); + field.dispose(); + }); + + it("refuses projection bias beyond the projector half-depth before recycling", () => { + const { receiver, field, slots } = fixture(1); + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { depth: 0.2 }); + const geometry = slots[0]?.geometry; + expect(() => + field.project(receiver, receiver.geometry, new Vector3(), normal, "stone", { depth: 0.01 }), + ).toThrow(/bias/i); + expect(slots[0]?.geometry).toBe(geometry); + field.dispose(); + }); + 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/__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/render/decals.ts b/packages/create-threenative/templates/shooter/src/render/decals.ts index 66c7c20657..221096547a 100644 --- a/packages/create-threenative/templates/shooter/src/render/decals.ts +++ b/packages/create-threenative/templates/shooter/src/render/decals.ts @@ -1,7 +1,9 @@ import { + type BufferGeometry, DataTexture, DoubleSide, - type Material, + Euler, + Matrix4, Mesh, MeshBasicMaterial, type Object3D, @@ -12,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. @@ -125,9 +128,109 @@ const scratchQuaternion = new Quaternion(); const scratchRoll = new Quaternion(); const scratchNormal = new Vector3(); +type DecalMesh = Mesh; +type DecalFade = { duration: number; opacity: (progress: number) => number }; + +function fadeOpacity(fade: DecalFade, progress: number): number { + const opacity = fade.opacity(progress); + if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1) + throw new Error("Invalid decal opacity: the authored curve must return a value in [0, 1]."); + return opacity; +} + +/** 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; }; @@ -135,9 +238,16 @@ type Variant = { export class DecalField { readonly #variants = new Map(); readonly #geometry: PlaneGeometry; + readonly #parent: Object3D; + readonly #size: number; + readonly #projected = new Map< + DecalMesh, + { receiver: Mesh; removed: () => void; elapsed: number; fade?: DecalFade } + >(); readonly #offset: number; #placed = 0; #capacity = 0; + #disposed = false; /** Slots stop being submitted once their pipeline exists. See `settle`. */ #settled = false; @@ -148,6 +258,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>; @@ -156,6 +267,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][]) { @@ -168,7 +281,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 @@ -229,6 +342,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); @@ -244,6 +358,179 @@ 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. + * `uvRect` selects normalized atlas bounds without mutating the borrowed map. Optional fade + * duration is seconds; its authored opacity curve receives normalized progress in [0, 1). + * Call `update(dt)` each frame for expiry and receivers removed through an ancestor subtree. + */ + project( + receiver: Mesh, + geometry: BufferGeometry, + point: Vector3, + normal: Vector3, + variant: TVariant, + options: { + depth: number; + scale?: number; + uvRect?: readonly [number, number, number, number]; + fade?: DecalFade; + }, + ): 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.", + ); + } + if (this.#offset < 0 || this.#offset > options.depth / 2) + throw new Error("Invalid decal bias: offset must be in [0, projector depth / 2]."); + const rect = options.uvRect; + if ( + rect !== undefined && + (rect.length !== 4 || + !rect.every(Number.isFinite) || + rect[0] < 0 || + rect[1] < 0 || + rect[2] > 1 || + rect[3] > 1 || + rect[0] >= rect[2] || + rect[1] >= rect[3]) + ) + throw new Error("Invalid decal atlas rectangle: ordered UV bounds in [0, 1] required."); + // Snapshot timing so mutating a caller's options cannot invalidate a live mark. + const fade = options.fade === undefined ? undefined : { ...options.fade }; + if ( + fade !== undefined && + (!Number.isFinite(fade.duration) || fade.duration <= 0 || typeof fade.opacity !== "function") + ) + throw new Error("Invalid decal fade: positive finite duration and opacity curve required."); + const opacity = fade === undefined ? 1 : fadeOpacity(fade, 0); + 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); + if (rect !== undefined) { + const uv = projected.getAttribute("uv"); + for (let i = 0; i < uv.count; i += 1) + uv.setXY( + i, + rect[0] + uv.getX(i) * (rect[2] - rect[0]), + rect[1] + uv.getY(i) * (rect[3] - rect[1]), + ); + } + 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; + mesh.material.opacity = opacity; + receiver.add(mesh); + const removed = (): void => this.#releaseProjection(mesh); + this.#projected.set(mesh, { receiver, removed, elapsed: 0, fade }); + 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); + } + } + + /** Advance authored fades and clean detached ancestors, visiting at most the slot capacity. */ + update(dt = 0): void { + if (!Number.isFinite(dt) || dt < 0) + throw new Error("Invalid decal timestep: finite nonnegative seconds required."); + for (const [mesh, entry] of this.#projected) { + if (!this.#contains(entry.receiver)) { + this.#releaseProjection(mesh); + continue; + } + if (entry.fade === undefined) continue; + const elapsed = Math.min(entry.elapsed + dt, entry.fade.duration); + if (elapsed >= entry.fade.duration) this.#releaseProjection(mesh); + else { + mesh.material.opacity = fadeOpacity(entry.fade, elapsed / entry.fade.duration); + entry.elapsed = elapsed; + } + } + } + + #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; + mesh.material.opacity = 1; + } + /** 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 }; @@ -253,6 +540,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; } @@ -261,9 +549,15 @@ 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) { + this.#releaseProjection(mesh); + mesh.removeFromParent(); + mesh.material.dispose(); + } + family.material.dispose(); } this.#variants.clear(); this.#geometry.dispose(); 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();