Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,15 @@ native/ Rust implementations (one crate per platform)
linux/ Vulkan/OpenGL + X11 + PulseAudio
android/ Vulkan/OpenGL ES + NativeActivity + AAudio
web/ WebGPU/WebGL + Canvas + Web Audio API (WASM via wasm-pack)
watchos/ Planned — README only, blocked on Perry watchOS target support
visionos/ Metal + UIKit-style shell (wgpu; iOS/tvOS-family port)
watchos/ SwiftUI Canvas (2D) + SceneKit (3D) — no wgpu/Jolt on
the watch; own .glb loader; built via Perry's
watchos-swift-app feature (see docs/watchos-target.md)
```

## FFI Pattern

Each platform implements ~230 `bloom_*` FFI functions declared in `package.json` under `perry.nativeLibrary.functions`. Native platforms use `#[no_mangle] extern "C"`, web uses `#[wasm_bindgen]`.
Each platform implements ~470 `bloom_*` FFI functions declared in `package.json` under `perry.nativeLibrary.functions`. Native platforms use `#[no_mangle] extern "C"`, web uses `#[wasm_bindgen]`.

String parameters are `i64` on native (Perry StringHeader pointers) and NaN-boxed string IDs on web (converted by JS glue layer).

Expand Down Expand Up @@ -129,7 +132,7 @@ String parameters are `i64` on native (Perry StringHeader pointers) and NaN-boxe
- `begin_frame` resets per-frame lighting state; renderer FFI calls
before `initWindow` panic with "Engine not initialized".

Physics FFI (~110 of the ~230) is generated by the `define_physics_ffi!` macro in `native/shared/src/physics_jolt.rs`; each platform crate invokes it once to re-export the full surface. On web the same surface is wasm_bindgen wrappers forwarding to `native/web/jolt_bridge.js`, which drives JoltPhysics.js.
Physics FFI (121 of the ~470) is generated by the `define_physics_ffi!` macro in `native/shared/src/physics_jolt.rs`; each platform crate invokes it once to re-export the full surface. On web the same surface is wasm_bindgen wrappers forwarding to `native/web/jolt_bridge.js`, which drives JoltPhysics.js.

## Web/WASM Target

Expand All @@ -143,7 +146,7 @@ Key features flags in `native/shared/Cargo.toml`:
- `jolt` — compiles the C++ Jolt shim via cmake on native; no-op on wasm32 (web uses JoltPhysics.js)
- `web` — uses web-time instead of std::time::Instant

The web crate exposes `_str` variants (accepting `&str`) and `_bytes` variants (accepting `&[u8]`) for functions that take strings or file data. The JS glue converts Perry NaN-boxed values via `__perryToJsValue` and fetches assets via sync XHR.
The web crate exposes `_str` variants (accepting `&str`) and `_bytes` variants (accepting `&[u8]`) for functions that take strings or file data. Perry's runtime decodes NaN-boxed FFI args to plain JS values (`wrapFfiForI64`) before the glue sees them; the glue routes string/asset params to the `_str`/`_bytes` variants and fetches assets via sync XHR.

## Key Files

Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,11 @@ setting the bar. ([full design rationale](docs/design-api.md))
| **Audio** | `@bloomengine/engine/audio` | Sound effects + music streaming |
| **Models** | `@bloomengine/engine/models` | 3D model loading (glTF, OBJ), skeletal animation |
| **Math** | `@bloomengine/engine/math` | Vectors, matrices, quaternions, easing |
| **Scene** | `@bloomengine/engine/scene` | Retained scene graph, frame callbacks, lighting |
| **Physics** | `@bloomengine/engine/physics` | Jolt-backed rigid + soft bodies, character, vehicles ([docs](docs/physics.md)) |
| **VFX** | `@bloomengine/engine/vfx` | GPU particle systems + decals |
| **World** | `@bloomengine/engine/world` | `.world.json` loading, validation, instantiation ([docs](docs/world-format.md)) |
| **Mobile** | `@bloomengine/engine/mobile` | Virtual joystick/buttons, touch-input helpers |

