From 2595e76aa4c352486a0842a27c3a0a4b48a9615b Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Sun, 27 Sep 2026 20:43:22 -0700 Subject: [PATCH 1/2] perf(data-gpu): archetype-level voxel exclusion in physicsRenderBridge, static-instance fast path for pbrFactorRender physicsRenderBridge previously excluded voxel-tagged and already-resolved rows with a per-row `store.read`/`store.get` check inside the scan loop, so it visited every row of every matching archetype each frame - including large populations of static voxel colliders (e.g. a terrain heightfield) that were always going to be skipped anyway. Move the `voxelShapeName` / `mesh` exclusion into the archetype query itself so those rows never enter the loop (O(pending) instead of O(all)). pbrFactorRender gains an opt-in `_pbrFactorStaticInstances` resource: once a scene's drawable count stabilizes, cache the per-mesh instance buffers and bind groups and just re-issue `drawIndexed` on later frames, skipping the per-frame TRS gather and instance-buffer upload for scenes that never move or grow (e.g. baked terrain). Co-Authored-By: Claude Sonnet 5 --- .../pbr-render/pbr-factor-render-plugin.ts | 74 +++++++++++++++++++ .../pbr-render/physics-bridge-plugin.ts | 27 +++---- 2 files changed, 86 insertions(+), 15 deletions(-) diff --git a/packages/data-gpu/src/graphics/rendering/pbr-render/pbr-factor-render-plugin.ts b/packages/data-gpu/src/graphics/rendering/pbr-render/pbr-factor-render-plugin.ts index 6ab947190..582753fff 100644 --- a/packages/data-gpu/src/graphics/rendering/pbr-render/pbr-factor-render-plugin.ts +++ b/packages/data-gpu/src/graphics/rendering/pbr-render/pbr-factor-render-plugin.ts @@ -22,15 +22,37 @@ import { interface InstCache { buffer: GPUBuffer; indexBuffer: GPUBuffer; bindGroup: GPUBindGroup; capacity: number } +interface StaticPrimDraw { + readonly primId: Entity; + readonly n: number; + readonly indexCount: number; + readonly indexFormat: GPUIndexFormat; + readonly vertexBuffer: GPUBuffer; + readonly indexBuffer: GPUBuffer; + readonly bindGroup: GPUBindGroup; +} + /** * pbrFactorRender — instanced primitive PBR from material factors only (no map * sampling). Extends `pbrIblRender` for IBL + skybox + optional glTF draws. * Pairs with `materialPaletteGpu` (`_paletteIndex` on factor materials). * * Pairs with `materialPaletteGpu` (`_paletteIndex`). Idle when no factor materials or drawables exist. + * + * Set resource `_pbrFactorStaticInstances = true` for fully static scenes + * (e.g. terrain heightfield): after drawable count stabilizes, skip per-frame + * TRS gather + instance buffer uploads and only re-issue drawIndexed. */ export const pbrFactorRender = Database.Plugin.create({ extends: Database.Plugin.combine(pbrIblRender, materialPaletteGpu, displayTransform), + resources: { + /** + * When true, instance matrices are uploaded once the drawable set is + * stable; later frames only rebind and draw. Clear or leave false for + * scenes that move/add bodies every frame. + */ + _pbrFactorStaticInstances: { default: false as boolean, nonPersistent: true }, + }, systems: { pbrFactorPrimitiveRenderSystem: { schedule: { during: ["render"], after: ["beginRenderPass", "pbrIblRenderSystem"], before: ["endRenderPass"] }, @@ -54,10 +76,15 @@ export const pbrFactorRender = Database.Plugin.create({ let mats = new Float32Array(64 * 16); const batches = new Map(); + let staticDraws: StaticPrimDraw[] | null = null; + let staticDrawCount = -1; + let staticStableFrames = 0; + return () => { const { device, renderPassEncoder, canvasFormat, depthFormat, _sceneUniformsBuffer, _factorPaletteBindGroup, _iblIrradiance, _iblPrefiltered, _iblBrdfLut, + _pbrFactorStaticInstances, } = db.store.resources; if (!device || !renderPassEncoder || !_sceneUniformsBuffer || !_factorPaletteBindGroup) return; if (!_iblIrradiance || !_iblPrefiltered || !_iblBrdfLut) return; @@ -104,6 +131,21 @@ export const pbrFactorRender = Database.Plugin.create({ cachedIrradiance = _iblIrradiance; } + // Static path: re-draw without re-gather / re-upload. + if (_pbrFactorStaticInstances && staticDraws != null && staticDraws.length > 0) { + renderPassEncoder.setPipeline(pipeline); + renderPassEncoder.setBindGroup(0, sceneBindGroup); + renderPassEncoder.setBindGroup(1, _factorPaletteBindGroup); + renderPassEncoder.setBindGroup(2, iblBindGroup); + for (const d of staticDraws) { + renderPassEncoder.setBindGroup(3, d.bindGroup); + renderPassEncoder.setVertexBuffer(0, d.vertexBuffer); + renderPassEncoder.setIndexBuffer(d.indexBuffer, d.indexFormat); + renderPassEncoder.drawIndexed(d.indexCount, d.n); + } + return; + } + let mc = 0; for (const arch of db.store.queryArchetypes(["_paletteIndex"])) mc += arch.rowCount; if (mc !== matCount) { @@ -150,6 +192,8 @@ export const pbrFactorRender = Database.Plugin.create({ renderPassEncoder.setBindGroup(1, _factorPaletteBindGroup); renderPassEncoder.setBindGroup(2, iblBindGroup); + const nextStatic: StaticPrimDraw[] = []; + for (const arch of db.store.queryArchetypes(PRIMITIVE)) { const vbs = arch.columns._vertexBuffer, ibs = arch.columns._indexBuffer; const counts = arch.columns._indexCount, formats = arch.columns._indexFormat; @@ -187,7 +231,37 @@ export const pbrFactorRender = Database.Plugin.create({ renderPassEncoder.setVertexBuffer(0, vbs.get(i)!); renderPassEncoder.setIndexBuffer(ibs.get(i)!, formats.get(i)); renderPassEncoder.drawIndexed(counts.get(i), n); + + if (_pbrFactorStaticInstances) { + nextStatic.push({ + primId, + n, + indexCount: counts.get(i), + indexFormat: formats.get(i), + vertexBuffer: vbs.get(i)!, + indexBuffer: ibs.get(i)!, + bindGroup: entry.bindGroup, + }); + } + } + } + + // Freeze only after the drawable set stops growing for one frame + // (mesh resolve / material assign finish during the first seconds). + if (_pbrFactorStaticInstances) { + if (drawCount === staticDrawCount && drawCount > 0) { + staticStableFrames++; + } else { + staticStableFrames = 0; + staticDrawCount = drawCount; + } + if (staticStableFrames >= 1 && nextStatic.length > 0) { + staticDraws = nextStatic; } + } else { + staticDraws = null; + staticStableFrames = 0; + staticDrawCount = -1; } }; }, diff --git a/packages/data-gpu/src/graphics/rendering/pbr-render/physics-bridge-plugin.ts b/packages/data-gpu/src/graphics/rendering/pbr-render/physics-bridge-plugin.ts index 333d24484..be5235f5e 100644 --- a/packages/data-gpu/src/graphics/rendering/pbr-render/physics-bridge-plugin.ts +++ b/packages/data-gpu/src/graphics/rendering/pbr-render/physics-bridge-plugin.ts @@ -13,23 +13,22 @@ import { boundsFromShapeMesh } from "../../scene/model/shape/bounds-from-shape-m import type { ColliderMesh } from "../../../physics/body/collider-mesh.js"; import { interpolation } from "../interpolation-plugin.js"; -/** Authored column sets for primitive collider rows (RigidBody, ConvexBody, - * StaticCollider, MeshCollider). Query by these sets — not the named handle — - * so solver-tagged variants are found after migration. Rows with - * `voxelShapeName` are skipped at runtime (mechs voxel bodies). */ +/** + * Authored column sets for primitive collider rows still missing a render mesh. + * - exclude `mesh` → once assigned, rows drop out of the scan (O(pending) not O(all)). + * - exclude `voxelShapeName` → mechs voxel bodies use voxelShapeVisualBridge only + * (never visit ~all static voxel colliders every frame). + */ const PRIMITIVE_COLLIDER_QUERIES: readonly { include: readonly string[]; - exclude?: readonly string[]; + exclude: readonly string[]; }[] = [ - { include: RIGID_BODY_COMPONENTS, exclude: ["convexPoints"] }, - { include: [...RIGID_BODY_COMPONENTS, "convexPoints"] }, - { include: STATIC_COLLIDER_COMPONENTS, exclude: ["colliderMesh"] }, - { include: [...STATIC_COLLIDER_COMPONENTS, "colliderMesh"] }, + { include: RIGID_BODY_COMPONENTS, exclude: ["convexPoints", "voxelShapeName", "mesh"] }, + { include: [...RIGID_BODY_COMPONENTS, "convexPoints"], exclude: ["voxelShapeName", "mesh"] }, + { include: STATIC_COLLIDER_COMPONENTS, exclude: ["colliderMesh", "voxelShapeName", "mesh"] }, + { include: [...STATIC_COLLIDER_COMPONENTS, "colliderMesh"], exclude: ["voxelShapeName", "mesh"] }, ]; -const hasVoxelShapeName = (store: { read(id: Entity): Record | null }, id: Entity): boolean => - store.read(id)?.voxelShapeName != null; - /** * physicsRenderBridge — assigns default primitive render meshes for standard * physics bodies whose visual is the collider primitive (RigidBody, @@ -97,15 +96,13 @@ export const physicsRenderBridge = Database.Plugin.create({ for (const { include, exclude } of PRIMITIVE_COLLIDER_QUERIES) { for (const arch of db.store.queryArchetypes( include as typeof RIGID_BODY_COMPONENTS, - exclude ? { exclude: exclude as ["convexPoints"] | ["colliderMesh"] } : undefined, + { exclude: exclude as ["mesh"] }, )) { const ids = arch.columns.id; const css = arch.columns.colliderShape; const hes = arch.columns.halfExtents; for (let i = arch.rowCount - 1; i >= 0; i--) { const id = ids.get(i); - if (hasVoxelShapeName(db.store, id)) continue; - if (db.store.get(id, "mesh") != null) continue; const shape = css.get(i); const he = hes.get(i); let meshId: Entity; From 1f8e177d689044563e29f03ddc3ebcfe7f579d6c Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Sun, 27 Sep 2026 20:48:26 -0700 Subject: [PATCH 2/2] chore: bump monorepo to 0.10.21 Co-Authored-By: Claude Sonnet 5 --- package.json | 2 +- packages/data-ai/.claude-plugin/plugin.json | 2 +- packages/data-ai/package.json | 2 +- packages/data-gpu/package.json | 2 +- packages/data-lit/package.json | 2 +- packages/data-persistence/package.json | 2 +- packages/data-react/package.json | 2 +- packages/data-rpc/package.json | 2 +- packages/data-solid/package.json | 2 +- packages/data-sync/package.json | 2 +- packages/data-testing/package.json | 2 +- packages/data/package.json | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index d062b9049..f64769a1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-monorepo", - "version": "0.10.20", + "version": "0.10.21", "private": true, "engines": { "node": ">=24" diff --git a/packages/data-ai/.claude-plugin/plugin.json b/packages/data-ai/.claude-plugin/plugin.json index c0411c422..3b09d57ba 100644 --- a/packages/data-ai/.claude-plugin/plugin.json +++ b/packages/data-ai/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "adobe-data-ai", - "version": "0.10.20", + "version": "0.10.21", "description": "Architecture skills for @adobe/data — data-oriented modelling, archetype iteration, hot-path performance, and related conventions.", "author": { "name": "Adobe" diff --git a/packages/data-ai/package.json b/packages/data-ai/package.json index 4e8e42fb7..d11e8661c 100644 --- a/packages/data-ai/package.json +++ b/packages/data-ai/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-ai", - "version": "0.10.20", + "version": "0.10.21", "description": "Cross-agent architecture skills for @adobe/data — installable as a Claude Code plugin or copied into any Agent-Skills-compatible agent (Cursor, Codex).", "type": "module", "private": false, diff --git a/packages/data-gpu/package.json b/packages/data-gpu/package.json index ca92e27d0..5e8127b66 100644 --- a/packages/data-gpu/package.json +++ b/packages/data-gpu/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-gpu", - "version": "0.10.20", + "version": "0.10.21", "description": "Adobe data WebGPU plugins and types for graphics and compute", "type": "module", "private": false, diff --git a/packages/data-lit/package.json b/packages/data-lit/package.json index aeb839542..7449a6fdf 100644 --- a/packages/data-lit/package.json +++ b/packages/data-lit/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-lit", - "version": "0.10.20", + "version": "0.10.21", "description": "Adobe data Lit bindings - hooks, elements, decorators", "type": "module", "private": false, diff --git a/packages/data-persistence/package.json b/packages/data-persistence/package.json index 75e4ab0aa..cd5b484c9 100644 --- a/packages/data-persistence/package.json +++ b/packages/data-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-persistence", - "version": "0.10.20", + "version": "0.10.21", "description": "Worker-based incremental persistence layer for @adobe/data ECS over OPFS (browser) and node:fs (server).", "type": "module", "sideEffects": false, diff --git a/packages/data-react/package.json b/packages/data-react/package.json index 2dd65ba47..db37a3d9d 100644 --- a/packages/data-react/package.json +++ b/packages/data-react/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-react", - "version": "0.10.20", + "version": "0.10.21", "description": "Adobe data React bindings — hooks and context for ECS database", "type": "module", "private": false, diff --git a/packages/data-rpc/package.json b/packages/data-rpc/package.json index a41728d61..6e9aeaede 100644 --- a/packages/data-rpc/package.json +++ b/packages/data-rpc/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-rpc", - "version": "0.10.20", + "version": "0.10.21", "description": "Schema-driven, bidirectional projection of @adobe/data async data services across a boundary (iframe / MessagePort / Worker). Only Data crosses the wire.", "type": "module", "sideEffects": false, diff --git a/packages/data-solid/package.json b/packages/data-solid/package.json index bd9be7f8c..f94707fc0 100644 --- a/packages/data-solid/package.json +++ b/packages/data-solid/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-solid", - "version": "0.10.20", + "version": "0.10.21", "description": "Adobe data SolidJS bindings — context and provider for ECS database", "type": "module", "private": false, diff --git a/packages/data-sync/package.json b/packages/data-sync/package.json index 8e1d33638..ea41c628a 100644 --- a/packages/data-sync/package.json +++ b/packages/data-sync/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-sync", - "version": "0.10.20", + "version": "0.10.21", "description": "Multi-user real-time synchronisation for @adobe/data ECS — server, client, and in-process loopback.", "type": "module", "sideEffects": false, diff --git a/packages/data-testing/package.json b/packages/data-testing/package.json index c27f31693..eccb21696 100644 --- a/packages/data-testing/package.json +++ b/packages/data-testing/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-testing", - "version": "0.10.20", + "version": "0.10.21", "description": "Conformance-testing utilities (Match + Conformance runners) for @adobe/data ECS features", "type": "module", "sideEffects": false, diff --git a/packages/data/package.json b/packages/data/package.json index a35a94c74..c1ef0fc75 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data", - "version": "0.10.20", + "version": "0.10.21", "description": "Adobe data oriented programming library", "type": "module", "sideEffects": false,