--- id: LSN-0031 ticket: render-all-scene-cache-and-camera-integration title: Frame Composition Belongs Above the Render Backend created: 2026-04-18 tags: [gfx, runtime, render, camera, scene, sprites, frame-composer] --- ## Context `DSC-0025` split canonical scene ownership from viewport caching and resolver policy, but the runtime still treated `Gfx.render_all()` as the operational frame entrypoint. That left scene binding, camera state, cache refresh, and sprite submission spread across the wrong layer. `DSC-0026` completed the integration by making `FrameComposer` the owner of frame orchestration and reducing `Gfx` to a backend that consumes already prepared render state. ## Key Decisions ### Frame Orchestration Must Not Live in the Backend **What:** `FrameComposer.render_frame()` became the canonical frame service, while `Gfx.render_all()` was retired from the runtime-facing flow. **Why:** The backend should execute composition, not decide scene binding, camera policy, cache refresh, or sprite lifecycle. Keeping those responsibilities above `Gfx` preserves a cleaner ownership model and avoids re-entangling policy with raster code. **Trade-offs:** This adds an explicit orchestration layer between runtime callsites and the renderer, but the resulting boundaries are easier to evolve and test. ### Scene Binding, Camera, Cache, and Sprites Form One Operational Unit **What:** `FrameComposer` owns: - active scene binding by bank id and shared scene reference; - camera coordinates in top-left world pixel space; - `SceneViewportCache` and `SceneViewportResolver`; - a frame-emission `SpriteController`. **Why:** These concerns all define what a frame is. Splitting them across multiple owners would recreate stale-state bugs and make no-scene behavior ambiguous. **Trade-offs:** The composer becomes a richer subsystem, but it carries policy in one place instead of leaking it into unrelated APIs. ### The World Path Must Stay Tile-Size Agnostic **What:** The integrated frame path derives cache sizing, resolver math, and world-copy behavior from per-layer scene metadata instead of a hard-coded `16x16` assumption. **Why:** The scene contract already allows canonical `8x8`, `16x16`, and `32x32` tile sizes. The frame service has to consume that contract faithfully or it becomes a hidden compatibility break. **Trade-offs:** The integration and tests need to exercise more than the legacy default path, but the renderer no longer bakes in a false invariant. ## Patterns and Algorithms - Put frame policy in a dedicated orchestration layer and keep the renderer backend-oriented. - Treat scene binding, camera state, cache lifetime, and sprite submission as one cohesive frame model. - Refresh cache state inside the orchestrator before composition instead of letting the renderer discover refresh policy. - Prefer frame-emission sprite submission with internal ordering over caller-owned sprite slots. - Keep the no-scene path valid so world composition remains optional, not mandatory. ## Pitfalls - Leaving `render_all()` alive as a canonical path creates a fragile dual-service model. - Letting `Gfx` own cache refresh semantics collapses the boundary between policy and execution. - Requiring a scene for every frame quietly breaks sprite-only or fade-only operation. - Testing only `16x16` scenes hides regressions against valid `8x8` or `32x32` content. ## Takeaways - Frame composition belongs in a subsystem that owns policy, not in the backend that draws pixels. - Scene binding, camera, cache, resolver, and sprite submission should converge under one frame owner. - No-scene rendering is part of the contract and should stay valid throughout integration work. - Tile-size assumptions must be derived from canonical scene metadata, never from renderer habit.