## Platforms

Expand All @@ -116,7 +120,7 @@ setting the bar. ([full design rationale](docs/design-api.md))
| Linux | Vulkan / OpenGL | Keyboard + mouse |
| iOS | Metal | Touch + gamepad |
| tvOS | Metal | Siri Remote + gamepad |
| watchOS | SwiftUI Canvas (no Metal) | Digital Crown + taps ([docs](docs/watchos-target.md)) |
| watchOS | SwiftUI Canvas (2D) + SceneKit (3D) | Digital Crown + taps ([docs](docs/watchos-target.md)) |
| Android | Vulkan / OpenGL ES | Touch + gamepad |
| **Web** | **WebGPU / WebGL** | **Keyboard + mouse + touch + gamepad** |

Expand Down
6 changes: 5 additions & 1 deletion bloom-renderer-spec-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
philosophy lives on in `README.md` and the raylib-modeled `src/` API).
Both were removed 2026-07-06; git history has them.

## Status vs. plan (as-built, 2026-07-06)
## Status vs. plan (as-built, 2026-07-16)

This document is the *plan*; the code has made choices where the plan
offered options, and diverged where reality was cheaper:
Expand All @@ -26,6 +26,10 @@ offered options, and diverged where reality was cheaper:
with material-path casters/receivers, material system with a stable
5-bind-group ABI (`material_abi.wgsl`), CPU+GPU pass profiler with
overlay, planar-reflection water, physical-resolution 2D text.
- **GI default:** SW GI (SDF-clipmap / Hi-Z tiers) is the shipping
default; the HW ray-query tier is opt-in (`BLOOM_HW_GI=1` /
`BLOOM_PT` / `--pt` on Windows, 66dad5b) after the measured A/B showed
+20 ms/frame on the 760M for a tonal-only difference (b34c1f3).
- **Live status lives in** `docs/perf/lumen-roadmap.md` (GI),
`docs/tickets.md` (EN-xxx work items), and per-ticket docs in
`docs/perf/`. When this spec and those disagree, those win.
Expand Down
7 changes: 4 additions & 3 deletions docs/design-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,11 @@ Bloom compiles TypeScript through [Perry](../../perry/perry) (our AOT compiler)
and hands data across an FFI boundary to platform-specific Rust crates. The
boundary has a specific shape, documented in `CLAUDE.md` and `package.json`:

- **~230 `bloom_*` FFI functions** declared in `package.json` under
- **~465 `bloom_*` FFI functions** declared in `package.json` under
`perry.nativeLibrary.functions`.
- **Native platforms** use `#[no_mangle] extern "C"` — a C ABI.
- **Web** uses `#[wasm_bindgen]` with JS glue that NaN-boxes string IDs.
- **Web** uses `#[wasm_bindgen]`; Perry's runtime decodes NaN-boxed args
(`wrapFfiForI64`) and the JS glue routes strings to `_str` variants.
- **String parameters** are `i64` Perry StringHeader pointers on native, NaN-
boxed IDs on web.
- **Handles** (textures, sounds, models, physics bodies) are all `i64` / plain
Expand Down Expand Up @@ -139,7 +140,7 @@ single FFI call has to punch back through.
interface Vec3 { x: number; y: number; z: number }
interface Texture { handle: number; width: number; height: number }
interface Sound { handle: number }
interface Model { handle: number }
interface Model { handle: number; meshCount: number; materialCount: number; transform: Mat4 }

// Functions operate on data:
const tex = loadTexture("assets/hero.png");
Expand Down
6 changes: 4 additions & 2 deletions docs/migration-0.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ myAtlas.id myAtlas.handle
old exact-dt behavior. See docs/physics.md "Stepping".
- Stale handles (use-after-free/destroy) now fail lookups instead of
aliasing whatever object reused the slot.
- `*Raw` function variants are documented `@internal` — they exist only
as a compiler workaround and will be removed.
- Most `*Raw` function variants are documented `@internal`. They work
around an aarch64-Android Perry miscompilation of `obj.field` reads
feeding f64 FFI args; a few (e.g. `loadMusicRaw`) are actively
recommended on Android and are staying.
- Coordinate system is now documented at the top of the physics and
scene modules: right-handed, Y-up, meters, SI units.
15 changes: 9 additions & 6 deletions docs/perf/003-stochastic-ssr.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# 003 — Stochastic SSR + temporal accumulation

