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.
@@ -0,0 +1,87 @@
|
||||
# Patterned scenery rendering
|
||||
|
||||
A patterned wall or background uses one small repeating image for each finish and tint. The camera samples that same image wherever the surface appears. A pan or zoom changes the visible rectangle and sampling scale; it does not create a new image for each camera position.
|
||||
|
||||

|
||||
|
||||
## Render pipeline
|
||||
|
||||
1. A visible, committed patterned surface requests its style from the scene's `StageSurfaceBakePool`.
|
||||
2. The cache checks the style key. A ready entry is reused; a missing entry is queued for the shared baker.
|
||||
3. At most one queued style is baked per process frame. The baker draws a four-world-unit overscan around the material period so edge details can be cropped without gaps.
|
||||
4. After the submitted drawing reaches `frame_post_draw`, the pool captures the image, crops the period, generates mipmaps, and publishes an `ImageTexture`.
|
||||
5. The temporary vector commands are cleared. The surface draws one camera-clipped quad using the shared image, repeat sampling, and mipmapped filtering.
|
||||
|
||||
The GPU still renders the textured rectangle every frame. When the view changes, the CPU also updates that rectangle and its texture coordinates. This is a small drawing command; it does not rebuild brick faces, grain, pores, or other vector details. The cache counters distinguish image builds and readbacks from ordinary redraws.
|
||||
|
||||
## Shared style images
|
||||
|
||||
The cache is owned by a scene-scoped pool. Its key includes finish, tint, wall/background kind, material version, repeat period, pixel density, and mipmap configuration. It excludes surface position, camera position, zoom, rectangle size, and erase fragment. Thus same-style surfaces and fragments share one image, while a tint change correctly requests a different image.
|
||||
|
||||

|
||||
|
||||
The current periods are finite, seamless repeat units baked at two bitmap pixels per world unit:
|
||||
|
||||
| Finish | Repeat period in world units | Bitmap size |
|
||||
| --- | ---: | ---: |
|
||||
| Brick | 800 × 304 | 1600 × 608 |
|
||||
| Wood | 960 × 384 | 1920 × 768 |
|
||||
| Grass | 704 × 576 | 1408 × 1152 |
|
||||
| Carpet | 544 × 416 | 1088 × 832 |
|
||||
| Concrete | 1120 × 760 | 2240 × 1520 |
|
||||
|
||||
For example, a 100-world-unit brick remains 100 world units wide. At 0.4× zoom it covers about 40 screen pixels; zoom changes how the GPU samples the image, not the brick geometry. The finite repeat unit makes detail bounded, so landmarks can become noticeable on very large surfaces.
|
||||
|
||||
## Zoom and mipmaps
|
||||
|
||||
The renderer captures the material once at fixed density, then builds smaller mip levels from that image. During zoom-out the GPU samples a smaller level to reduce shimmer; during zoom-in it samples a larger level. This replaces separately drawing the material at several CPU-selected zoom levels. At the Stage's maximum 6× zoom, the source image is magnified three times and can look softer. The current quality/performance tradeoff is fixed in the cache configuration.
|
||||
|
||||

|
||||
|
||||
## Erasing and pattern alignment
|
||||
|
||||
The eraser still cuts a rectangle into up to four rectangular fragments. Each fragment draws only its own bounds, but requests the same style image; it does not bake a fragment-specific copy. Cancel restores the original geometry, and Commit retains the new fragments. Both use the existing material sampling rules.
|
||||
|
||||
The sampling origin is significant: walls use `global_position`; background ground uses `global_position + motif_origin_offset`. Each surface computes its source UV from its local visible position plus that draw origin, wrapped by the material period. This preserves the current alignment convention across cuts. Avoid changing the origin rule as a generic transform correction: unusual transformed fragments need separate verification before making a broader alignment claim.
|
||||
|
||||

