Files
stickman/docs/specs/scenery_bitmap_tile_cache.md
ryan c1758efd3b Add tests for scenery surface caching and preview behavior
- Introduced `test_scenery_brick_preview.gd` to validate focused placement and CPU draw-cost for large brick walls.
- Added `test_scenery_surface_cache.gd` to assess texture budget, visual equivalence, and cache invalidation.
- Created `test_scenery_surface_preview.gd` to evaluate adaptive preview selection and placement behavior.
- Implemented various helper functions to measure drawing performance and validate expected behaviors during tests.
- Added unique identifiers for new test files to ensure proper tracking and management.
2026-10-21 11:06:57 -04:00

79 lines
9.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Repeating scenery material cache
Status: accepted architecture for the 2026-10-20 scenery cache follow-up.
## Problem and result
`content/stages/fps.scene` exposed two related costs. The old vector tile cache could accumulate enough native drawing resources to report `Element limit reached`. Its replacement bounds those resources, but panning after zoom still reveals new world-position tiles and continuously starts more bitmap captures. Brick, wood, grass, carpet, and concrete are repeated finishes. Bake **one finite, seamless material texture per finish and tint** and repeat it across every visible rectangle. Once that style is ready, camera pan and zoom update only the draw quad and sampling coordinates; they do not build or read back more imagery.
Keep the scene-scoped `StageSurfaceCache.BakePool` under `SceneTree.current_scene` as a sibling of `World`. `StageRectSurface` owns a lightweight `StageSurfaceCache` handle for its style. A single reusable `SubViewport` baker is enough. Paint and interactive placement keep their existing direct drawing paths. No `.scene` schema or child-facing control changes are needed.
```text
current_scene
├── World
│ ├── StageRectSurface ──┐
│ └── erased fragment ──┼── handles for the same finish + tint
└── StageSurfaceBakePool ─┴── one repeating ImageTexture
└── SurfaceBitmapBaker (temporary vector commands)
```
## Material periods and appearance
Bake every period at **2 bitmap pixels per world unit**. The material repeats in both directions. These dimensions are multiples of the current renderer's natural cells:
| Finish | Repeated cells | World period | Bitmap size at 2× |
| --- | ---: | ---: | ---: |
| Brick wall | 8 columns × 8 rows | 800 × 304 | 1600 × 608 |
| Wood wall | 3 columns × 6 rows | 960 × 384 | 1920 × 768 |
| Grass background | 16 × 16 | 704 × 576 | 1408 × 1152 |
| Carpet background | 16 × 16 | 544 × 416 | 1088 × 832 |
| Concrete background | 4 × 4 slabs | 1120 × 760 | 2240 × 1520 |
The 8-row brick and 6-row wood periods preserve stagger parity; the wood period also preserves its three-tone cycle. Keep brick size, plank size, color relationships, rough outlines, grain, pores, and other material features at their current world scale. Remove view-size-driven changes to brick or plank dimensions from the material bake: zoom must sample the same pattern. The resulting bitmap keeps full artwork at 40% zoom through GPU minification. At the Stage's maximum 6× zoom, the 2× bitmap is magnified threefold and may appear softer; this is an accepted quality trade-off.
The renderer uses absolute pattern cell coordinates for geometry and `posmod(cell_index, period_cell_count)` for **every decorative seed and tone choice**, including negative world coordinates. Wrap row and column before computing brick jitter, palette, marks, pores, wood tone, grain, knots, grass tufts, carpet fibers, and concrete speckles. Features crossing a period edge must be drawn from the matching wrapped neighbor, then clipped to the exact period bitmap. Check the first and last rows and columns together; a matching cell seed alone does not repair geometry or filtering at the seam. A finite period makes the former never-ending random variation visibly repeat; the chosen periods balance variety with bounded bitmap size.
## Rendering and erase coordinates
Generate mipmaps on the captured `Image` before creating its `ImageTexture`. On each patterned `StageRectSurface`, enable `CanvasItem.TEXTURE_REPEAT_ENABLED`. Draw the intersection of surface bounds and camera view as a single clipped quad with `draw_texture_rect_region(..., clip_uv = false)`; its source rectangle can span many repetitions. Compute the source position from the same renderer sample coordinate used today: **surface-local position plus `get_surface_draw_origin_world()`**, reduced with `fposmod` by the period before converting to bitmap pixels. Reducing the offset avoids large UV coordinates during long camera pans. Source width and height come from the visible local rectangle at two bitmap pixels per world unit. Godot's repeat sampler and mipmaps handle zoom without a cached zoom level.
The eraser continues splitting one rectangular surface into up to four fragments. Each fragment clips its own quad to its bounds and the camera. It requests the same style texture; the bitmap key is independent of fragment position, size, camera position, and zoom. Preserve the existing sampling convention: wall draw origin is `global_position`; background ground draw origin is `global_position + motif_origin_offset`. Moving, rotating, saving, reloading, erasing, Cancel, and Commit must keep each material's pattern aligned under those existing rules. A fragment's differing sample offset changes only its source UV, never the style bitmap.
Only request a style when some of that surface is camera-visible and not proven fully covered by later opaque scenery. Keep conservative coverage checks; partial union of multiple cover surfaces is optional. A surface with no texture yet draws its base color while its one style capture is queued. An already ready style remains warm through camera movement, including when the surface temporarily leaves the viewport.
## Cache and baker contracts
`StageSurfaceCache` remains a `RefCounted` handle with `signal updated`, `attach_to(owner_node: Node)`, `get_texture() -> Texture2D`, `dispose()`, and `static get_stats(owner_node: Node = null) -> Dictionary`. Its build request changes from a positional tile to a style request. The payload is finish (`StringName`), tint (`Color`), wall/background kind (`bool`), renderer/material revision, and the fixed period and density derived from finish. `StageRectSurface` no longer retains a dictionary of camera-position tiles. One handle may be replaced when tint or finish changes; other surfaces with the same key keep their texture.
`StageSurfaceCache.BakePool` has one entry per requested style. Entry identity includes all values that alter baked pixels, especially finish, tint, wall/background kind, and renderer revision. It excludes camera transform, surface bounds, eraser fragment, and sample phase. A queued entry with no live visible interested owner can be canceled. The pool serializes captures through one `SubViewport`, waits until the GPU has consumed its vector commands, reads the completed image, generates mipmaps, publishes an immutable `ImageTexture`, and clears baker vector commands. A failed capture leaves the base color visible and permits retry; it must not publish an empty ready texture.
Keep a **64 MiB global resident texture budget per scene**, including all mip levels, retired textures awaiting a safe frame, and reserved bytes for a build. One copy each of the five listed periods is about 32 MiB before mipmaps and 43 MiB with mipmaps at RGBA8; additional tint variants can require eviction. Evict styles without visible interested owners before styles in active use, and release canvas references safely before freeing a texture. Keep recently used styles warm so panning away and back does not trigger a new capture under normal budget. If active styles collectively exceed the budget, retain the hard cap and show base color for a style that cannot be admitted; expose this through stats rather than silently exceeding the budget. Revisit period size or density if this occurs in ordinary scenes.
`get_stats()` must expose at least resident texture bytes, entries, queued styles, active baker count, builds, readbacks, cache hits, and budget refusals. These counters make the reuse behavior observable without adding UI.
## Data flow
```mermaid
flowchart LR
Visible[Visible patterned surface] -->|finish + tint| Cache[Scene style cache]
Cache -->|ready texture| UV[Sample repeated texture at existing pattern offset]
Cache -->|first style request| Baker[One temporary bitmap baker]
Baker -->|capture + mipmaps| Cache
UV --> Clip[Clip quad to surface and camera]
Clip --> Stage[Stage canvas]
Camera[Pan or zoom] --> UV
```
## Observable acceptance and verification
1. Open `content/stages/fps.scene` in EDIT, DIRECT, and PLAY. Allow visible brick, wood, and concrete styles to become ready. Pan left and right repeatedly and zoom in and out through the same region. `builds` and `readbacks` stay unchanged after those styles are ready; only a new finish or tint may add a style capture. Frame movement does not pause for repeated tile buffering, and `Element limit reached` does not recur.
2. Inspect the left/right and top/bottom period boundaries for every finish at distant, 40%, normal, and close zoom. There are no lines, shifted brick courses, broken planks, or mismatched decorative marks. Zoom changes sampling detail without changing motif geometry.
3. Erase through a brick wall and background ground, Cancel, then erase and Commit. Inspect both sides of each fragment edge, then save and reload. The cut stays clear, surrounding patterns stay aligned, and the cache still has one entry per shared style.
4. Change a wall's tint and finish. The changed style may bake once; unchanged surfaces keep their ready texture. Return to the earlier style and pan again to check that recently used styles stay warm within the 64 MiB budget. Observe total bytes, pending styles, and baker lifetime during a long Stage session.
Use a render-capable run of the actual Stage for appearance and frame-time observations. This architecture update does not require a new test file.
## Trade-offs
A periodic material replaces unique random detail across an arbitrarily large wall with a bounded, repeating pattern. The chosen periods and seeded variation should make repetition unobtrusive at ordinary framing, but repeated landmarks can become noticeable on very large surfaces. GPU mipmaps replace four separately baked zoom levels and keep pan/zoom free of new builds. A one-time GPU readback remains when a finish/tint style first appears. Multiple color variants can evict warm styles under the 64 MiB cap, so those cases need measurement on representative scenes.