**Effort:** ~2 days · **Expected gain:** SSR 4× cheaper per frame · **Status:** open
**Effort:** ~2 days · **Expected gain:** SSR 4× cheaper per frame · **Status:** landed

## Problem
## Problem (historical — pre-landing)

The SSR pass (`SSR_SHADER_WGSL` in `renderer.rs`) marches 32 steps per pixel at
The SSR pass (`SSR_SHADER_WGSL`, now in `renderer/shaders/ssgi.rs`) marched 32 steps per pixel at
half-res. Every metallic fragment (and near-metal dielectric at low roughness)
does the full march. With Sponza's chrome lamp fittings and polished marble,
that's a significant pixel count × 32 steps.
Expand Down Expand Up @@ -58,7 +58,10 @@ pass is a smaller version of the same idea.
existing TAA by writing both current + history.
- Preserve the `ssr_enabled` toggle and the quality-preset plumbing.

## Files likely to change
## Files changed (as landed)

- `native/shared/src/renderer.rs` — rewrite SSR_SHADER_WGSL, add history RT,
add (or extend existing) reprojection pass.
- `native/shared/src/renderer/ssr_pass.rs` — stochastic GGX march (8 steps)
with 3×3 pre-filter + neighborhood-clamped temporal history
(`record_ssr_temporal`).
- `native/shared/src/renderer/shaders/ssgi.rs` — `SSR_SHADER_WGSL` lives here
now (the old single `renderer.rs` was split into the `renderer/` module).
13 changes: 8 additions & 5 deletions docs/perf/004-cached-shadow-maps.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 004 — Cache shadow cascades for static casters

**Effort:** ~2 days · **Expected gain:** Shadow pass → ~0 after first frame (was ~14 ms) · **Status:** open
**Effort:** ~2 days · **Expected gain:** Shadow pass → ~0 after first frame (was ~14 ms) · **Status:** landed

## Problem

Expand Down Expand Up @@ -73,8 +73,11 @@ this per 128×128 page. For Bloom, per-cascade is enough for now.
- Add a `setShadowsAlwaysFresh(bool)` TS toggle as an escape hatch for games
that do a lot of dynamic light changes.

## Files likely to change
## Files changed (as landed)

- `native/shared/src/shadows.rs` — add `dirty` flag + invalidation helpers.
- `native/shared/src/renderer.rs` — gate the shadow pass on `dirty`.
- `native/shared/src/scene.rs` — dirty the shadows when a transform changes.
- `native/shared/src/shadows.rs` — `dirty` / `always_fresh` flags,
per-cascade `rendered_cascade_sig`, static caster caches.
- `native/shared/src/renderer/shadow_pass.rs` — the shadow pass gates on
`scene.shadow_version` (the old single `renderer.rs` was split into the
`renderer/` module).
- `native/shared/src/scene.rs` — bumps `shadow_version` when casters change.
16 changes: 14 additions & 2 deletions docs/perf/005-depth-prepass.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 005 — Depth prepass for main HDR pass

**Effort:** ~1 day · **Expected gain:** main_hdr 17 ms → ~8 ms · **Status:** deprioritized (see Findings, 2026-04-21)
**Effort:** ~1 day · **Expected gain:** main_hdr 17 ms → ~8 ms · **Status:** landed (revived as EN-044 after the 2026-04-21 prototype failed — see Postscript)

## Problem

Expand Down Expand Up @@ -79,7 +79,8 @@ Classic **Z-prepass**. Before `main_hdr_pass`:

## Files likely to change