|
||||
|
||||
## Memory and rebuild boundaries
|
||||
|
||||
The scene cache has a 64 MiB resident texture budget. Accounting includes mip levels, textures waiting for safe retirement, and reserved space for a capture. Unused entries are evicted by least-recent use when needed. A style held by active surfaces can prevent admission of another style; the pool reports this as budget pressure rather than exceeding the cap. Under ordinary use, returning to a recently used style should reuse its warm image. Many tint variants can force eviction and a later rebuild.
|
||||
|
||||
A style's first capture still costs CPU/GPU work and can briefly affect a frame. Once ready, moving the camera across a large wall does not enqueue position tiles or read back new images. The shared baker also clears its vector command list after capture, preventing the native drawing resources that previously accumulated in many per-tile viewports.
|
||||
|
||||
These images are temporary runtime data. A saved `.scene` keeps surface settings and geometry; the cache recreates its images when needed in a new Stage session. Closing the scene releases its pool.
|
||||
|
||||
## What causes another bake?
|
||||
|
||||
| Action | New image work? |
|
||||
| --- | --- |
|
||||
| Pan left, then return right | No, while the style remains cached. |
|
||||
| Zoom in or out | No. The GPU samples existing mipmaps. |
|
||||
| Resize a wall | No. Change the rectangle and texture coordinates. |
|
||||
| Erase, Cancel, or Commit | No, if the style is ready. Change geometry and clipping. |
|
||||
| Use the same finish and tint on another wall | No, if the style is ready. Share its image. |
|
||||
| Choose a new finish or tint | Bake once if that style is missing. |
|
||||
| Return to an evicted style, or open a fresh session | Recreate the missing image. |
|
||||
|
||||
## Terms used here
|
||||
|
||||
- **Bake:** render the vector artwork once to make an image.
|
||||
- **Texture:** the image the GPU samples when drawing.
|
||||
- **Quad:** one rectangle, represented by two triangles.
|
||||
- **UVs:** coordinates that select which part of a texture to sample.
|
||||
- **Mipmaps:** smaller copies of an image, prepared in advance for zooming out.
|
||||
- **Readback:** copy the rendered image from the GPU so the cache can keep an independent texture.
|
||||
|
||||
## Scope and code map
|
||||
|
||||
The bitmap cache is for committed patterned walls and backgrounds in render-capable runs. Plain paint surfaces, interactive placement previews, and headless runs retain the direct renderer path. Fully opaque scenery can suppress a fully hidden surface's texture request; partial coverage remains conservative.
|
||||
|
||||
- [StageSurfaceCache](../../scripts/stage/scenery/stage_surface_cache.gd) owns style handles, the scene pool, bake queue, image capture, mipmaps, cache keys, and budget statistics.
|
||||
- [StageRectSurface](../../scripts/stage/scenery/stage_rect_surface.gd) requests the style, clips the surface to the visible camera area, and draws the repeated texture.
|
||||
- [StageSurfaceRenderer](../../scripts/stage/scenery/stage_surface_renderer.gd) defines patterned artwork, material periods, and the direct-drawing path.
|
||||
- [Repeating scenery material cache spec](../specs/scenery_bitmap_tile_cache.md) records implementation constraints and acceptance checks.
|
||||
|
||||
In one observed warm run of `fps.scene`, four style entries used 36,399,144 bytes (about 34.71 MiB); 720 warm camera frames recorded zero builds, readbacks, or evictions. This is a run snapshot, not a guarantee for other scenes, tint combinations, or hardware.
|
||||
@@ -0,0 +1,43 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="640" viewBox="0 0 1200 640" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Erasing changes rectangles while sharing the same background texture</title>
|
||||
<desc id="desc">A rectangular cut turns one wall into top, bottom, left and right fragments. All fragments sample the existing image using their pattern coordinates. The image itself is not painted over or copied.</desc>
|
||||
<defs>
|
||||
<pattern id="brick" width="80" height="48" patternUnits="userSpaceOnUse"><rect width="80" height="48" fill="#d2cbaf"/><rect x="2" y="2" width="36" height="19" fill="#25b8df"/><rect x="42" y="2" width="36" height="19" fill="#39c7ee"/><rect x="-18" y="26" width="36" height="19" fill="#32c2e8"/><rect x="22" y="26" width="36" height="19" fill="#21b4da"/><rect x="62" y="26" width="36" height="19" fill="#32c2e8"/></pattern>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto"><path d="M0 0L10 5L0 10Z" fill="#475569"/></marker>
|
||||
<style>text{font-family:Segoe UI,Arial,sans-serif;fill:#172033}.title{font-size:30px;font-weight:700}.heading{font-size:22px;font-weight:700}.body{font-size:18px}.small{font-size:16px}.arrow{fill:none;stroke:#475569;stroke-width:3;marker-end:url(#arrow)}</style>
|
||||
</defs>
|
||||
<rect width="1200" height="640" rx="16" fill="#f8fafc"/>
|
||||
<text x="40" y="53" class="title">Erase the wall geometry. Keep the shared image.</text>
|
||||
<text x="40" y="86" class="body">Fragments crop their own rectangles and use the existing pattern coordinates.</text>
|
||||
<text x="50" y="136" class="heading">Before: one wall rectangle</text>
|
||||
<g transform="translate(50 160)">
|
||||
<rect width="460" height="300" fill="url(#brick)" stroke="#94a3b8" stroke-width="2"/>
|
||||
<rect x="155" y="90" width="150" height="120" fill="#fff" fill-opacity=".3" stroke="#b45309" stroke-width="4" stroke-dasharray="10 6"/>
|
||||
<rect x="174" y="131" width="112" height="36" rx="6" fill="#fff7ed"/>
|
||||
<text x="230" y="155" class="body" text-anchor="middle">Cut here</text>
|
||||
</g>
|
||||
<path d="M532 310H648" class="arrow"/>
|
||||
<text x="590" y="283" class="small" text-anchor="middle">Erase area</text>
|
||||
|
||||
<text x="680" y="136" class="heading">After: up to four fragments</text>
|
||||
<g transform="translate(680 160)">
|
||||
<rect width="460" height="300" fill="#dbeafe"/>
|
||||
<g fill="url(#brick)" stroke="#0f766e" stroke-width="3">
|
||||
<rect width="460" height="90"/>
|
||||
<rect x="0" y="210" width="460" height="90"/>
|
||||
<rect x="0" y="90" width="155" height="120"/>
|
||||
<rect x="305" y="90" width="155" height="120"/>
|
||||
</g>
|
||||
<g fill="#fff"><rect x="195" y="26" width="70" height="36" rx="6"/><rect x="182" y="242" width="96" height="36" rx="6"/><rect x="43" y="131" width="70" height="36" rx="6"/><rect x="342" y="131" width="78" height="36" rx="6"/></g>
|
||||
<text x="230" y="51" class="body" text-anchor="middle">Top</text>
|
||||
<text x="230" y="267" class="body" text-anchor="middle">Bottom</text>
|
||||
<text x="78" y="155" class="body" text-anchor="middle">Left</text>
|
||||
<text x="381" y="155" class="body" text-anchor="middle">Right</text>
|
||||
<text x="230" y="143" class="body" text-anchor="middle">Empty area</text>
|
||||
<text x="230" y="173" class="small" text-anchor="middle">Scene behind</text>
|
||||
</g>
|
||||
|
||||
<rect x="40" y="510" width="1120" height="95" rx="12" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="62" y="546" class="heading">All remaining pieces still use the same cached texture.</text>
|
||||
<text x="62" y="580" class="body">Cancel restores the original geometry. Commit keeps the fragments. Neither paints into the image.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,39 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1200" height="630" viewBox="0 0 1200 630" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Zoom uses smaller copies already stored in the material texture</title>
|
||||
<desc id="desc">The brick material starts at 1600 by 608 bitmap pixels. Its mip chain includes 800 by 304, 400 by 152 and 200 by 76, continuing down to one pixel. These copies are prepared once. Brick world dimensions stay fixed.</desc>
|
||||
<defs>
|
||||
<pattern id="brick" width="100" height="38" patternUnits="userSpaceOnUse"><rect width="100" height="38" fill="#d2cbaf"/><rect x="2" y="2" width="46" height="15" fill="#26b8df"/><rect x="52" y="2" width="46" height="15" fill="#39c7ee"/><rect x="-23" y="21" width="46" height="15" fill="#32c2e8"/><rect x="27" y="21" width="46" height="15" fill="#21b4da"/><rect x="77" y="21" width="46" height="15" fill="#32c2e8"/></pattern>
|
||||
<pattern id="half" xlink:href="#brick" patternTransform="scale(.5)"/>
|
||||
<pattern id="quarter" xlink:href="#brick" patternTransform="scale(.25)"/>
|
||||
<pattern id="eighth" xlink:href="#brick" patternTransform="scale(.125)"/>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto"><path d="M0 0L10 5L0 10Z" fill="#7c3aed"/></marker>
|
||||
<style>text{font-family:Segoe UI,Arial,sans-serif;fill:#172033}.title{font-size:30px;font-weight:700}.heading{font-size:22px;font-weight:700}.body{font-size:18px}.small{font-size:16px}.arrow{fill:none;stroke:#7c3aed;stroke-width:3;marker-end:url(#arrow)}</style>
|
||||
</defs>
|
||||
<rect width="1200" height="630" rx="16" fill="#f8fafc"/>
|
||||
<text x="40" y="53" class="title">Zoom samples ready-made sizes of the same image.</text>
|
||||
<text x="40" y="86" class="body">Mipmaps are generated once with the material. The GPU selects and blends them during rendering.</text>
|
||||
|
||||
<rect x="60" y="150" width="400" height="152" fill="url(#brick)" stroke="#94a3b8"/>
|
||||
<path d="M471 228H508" class="arrow"/>
|
||||
<rect x="530" y="188" width="200" height="76" fill="url(#half)" stroke="#94a3b8"/>
|
||||
<path d="M741 228H778" class="arrow"/>
|
||||
<rect x="800" y="207" width="100" height="38" fill="url(#quarter)" stroke="#94a3b8"/>
|
||||
<path d="M911 228H948" class="arrow"/>
|
||||
<rect x="970" y="217" width="50" height="19" fill="url(#eighth)" stroke="#94a3b8"/>
|
||||
<text x="1043" y="234" class="heading">…</text>
|
||||
|
||||
<text x="260" y="342" class="heading" text-anchor="middle">1600 × 608</text>
|
||||
<text x="260" y="373" class="small" text-anchor="middle">Base image: 2 pixels / world unit</text>
|
||||
<text x="630" y="342" class="heading" text-anchor="middle">800 × 304</text>
|
||||
<text x="630" y="373" class="small" text-anchor="middle">Half size</text>
|
||||
<text x="850" y="342" class="heading" text-anchor="middle">400 × 152</text>
|
||||
<text x="850" y="373" class="small" text-anchor="middle">Quarter size</text>
|
||||
<text x="1000" y="342" class="heading" text-anchor="middle">200 × 76</text>
|
||||
<text x="1000" y="373" class="small" text-anchor="middle">Eighth size</text>
|
||||
<text x="60" y="414" class="small">The actual chain continues down to 1 × 1. Diagram sizes are scaled for illustration.</text>
|
||||
|
||||
<rect x="40" y="450" width="1120" height="143" rx="12" fill="#ecfdf5" stroke="#0f766e" stroke-width="2"/>
|
||||
<text x="62" y="486" class="heading">World size, image size and screen size are different things.</text>
|
||||
<text x="62" y="521" class="body">A brick stays 100 world units wide. At 40% zoom, it covers about 40 screen pixels.</text>
|
||||
<text x="62" y="556" class="body">Camera zoom changes sampling detail; it does not bake another image or resize the brick geometry.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,51 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="640" viewBox="0 0 1200 640" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Background rendering: prepare a style once, reuse it while moving the camera</title>
|
||||
<desc id="desc">A visible surface requests its finish and tint from the scene cache. A cache hit goes directly to the GPU repeat sampler. A cache miss uses one temporary baker, captures an image and mipmaps, and releases vector commands.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto"><path d="M0 0L10 5L0 10Z" fill="#475569"/></marker>
|
||||
<style>text{font-family:Segoe UI,Arial,sans-serif;fill:#172033}.title{font-size:30px;font-weight:700}.heading{font-size:22px;font-weight:700}.body{font-size:18px}.small{font-size:16px}.arrow{fill:none;stroke:#475569;stroke-width:3;marker-end:url(#arrow)}</style>
|
||||
</defs>
|
||||
<rect width="1200" height="640" rx="16" fill="#f8fafc"/>
|
||||
<text x="40" y="53" class="title">Prepare a material once. Reuse it as the camera moves.</text>
|
||||
<text x="40" y="86" class="body">Only visible, committed patterned scenery requests a cached material.</text>
|
||||
|
||||
<rect x="40" y="140" width="250" height="126" rx="12" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="60" y="177" class="heading">Visible surface</text>
|
||||
<text x="60" y="210" class="body">Finish + tint</text>
|
||||
<text x="60" y="239" class="small">Example: blue brick</text>
|
||||
<path d="M292 203H370" class="arrow"/>
|
||||
|
||||
<rect x="390" y="140" width="260" height="126" rx="12" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="410" y="177" class="heading">Scene style cache</text>
|
||||
<text x="410" y="210" class="body">Already have this image?</text>
|
||||
<text x="410" y="239" class="small">Shared by all matching surfaces</text>
|
||||
<path d="M652 203H740" class="arrow"/>
|
||||
<text x="674" y="182" class="small">Hit</text>
|
||||
|
||||
<rect x="760" y="140" width="400" height="126" rx="12" fill="#ecfdf5" stroke="#0f766e" stroke-width="2"/>
|
||||
<text x="782" y="177" class="heading">GPU draws the visible rectangle</text>
|
||||
<text x="782" y="210" class="body">Repeat texture + sample its mipmaps</text>
|
||||
<text x="782" y="239" class="small">Pan / zoom updates sampling coordinates</text>
|
||||
|
||||
<path d="M520 268V324" class="arrow"/>
|
||||
<text x="540" y="304" class="small">Miss: prepare this style</text>
|
||||
<rect x="390" y="344" width="260" height="158" rx="12" fill="#f5f3ff" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="410" y="379" class="heading">One temporary baker</text>
|
||||
<text x="410" y="412" class="body">Draw one pattern period</text>
|
||||
<text x="410" y="441" class="body">Wait for GPU; capture image</text>
|
||||
<text x="410" y="476" class="small">Then clear vector commands</text>
|
||||
<path d="M652 423H740" class="arrow"/>
|
||||
|
||||
<rect x="760" y="344" width="400" height="158" rx="12" fill="#f5f3ff" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="782" y="379" class="heading">Store the reusable texture</text>
|
||||
<text x="782" y="412" class="body">Crop overscan; generate mipmaps</text>
|
||||
<text x="782" y="441" class="body">Keep the image, not the vector artwork</text>
|
||||
<text x="782" y="476" class="small">Ready styles stay warm within the cache budget</text>
|
||||
<path d="M960 342V282" class="arrow"/>
|
||||
<text x="980" y="309" class="small">Ready</text>
|
||||
|
||||
<rect x="40" y="548" width="540" height="56" rx="10" fill="#dbeafe"/>
|
||||
<text x="59" y="582" class="body">Blue route: reuse during ordinary pan and zoom</text>
|
||||
<rect x="604" y="548" width="556" height="56" rx="10" fill="#ede9fe"/>
|
||||
<text x="623" y="582" class="body">Purple route: bake only when a style is missing</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,44 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="650" viewBox="0 0 1200 650" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Many wall rectangles share one material image</title>
|
||||
<desc id="desc">One blue-brick image supplies Wall A, Wall B, and an erased fragment. Different sizes, positions, and camera views do not create more images. A different tint or finish is a different cached style.</desc>
|
||||
<defs>
|
||||
<pattern id="brick" width="64" height="40" patternUnits="userSpaceOnUse">
|
||||
<rect width="64" height="40" fill="#d2cbaf"/>
|
||||
<g><rect x="2" y="2" width="28" height="15" fill="#25b8df"/><rect x="34" y="2" width="28" height="15" fill="#39c7ee"/><rect x="-14" y="22" width="28" height="15" fill="#32c2e8"/><rect x="18" y="22" width="28" height="15" fill="#21b4da"/><rect x="50" y="22" width="28" height="15" fill="#32c2e8"/></g>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto"><path d="M0 0L10 5L0 10Z" fill="#2563eb"/></marker>
|
||||
<style>text{font-family:Segoe UI,Arial,sans-serif;fill:#172033}.title{font-size:30px;font-weight:700}.heading{font-size:22px;font-weight:700}.body{font-size:18px}.small{font-size:16px}.arrow{fill:none;stroke:#2563eb;stroke-width:3;marker-end:url(#arrow)}</style>
|
||||
</defs>
|
||||
<rect width="1200" height="650" rx="16" fill="#f8fafc"/>
|
||||
<text x="40" y="53" class="title">Wall area can grow. The number of style images stays the same.</text>
|
||||
<text x="40" y="86" class="body">Position, wall size, camera position and zoom are not part of the style key.</text>
|
||||
|
||||
<rect x="400" y="117" width="400" height="163" rx="12" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<rect x="420" y="140" width="160" height="112" fill="url(#brick)" stroke="#94a3b8"/>
|
||||
<text x="602" y="162" class="heading">One shared image</text>
|
||||
<text x="602" y="194" class="body">Blue brick</text>
|
||||
<text x="602" y="224" class="small">Varied brick faces</text>
|
||||
<text x="602" y="250" class="small">Repeats both ways</text>
|
||||
<path d="M600 282V311H210V341" class="arrow"/>
|
||||
<path d="M600 311V341" class="arrow"/>
|
||||
<path d="M600 311H990V341" class="arrow"/>
|
||||
|
||||
<rect x="40" y="360" width="340" height="192" rx="12" fill="#fff" stroke="#cbd5e1" stroke-width="2"/>
|
||||
<text x="62" y="396" class="heading">Wall A</text>
|
||||
<rect x="62" y="416" width="296" height="103" fill="url(#brick)"/>
|
||||
|
||||
<rect x="430" y="360" width="340" height="192" rx="12" fill="#fff" stroke="#cbd5e1" stroke-width="2"/>
|
||||
<text x="452" y="396" class="heading">Wall B</text>
|
||||
<rect x="452" y="416" width="190" height="103" fill="url(#brick)"/>
|
||||
<text x="659" y="455" class="small">Same</text>
|
||||
<text x="659" y="480" class="small">style</text>
|
||||
|
||||
<rect x="820" y="360" width="340" height="192" rx="12" fill="#fff" stroke="#cbd5e1" stroke-width="2"/>
|
||||
<text x="842" y="396" class="heading">Erased fragment</text>
|
||||
<rect x="842" y="416" width="90" height="103" fill="url(#brick)"/>
|
||||
<text x="952" y="455" class="small">Only the visible</text>
|
||||
<text x="952" y="480" class="small">rectangle changes</text>
|
||||
|
||||
<text x="40" y="591" class="body">A different finish or tint is a separate style. A matching warm style reuses the existing image.</text>
|
||||
<text x="40" y="621" class="small" fill="#64748b">Patterns shown here are schematic; the actual brick image contains 8 columns × 8 rows of varied artwork.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,42 @@
|
||||
# Scenery surface caching
|
||||
|
||||
`StageRectSurface` renders committed patterned scenery through reusable texture tiles.
|
||||
Drawing previews use the adaptive lightweight renderer. Paint remains a single rectangle.
|
||||
|
||||
## Camera movement
|
||||
|
||||
Tiles use a fixed grid in the surface's local coordinates. Texture resolution changes in
|
||||
powers of two, so adjacent wheel steps reuse existing tiles. Panning requests only newly
|
||||
exposed tiles. Existing textures stay visible while missing tiles are prepared, with a
|
||||
lightweight surface motif beneath them to prevent blank areas.
|
||||
|
||||
`StageSurfaceCache` keeps queued tiles as metadata. A shared queue creates and renders at
|
||||
most one tile per process frame across all surfaces. It also retires at most two obsolete
|
||||
render targets per frame. This avoids large allocations, vector rebuilds, and batches of
|
||||
GPU resource releases during camera input.
|
||||
|
||||
Each tile has a 256 × 128 pixel core and a four pixel gutter. The gutter is cropped during
|
||||
compositing to prevent seams. Each surface requests at most 192 visible tiles and retains
|
||||
at most 224 tiles, evicting older unused entries. Half-resolution tiles retain full brick
|
||||
texture and wood grain. Only quarter-resolution tiles and below omit fine texture, for
|
||||
distant views. Brick and plank positions remain fixed. Detail can fill in progressively
|
||||
after movement, including when returning from a distant view to a normal zoom level.
|
||||
|
||||
## Invalidation and coverage
|
||||
|
||||
Geometry, finish, tint, and motif-origin changes invalidate that surface's tiles. Camera
|
||||
movement selects tiles without invalidating their artwork. Render targets belong to the
|
||||
surface node; queued references are weak so deleted surfaces cannot keep jobs alive.
|
||||
|
||||
Fully opaque rectangular sibling surfaces can suppress artwork entirely hidden behind
|
||||
them. Coverage checks account for transforms, draw order, and visibility. Transparent
|
||||
surfaces, custom materials, differing relative-Z conventions, and Y-sorted parents disable
|
||||
this optimization conservatively. Revealing a surface resumes its pending tiles.
|
||||
|
||||
## Verification
|
||||
|
||||
`tests/test_scenery_surface_cache.gd` checks rendered appearance against the direct vector
|
||||
renderer, tile reuse and invalidation, the shared build limit, allocation and retention
|
||||
bounds, and covered-surface behavior. Run it with a graphical Godot renderer for pixel
|
||||
comparisons. The adaptive preview regressions remain in
|
||||
`tests/test_scenery_surface_preview.gd` and `tests/test_scenery_brick_preview.gd`.
|
||||
@@ -178,4 +178,8 @@ This document tracks known technical debt, optimization opportunities, and minor
|
||||
| 2026-10-17 | Wrote the Phase 19 selected-artwork depth inspector specification (`docs/specs/phase_19_selected_artwork_depth_inspector.md`) for final sign-off. It reuses the existing `StageInspectorPanel` and `StagePanelChoiceGroup`, applies Far/Near/On the floor immediately to one live or missing Scenery placement through `StageSceneryArtwork.set_depth()`, and follows the Stage's existing dirty/save path. The current `.scene` depth field, `StageDrawOrder`, and `stage_panels` tokens need no changes. Updated #72 to include immediate depth edits in a future Stage-wide Undo phase. No runtime code changed by this specification entry. |
|
||||
| 2026-10-17 | Wrote the Phase 19 Background ground and wall color architecture specification (`docs/specs/phase_19_background_ground_and_wall_color.md`) for final sign-off. It defines distinct inert wall/backdrop scene types over a small shared rectangle contract, world-aligned bounded vector materials, selected-piece color preview without mutating saved tint, one eight-color Stage settings list, scene v1.6 migration of old Brick/Wood placeholder green, shared Stage inspector controls, and confirmed single-piece backdrop erasure. Logged #74 for zoom-consistent motif budgets and #75 for the old unused wall-color ambiguity. No runtime code changed. |
|
||||
|
||||
| 2026-10-20 | Approved the scenery bitmap tile cache architecture (`docs/specs/scenery_bitmap_tile_cache.md`). Lightweight `StageSurfaceCache` handles share immutable bitmap tiles through one `StageSurfaceBakePool` under the current scene, with a 64 MiB resident bitmap budget and one reusable offscreen baker. Camera-visible tiles use four fixed zoom levels (0.125, 0.5, 1, 2); the coarsest tile bakes first and refined detail overlays it. At maximum 6× zoom, the 2× bitmap may look softer because it is magnified threefold. Erased fragments clip shared tiles only when their complete pattern sampling phase matches. The previous per-surface viewport cache retained vector commands and had no scene-wide resource limit, a likely contributor to `Element limit reached`; the exact RID pool remains unconfirmed. GPU readback during bitmap capture and regeneration after budget eviction are accepted costs to measure during implementation. |
|
||||
|
||||
| 2026-10-20 | Replaced the positional scenery tile design after the user observed continuous buffering when panning after zoom. The accepted revision in `docs/specs/scenery_bitmap_tile_cache.md` bakes one seamless, 2× bitmap per finish/tint period and uses GPU repeat plus generated mipmaps for all camera positions and zooms. Erased fragments share the style texture and crop it using their existing sample coordinate. The scene-wide 64 MiB limit includes mipmaps; inactive style variants may be evicted, while recently used styles stay warm when space allows. This trades unique random detail across an unlimited wall for finite repeated variation and keeps the existing possible 3× bitmap enlargement at maximum zoom. A style change can bake once; repeated camera movement must not increment build or readback counts after styles are ready. |
|
||||
|
||||
_End of Log_
|
||||
|
||||
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 114 KiB |
@@ -0,0 +1,32 @@
|
||||
# Child-Facing Dialog Design Principles
|
||||
|
||||
Use this guide when creating or revising child-facing dialogs, popups, and short setup flows. It captures the shared visual language established in the Choose Scenery dialog. In-stage selected-object inspectors continue to follow the [Stage panel UI system](stage_panel_ui_system.md).
|
||||
|
||||
## Visual style
|
||||
|
||||
- Use rounded dialog shells, buttons, choice tiles, and preview containers. A corner radius around 16–22 logical pixels is a useful starting point for large controls; use smaller radii for compact swatches.
|
||||
- Give controls a clear outline. Use about a 2 px border at rest and 3 px for selected, hovered, or focused states. Keep focus visually distinct from selection.
|
||||
- Style the main action with a saturated blue fill, bold white text, and a visible border. Use the theme’s action color or edit-mode blue token rather than adding a one-off color.
|
||||
- Use dark neutral fills and light text for secondary actions. Leave about 12–16 logical pixels between separate footer buttons so they read as individual choices.
|
||||
- Use large, readable type: roughly 30 px for a dialog title, 22–26 px for section headings, and 20–22 px for main control labels and body copy. Helper text can be smaller when it remains easy to read.
|
||||
- Keep pointer targets at least 44 × 44 logical pixels. Provide generous padding around labels and icons.
|
||||
|
||||
## Dismissal and action labels
|
||||
|
||||
- Give the child a visible, labeled action that describes what happens, such as **Cancel**, **Done**, **Back**, or **Close**.
|
||||
- Avoid making a small corner **×** the default or only way to dismiss a dialog. Use it only when it adds a useful secondary shortcut; the labeled action remains available.
|
||||
- Make the primary action’s wording describe the result, such as **Draw wall panel** or **Use color**. Keep Cancel separate from the primary action and visually secondary.
|
||||
- Escape can remain a keyboard shortcut, but it does not replace a visible action.
|
||||
|
||||
## Theme and implementation
|
||||
|
||||
- Put reusable colors, sizes, spacing, radii, and border widths in `SandboxTheme` or `PropTheme` tokens. Apply local stylebox overrides instead of mutating a shared theme resource.
|
||||
- Keep larger text from clipping: use containers that can expand or wrap, and size the dialog after its visible content has been laid out.
|
||||
- Preserve clear hover, pressed, selected, focused, and disabled states. Do not use color as the only indication of a state.
|
||||
- Adapt the arrangement to each workflow while keeping this shared visual language. The Choose Scenery dialog in `scripts/shared/ui/asset_selector.gd` and its `popups.asset_selector` theme tokens are the current implementation reference.
|
||||
|
||||
## Stable position during dynamic resizing
|
||||
|
||||
- When a dialog changes size after a tab, filter, or other in-place choice, preserve its current anchor so the clicked control stays under the pointer.
|
||||
- Let the dialog grow from its stable edge while there is room. Before expanding, account for the usable viewport; if the full content will not fit, cap the dialog and let its content area scroll.
|
||||
- Move the dialog only as much as needed to keep essential controls visible. Avoid recentering after every content change, which makes controls jump away from the pointer.
|
||||
@@ -118,8 +118,8 @@ These are relationship examples, not fixed pixel geometry. On a narrow viewport
|
||||
|
||||
### Wall panel
|
||||
|
||||
- **Backgrounds → Wall panel** offers picture choices **Paint**, **Brick**, and **Wood**, plus a large **Color** swatch/button. Common colors appear first; **More colors…** opens a themed color dialog with a visible Cancel. Finish and color choices are reflected in a small preview and then in the translucent Stage draft.
|
||||
- Choosing the look closes the chooser and arms **Draw wall panel**. The capture bar says **“Drag across the space for a wall panel”**, with **Done** and **✕ Cancel**. The pointer has a corner reticle and a nearby pill **“Drag to draw wall”**. A press sets the first corner; dragging draws a world-anchored translucent patterned rectangle; release commits one finite panel. The pattern preview remains at sensible scale rather than stretching with the draft.
|
||||
- **Backgrounds → Wall panel** offers picture choices **Paint**, **Brick**, and **Wood**, plus a large preview of only the selected finish (Paint is a flat color; Brick and Wood show their texture). The selected-color tile replaces the chooser content and footer with the themed custom color view; **Back to wall panel** discards its draft and returns to setup, while **Use color** applies it and returns. The chooser's **Recently used** row is the only recent-color list. In the expanded setup chooser, **Cancel** exits and **Draw wall panel** starts placement; there is no separate Back or close-X action in that state.
|
||||
- Choosing a look updates its preview. Pressing **Draw wall panel** closes the chooser and arms placement. The capture bar says **“Drag across the space for a wall panel”**, with **Done** and **✕ Cancel**. The pointer has a corner reticle and a nearby pill **“Drag to draw wall”**. A press sets the first corner; dragging draws a world-anchored translucent patterned rectangle; release commits one finite panel. The pattern preview remains at sensible scale rather than stretching with the draft.
|
||||
- Snap, when enabled, visibly aligns the draft to nearby grid lines. An unusably small draft shows a red outline and **“Make the wall bigger”**; release leaves no panel. A valid draft uses the EDIT accent plus a neutral outline and dimension cue. This is appearance only: a helper in the chooser says **“Looks like a wall. Stickmen can pass through.”**
|
||||
- Touch can drag or tap the first and second corners. The first tap leaves a visible anchored corner and continuous rectangle preview; the second tap commits. The same visible Cancel and Done remain available. Right-click cancels a draft and exits the wall tool under the exit rules below.
|
||||
|
||||
@@ -219,7 +219,7 @@ This matrix identifies current UI that already exists. “Extend/share” is a d
|
||||
| Scenery chooser and Artwork cards | `AssetSelector`, `AssetCard`, `StageAssetBrowser`, thumbnail cache/renderers, `scenes/stage/asset_selector.tscn` | **Extend/share the gallery shell and card behavior, reuse browsing/thumbnail work.** The current selector is a fixed four-column paged popup and sets card focus to `FOCUS_NONE`; these must be corrected for responsive layout and keyboard use in the common path rather than copied into a Scenery-only popup. Backgrounds/Artwork tabs use native button choice states and shared popup styling; placement-role text stays feature-local. |
|
||||
| Sky and wall/Ground finish cards | `AssetCard` and the responsive gallery behavior above; `SandboxTheme` popup/card tokens | **Share the same preview-card and focus/selected-state rules.** Sky and finishes supply their own preview imagery and one-tap selection meaning; card dragging is disabled for these choices. They do not pretend to be `.prop` assets or reuse `StageAssetBrowser` asset scanning. Wall and Ground share names/preview motifs for Paint, Brick, and Wood. |
|
||||
| Artwork tags, Props filters, authoring tags | No existing reusable tag-chip control; `StagePanelChoiceGroup` is a vertical exclusive inspector control; Stage speech UI demonstrates `HFlowContainer` wrapping | **Extract one shared wrapping filter/choice treatment.** Use native focusable buttons and one theme contract across Scenery browsing, Props browsing, and optional multi-tag authoring. Single-choice browser filters and multi-choice authoring use the same visual states but different selection rules. Do not clone local chip styling or force the vertical inspector group into a wrapping browser row. |
|
||||
| Wall Color and More colors dialog | `ColorSwatchSelector`; native `ColorPickerButton` already used by the Prop Editor; `StageModalViews` dialog theme | **Extend/share the color-swatch treatment** so it wraps on narrow screens, has visible chosen/focus states, and uses Stage tokens. Use native color picker behavior in a themed Stage dialog. The Prop Editor's `PropTheme` styling is not a Stage style source; wall/color values remain feature-local. |
|
||||
| Wall Color and custom color view | `ColorSwatchSelector`; native `ColorPicker` in the themed Stage view | **Extend/share the color-swatch treatment** so it wraps on narrow screens, has visible chosen/focus states, and uses Stage tokens. Open custom color selection from the selected-color tile, keep one recent-color row in the chooser, and provide **Back to wall panel** / **Use color** actions. The Prop Editor's `PropTheme` styling is not a Stage style source; wall/color values remain feature-local. |
|
||||
| Scenery overlap **Pieces here** | `StageModalViews.show_objects_here()` and its themed, viewport-clamped, scrollable `PopupPanel` | **Generalize the existing overlap chooser.** Supply Scenery-only candidates and child-facing piece names/depth, while retaining the same placement, 44 px rows, dismiss, and focus behavior. Do not create a second overlapping-items popup. |
|
||||
| Missing Scenery artwork | Existing `MissingPropPlaceholder`/`MissingStickmanPlaceholder` preserve missing placements; `StageModalViews` missing-assets dialog; `StageInspectorPanel`; `StageAssetBrowser` gallery/file chooser | **Reuse and generalize the recovery path.** Keep saved placement continuity and one themed load summary, but give Scenery its neutral labeled marker, selection-card repair actions, and the existing artwork/file choosers. Do not copy the older red marker styling or create a second asset gallery or dialog skin. |
|
||||
| **Erase this piece?** | `StageModalViews` confirmation/dialog theming; existing Erase Use spot `ConfirmationDialog` and safe/danger treatment in `SandboxStage` | **Reuse the confirmation pattern.** Share the button sizing, themed window, and danger/safe styles instead of copying the spot-erase styling into another private function. The affected piece and wording are local. |
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# 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.
|
||||