- `native/shared/src/renderer.rs` — new `scene_depth_pipeline`, new
- `native/shared/src/renderer/` (as landed: `scene_pass.rs` + pipeline
creation in `mod.rs`) — new `scene_depth_pipeline`, new
`scene_pipeline_equal`, new prepass block in `end_frame_with_scene`.
- `native/shared/src/scene.rs` — add a `render_depth_only()` method mirroring
`render()`.
Expand Down Expand Up @@ -152,3 +153,14 @@ That's a multi-day refactor, not the ~1 day this ticket originally
estimated. Not worth it while the frame budget is dominated by bloom /
TAA / SSGI / final-composite passes (the real targets of tickets 007 and
010).

## Postscript (2026-07) — revived and landed as EN-044

The prepass was later rebuilt along the exact lines the Recommendation
above sketched, and is now live: `renderer/scene_pass.rs` runs an
unconditional `bloom_depth_prepass` render pass (alpha-cutout honored via
the `fs_depth_prepass` fragment entry), with the main pass loading depth
and drawing through `scene_pipeline_prepassed` (`Equal`/`LessEqual`).
Both correctness gaps from the post-mortem — alpha discard and coplanar
layering — are handled. The Findings above stand as the record of why
the first attempt failed.
17 changes: 11 additions & 6 deletions docs/perf/006-shadow-pass-culling.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ On the panning-camera cache-miss path at `--quality 2 --fps-only 300`:
The ticket's 14 ms → 7-10 ms target was set against the pre-95da6af
baseline. `CASCADE_MAP_SIZE` dropped 2048 → 1024 in 95da6af, which already
took the pass from 14 ms to ~3 ms on the cache-miss path, so absolute
headroom is small. The proportional cut (34% on the GPU side) matches
headroom is small. (The 1024 drop was itself later reverted —
`shadows.rs:20` is back to `CASCADE_MAP_SIZE = 2048` today; the culling
win measured here is independent of map size.) The proportional cut (34% on the GPU side) matches
the ticket's intent. The 10% FPS target in the original acceptance is
unreachable at today's baseline because shadow is no longer a 14 ms cost.

Expand Down Expand Up @@ -79,13 +81,16 @@ test — no need to compute the camera/light union manually.
- `extract_frustum_planes` in `scene.rs` already does the Gribb-Hartmann
extraction. Reuse.
- `aabb_outside_frustum` also already there. Reuse.
- Build the per-cascade draw list in the same loop that's currently at
`renderer.rs` ~line 8250 (look for `ShadowDrawEntry`). The existing loop
builds one shared list — extend to build `[Vec<ShadowDrawEntry>; 3]`.
- Build the per-cascade draw list in the same loop as the existing shadow
draw-list construction (now in `renderer/shadow_pass.rs`; look for
`ShadowDrawEntry`). The existing loop builds one shared list — extend to
build `[Vec<ShadowDrawEntry>; 3]`.
- Keep the `SHADOW_MAX_NODES` cap per cascade (currently 1024).
- Don't cull by the light's near/far plane — in a static Sponza scene the
light volume covers everything anyway.

## Files likely to change
## Files changed (as landed)

- `native/shared/src/renderer.rs` — shadow-pass draw-list construction.
- `native/shared/src/renderer/shadow_pass.rs` — shadow-pass draw-list
construction with per-cascade frustum culling (the old single
`renderer.rs` was split into the `renderer/` module).
6 changes: 3 additions & 3 deletions docs/perf/007-prep-wgpu-upgrade.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 007-prep — wgpu upgrade (Metal ray-query unlock)

**Effort:** 2-3 days · **Expected gain:** none (enabler) · **Status:** open
**Effort:** 2-3 days · **Expected gain:** none (enabler) · **Status:** landed (wgpu 29)

## Problem

Expand All @@ -25,7 +25,7 @@ at the time this ticket starts — verify at kickoff). Sweep API churn across:
- `native/shared/src/renderer/mod.rs` — largest surface area; will touch most
breaking changes (descriptor field renames, feature-flag bits, surface-config
shapes, ray-tracing types if they became stable).
- `native/shared/src/renderer/shaders.rs` — naga WGSL parser accepts a slightly
- `native/shared/src/renderer/shaders/` — naga WGSL parser accepts a slightly
different shape each release (e.g. attribute syntax, `override` semantics).
- Platform crates — window creation / surface config wiring.

Expand Down Expand Up @@ -53,7 +53,7 @@ No renderer logic changes beyond what the compiler forces. No Lumen code yet.
- 8 `Cargo.toml` files (native/shared + 7 platform crates).
- 8 `Cargo.lock` files.
- `native/shared/src/renderer/mod.rs` — descriptor / feature / surface churn.
- `native/shared/src/renderer/shaders.rs` — WGSL parser churn if any.
- `native/shared/src/renderer/shaders/` — WGSL parser churn if any.
- `native/web/src/lib.rs` — `wasm-bindgen` interactions that touch wgpu types.
- Platform `lib.rs` / `main.rs` files where surface creation happens.

Expand Down
8 changes: 4 additions & 4 deletions docs/perf/007a-lumen-screen-probes-sw.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 007a — Lumen screen probes (SW trace, Hi-Z)

**Effort:** 2-3 days · **Expected gain:** SSGI 2×+ faster · **Status:** open
**Effort:** 2-3 days · **Expected gain:** SSGI 2×+ faster · **Status:** landed

Supersedes the old ticket 007. Phase 1a of the [Lumen roadmap](lumen-roadmap.md).
Runs in parallel with 007b; both depend on 007-prep.
Expand All @@ -20,10 +20,10 @@ current ~2.9 M — ~60× fewer rays.

## Approach — file-by-file

### Shaders — `native/shared/src/renderer/shaders.rs`
### Shaders — now `native/shared/src/renderer/shaders/ssgi.rs`

Delete `SSGI_SHADER_WGSL` (lines 2219-2405) and `SSGI_TEMPORAL_SHADER_WGSL`
(lines 2412-2496). Add:
Delete `SSGI_SHADER_WGSL` and `SSGI_TEMPORAL_SHADER_WGSL` (both gone as
landed — replaced by the `SSGI_PROBE_*` shaders). Add:

- `PROBE_HELPERS_WGSL` — `oct_encode(dir) -> vec2<f32>`, `oct_decode(uv) -> vec3<f32>`,
`view_pos_from_depth(uv, depth, inv_proj)`, `reconstruct_view_normal(view_pos)`
Expand Down
2 changes: 1 addition & 1 deletion docs/perf/007b-lumen-screen-probes-hw.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 007b — Lumen screen probes (HW trace, ray-query)

**Effort:** 1-2 weeks · **Expected gain:** off-screen occlusion + bleed · **Status:** open
**Effort:** 1-2 weeks · **Expected gain:** off-screen occlusion + bleed · **Status:** landed (HW trace is opt-in since 66dad5b — `BLOOM_HW_GI=1` / `BLOOM_PT` / `--pt` on Windows; SW is the default tier)

Phase 1b of the [Lumen roadmap](lumen-roadmap.md). Depends on 007-prep and
007a; develops in parallel with 007a, merges after 007a lands so the probe
Expand Down
3 changes: 2 additions & 1 deletion docs/perf/008-visibility-buffer.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ modes. This is a ~2-day win instead of 2 weeks.

## Files likely to change

- `native/shared/src/renderer.rs` — scene_pipeline, main_hdr_pass, shading
- `native/shared/src/renderer/` (`mod.rs` + `scene_pass.rs`; the old single
`renderer.rs` was split) — scene_pipeline, main_hdr_pass, shading
pass, SSR/SSGI/SSAO inputs.
- `native/shared/src/scene.rs` — mesh_id assignment, vertex buffer layout.

Expand Down
3 changes: 2 additions & 1 deletion docs/perf/009-gpu-driven-rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ feature flags. Check adapter support at device creation.

## Files likely to change

- `native/shared/src/renderer.rs` — shared VB/IB, descriptor buffer, GPU
- `native/shared/src/renderer/mod.rs` (the old single `renderer.rs` was
split into the `renderer/` module) — shared VB/IB, descriptor buffer, GPU
cull compute shader, new render pass using `draw_indexed_indirect_count`.
- `native/shared/src/scene.rs` — reworking of per-node GPU resources.

Expand Down
3 changes: 2 additions & 1 deletion docs/perf/010-async-compute.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ This ticket depends on having compute-shader versions of SSAO/SSR/SSGI first

## Files likely to change

- `native/shared/src/renderer.rs` — encoder splitting, queue creation,
- `native/shared/src/renderer/mod.rs` (the old single `renderer.rs` was
split into the `renderer/` module) — encoder splitting, queue creation,
fence/semaphore plumbing.
- Possibly `native/macos/src/lib.rs` — if wgpu-hal drop is needed for
second queue.
Expand Down
17 changes: 16 additions & 1 deletion docs/perf/011-cross-platform-ffi.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ bloom_print_profiler_summary()
```

Each one is a thin wrapper around an existing method on `engine().renderer`
or `engine().profiler`. Shared code (`native/shared/src/renderer.rs`,
or `engine().profiler`. Shared code (`native/shared/src/renderer/mod.rs`,
`native/shared/src/profiler.rs`) is platform-agnostic and already exposes
everything.

Expand Down Expand Up @@ -117,3 +117,18 @@ Verification:
audit).
- The TS API side (`src/core/index.ts` ~240-316) matches the FFI
surface 1:1.

## Superseded by `define_core_ffi!` (as-built today)

The per-platform hand-copied FFI blocks described above no longer exist:
they drifted badly enough (Android shipped 60 functions behind, Windows
silently no-op'd the scene graph) that the whole non-physics FFI surface
was consolidated into the shared `define_core_ffi!` macro in
`native/shared/src/ffi_core/` (e.g. `bloom_set_quality_preset` lives in
`ffi_core/visual.rs`, profiler getters in `ffi_core/game_loop.rs`). Each
platform crate invokes the macro once in its `lib.rs`. New `bloom_*`
functions are added to the macro — never hand-copied per platform — and
`tools/validate-ffi.js` enforces parity in CI. The sole exception is web,
which still hand-writes `#[wasm_bindgen]` wrappers in
`native/web/src/lib.rs`. The TIMESTAMP_QUERY per-platform feature
requests from this ticket survive unchanged.
3 changes: 2 additions & 1 deletion docs/perf/012-remaining-bind-group-caches.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ existing four.

## Files likely to change

- `native/shared/src/renderer.rs` — add 5 more cache fields, build/read
- `native/shared/src/renderer/mod.rs` + `renderer/postfx_chain.rs` (the old
single `renderer.rs` was split) — add 5 more cache fields, build/read
them in `end_frame_with_scene`, invalidate in `resize()`.

## Deferred — prototype notes
Expand Down
2 changes: 1 addition & 1 deletion docs/perf/013-lumen-surface-cache.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 013 — Lumen Surface Cache (Mesh Cards)

**Effort:** 1-2 weeks · **Expected gain:** HW bounce reaches full Lumen quality · **Status:** open
**Effort:** 1-2 weeks · **Expected gain:** HW bounce reaches full Lumen quality · **Status:** landed (V3 — as-built: 64×64 cards in a 4096² atlas, per-frame drained capture queue; see the [README](README.md) 013 row for the landed summary)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Synchronize the card-resolution details.

The new as-built summary says cards are 64×64, but this document still specifies 256×256 cards in the approach and cost model. Update the stale 256×256 references and related bandwidth/VRAM estimates before treating this as the authoritative landed record.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/perf/013-lumen-surface-cache.md` at line 3, Update the
013-lumen-surface-cache document’s approach and cost-model references from
256×256 cards to the landed 64×64 resolution, and recalculate the associated
bandwidth and VRAM estimates consistently. Keep the as-built 64×64 summary and
other unrelated performance details unchanged.


Phase 2 of the [Lumen roadmap](lumen-roadmap.md). Depends on 007a + 007b.

Expand Down
Loading
Loading