Update Grid Rendering Pipeline

John McCardle 2026-02-07 23:48:35 +00:00
commit aa5205e8ad

@ -1,390 +1,390 @@
# Grid Rendering Pipeline # Grid Rendering Pipeline
## Overview ## Overview
The Grid rendering pipeline handles multi-layer tilemap rendering with chunk-based caching for optimal performance. It supports arbitrary numbers of rendering layers, viewport culling, zoom, and entity rendering. The Grid rendering pipeline handles multi-layer tilemap rendering with chunk-based caching for optimal performance. It supports arbitrary numbers of rendering layers, viewport culling, zoom, and entity rendering.
**Parent Page:** [[Grid-System]] **Parent Page:** [[Grid-System]]
**Related Pages:** **Related Pages:**
- [[Performance-and-Profiling]] - Metrics and profiling tools - [[Performance-and-Profiling]] - Metrics and profiling tools
- [[Entity-Management]] - Entity rendering within grids - [[Entity-Management]] - Entity rendering within grids
- [[Grid-TCOD-Integration]] - FOV and pathfinding - [[Grid-TCOD-Integration]] - FOV and pathfinding
**Key Files:** **Key Files:**
- `src/UIGrid.cpp::render()` - Main rendering orchestration - `src/UIGrid.cpp::render()` - Main rendering orchestration
- `src/GridLayers.cpp` - ColorLayer and TileLayer rendering - `src/GridLayers.cpp` - ColorLayer and TileLayer rendering
- `src/UIGrid.cpp::renderChunk()` - Per-chunk RenderTexture management - `src/UIGrid.cpp::renderChunk()` - Per-chunk RenderTexture management
--- ---
## Architecture Overview ## Architecture Overview
### Layer-Based Rendering ### Layer-Based Rendering
Grids render content through **layers** rather than per-cell properties. Each layer is either a `ColorLayer` (solid colors) or `TileLayer` (texture sprites). Grids render content through **layers** rather than per-cell properties. Each layer is either a `ColorLayer` (solid colors) or `TileLayer` (texture sprites).
**Render order is determined by z_index:** **Render order is determined by z_index:**
- Layers with `z_index < 0` render **below** entities - Layers with `z_index < 0` render **below** entities
- Entities render at the z=0 boundary - Entities render at the z=0 boundary
- Layers with `z_index >= 0` render **above** entities (overlays) - Layers with `z_index >= 0` render **above** entities (overlays)
Within each group, lower z_index values render first (behind higher values). Within each group, lower z_index values render first (behind higher values).
``` ```
z_index: -3 -2 -1 0 +1 +2 z_index: -3 -2 -1 0 +1 +2
| | | | | | | | | | | |
[background] [tiles] [ENTITIES] [fog] [UI overlay] [background] [tiles] [ENTITIES] [fog] [UI overlay]
``` ```
### Chunk-Based Caching ### Chunk-Based Caching
Large grids are divided into **chunks**. Each chunk maintains its own `sf::RenderTexture` that caches the rendered result. Large grids are divided into **chunks**. Each chunk maintains its own `sf::RenderTexture` that caches the rendered result.
**Key concepts:** **Key concepts:**
- Only chunks intersecting the viewport are considered for rendering - Only chunks intersecting the viewport are considered for rendering
- Each chunk tracks whether its content is "dirty" (needs redraw) - Each chunk tracks whether its content is "dirty" (needs redraw)
- Static content renders once, then the cached texture is reused - Static content renders once, then the cached texture is reused
### Dirty Flag Propagation ### Dirty Flag Propagation
When layer content changes, only affected chunks are marked dirty: When layer content changes, only affected chunks are marked dirty:
1. `layer.set((x, y), value)` marks the containing chunk as dirty 1. `layer.set((x, y), value)` marks the containing chunk as dirty
2. On next render, dirty chunks redraw to their RenderTexture 2. On next render, dirty chunks redraw to their RenderTexture
3. Clean chunks simply blit their cached texture 3. Clean chunks simply blit their cached texture
This means a 1000x1000 grid with one changing cell redraws only one chunk, not 1,000,000 cells. This means a 1000x1000 grid with one changing cell redraws only one chunk, not 1,000,000 cells.
--- ---
## Render Pipeline Stages ## Render Pipeline Stages
### Stage 1: Viewport Calculation ### Stage 1: Viewport Calculation
Calculate which chunks and cells are visible based on camera position, zoom, and grid dimensions. Calculate which chunks and cells are visible based on camera position, zoom, and grid dimensions.
**Key properties:** **Key properties:**
- `center` - Camera position in pixel coordinates (Vector) - `center` - Camera position in pixel coordinates (Vector)
- `zoom` - Scale factor (1.0 = normal, 2.0 = 2x magnification) - `zoom` - Scale factor (1.0 = normal, 2.0 = 2x magnification)
- `size` - Viewport dimensions in screen pixels - `size` - Viewport dimensions in screen pixels
### Stage 2: Below-Entity Layers ### Stage 2: Below-Entity Layers
For each layer with `z_index < 0`, sorted by z_index: For each layer with `z_index < 0`, sorted by z_index:
1. Determine which chunks intersect viewport 1. Determine which chunks intersect viewport
2. For each visible chunk: 2. For each visible chunk:
- If dirty: redraw layer content to chunk's RenderTexture - If dirty: redraw layer content to chunk's RenderTexture
- Draw chunk's cached texture to output - Draw chunk's cached texture to output
### Stage 3: Entity Rendering ### Stage 3: Entity Rendering
Entities render at the z=0 boundary: Entities render at the z=0 boundary:
1. Iterate entity collection 1. Iterate entity collection
2. Cull entities outside viewport bounds 2. Cull entities outside viewport bounds
3. Draw visible entity sprites at interpolated positions 3. Draw visible entity sprites at interpolated positions
### Stage 4: Above-Entity Layers ### Stage 4: Above-Entity Layers
For each layer with `z_index >= 0`, sorted by z_index: For each layer with `z_index >= 0`, sorted by z_index:
- Same chunk-based rendering as Stage 2 - Same chunk-based rendering as Stage 2
- These layers appear as overlays (fog, highlights, UI elements) - These layers appear as overlays (fog, highlights, UI elements)
### Stage 5: Final Compositing ### Stage 5: Final Compositing
All rendered content is drawn to the window. All rendered content is drawn to the window.
--- ---
## Creating and Managing Layers ## Creating and Managing Layers
### Standalone Layer Objects ### Standalone Layer Objects
Layers are created as standalone objects, then attached to grids: Layers are created as standalone objects, then attached to grids:
```python ```python
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600), layers=[]) grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600), layers=[])
# Create layers as standalone objects # Create layers as standalone objects
background = mcrfpy.ColorLayer(name="background", z_index=-2) background = mcrfpy.ColorLayer(name="background", z_index=-2)
grid.add_layer(background) grid.add_layer(background)
terrain = mcrfpy.TileLayer(name="terrain", z_index=-1, texture=tileset) terrain = mcrfpy.TileLayer(name="terrain", z_index=-1, texture=tileset)
grid.add_layer(terrain) grid.add_layer(terrain)
# Overlay above entities (z_index >= 0) # Overlay above entities (z_index >= 0)
fog = mcrfpy.ColorLayer(name="fog", z_index=1) fog = mcrfpy.ColorLayer(name="fog", z_index=1)
grid.add_layer(fog) grid.add_layer(fog)
``` ```
### Passing Layers at Construction ### Passing Layers at Construction
You can also pass layers during Grid construction: You can also pass layers during Grid construction:
```python ```python
bg = mcrfpy.ColorLayer(name="background", z_index=-2) bg = mcrfpy.ColorLayer(name="background", z_index=-2)
tiles = mcrfpy.TileLayer(name="terrain", z_index=-1, texture=tileset) tiles = mcrfpy.TileLayer(name="terrain", z_index=-1, texture=tileset)
fog = mcrfpy.ColorLayer(name="fog", z_index=1) fog = mcrfpy.ColorLayer(name="fog", z_index=1)
grid = mcrfpy.Grid( grid = mcrfpy.Grid(
grid_size=(50, 50), grid_size=(50, 50),
pos=(0, 0), pos=(0, 0),
size=(800, 600), size=(800, 600),
layers=[bg, tiles, fog] layers=[bg, tiles, fog]
) )
``` ```
### Accessing Layers ### Accessing Layers
```python ```python
# By name # By name
fog_layer = grid.layer("fog") fog_layer = grid.layer("fog")
# All layers (returns tuple) # All layers (returns tuple)
all_layers = grid.layers all_layers = grid.layers
print(f"Grid has {len(all_layers)} layers") print(f"Grid has {len(all_layers)} layers")
``` ```
### Removing Layers ### Removing Layers
```python ```python
grid.remove_layer(fog_layer) grid.remove_layer(fog_layer)
``` ```
--- ---
## Layer Operations ## Layer Operations
### ColorLayer ### ColorLayer
```python ```python
cl = mcrfpy.ColorLayer(name="highlights", z_index=1) cl = mcrfpy.ColorLayer(name="highlights", z_index=1)
grid.add_layer(cl) grid.add_layer(cl)
# Fill all cells with one color # Fill all cells with one color
cl.fill(mcrfpy.Color(0, 0, 0, 255)) cl.fill(mcrfpy.Color(0, 0, 0, 255))
# Set individual cell # Set individual cell
cl.set((5, 5), mcrfpy.Color(255, 0, 0, 128)) cl.set((5, 5), mcrfpy.Color(255, 0, 0, 128))
# Read cell value # Read cell value
color = cl.at((5, 5)) color = cl.at((5, 5))
print(f"Color: ({color.r}, {color.g}, {color.b}, {color.a})") print(f"Color: ({color.r}, {color.g}, {color.b}, {color.a})")
``` ```
### TileLayer ### TileLayer
```python ```python
tl = mcrfpy.TileLayer(name="terrain", z_index=-1, texture=tileset) tl = mcrfpy.TileLayer(name="terrain", z_index=-1, texture=tileset)
grid.add_layer(tl) grid.add_layer(tl)
# Set tile sprite index # Set tile sprite index
tl.set((5, 5), 42) tl.set((5, 5), 42)
# Read tile index # Read tile index
idx = tl.at((5, 5)) idx = tl.at((5, 5))
# Fill all cells # Fill all cells
tl.fill(0) # All floor tiles tl.fill(0) # All floor tiles
``` ```
### Layer Properties ### Layer Properties
```python ```python
layer = grid.layer("fog") layer = grid.layer("fog")
# Visibility toggle # Visibility toggle
layer.visible = False # Hide layer layer.visible = False # Hide layer
layer.visible = True # Show layer layer.visible = True # Show layer
# Z-index (render order) # Z-index (render order)
layer.z_index = 2 # Move to front layer.z_index = 2 # Move to front
# Grid dimensions (read-only) # Grid dimensions (read-only)
w, h = layer.grid_size w, h = layer.grid_size
``` ```
--- ---
## Performance Characteristics ## Performance Characteristics
**Static Grids:** **Static Grids:**
- Near-zero CPU cost after initial render - Near-zero CPU cost after initial render
- Cached chunk textures reused frame-to-frame - Cached chunk textures reused frame-to-frame
- Only viewport calculation and texture blitting - Only viewport calculation and texture blitting
**Dynamic Grids:** **Dynamic Grids:**
- Cost proportional to number of dirty chunks - Cost proportional to number of dirty chunks
- Single-cell changes affect only one chunk - Single-cell changes affect only one chunk
- Bulk operations should batch changes before render - Bulk operations should batch changes before render
**Large Grids:** **Large Grids:**
- 1000x1000+ grids render efficiently - 1000x1000+ grids render efficiently
- Only visible chunks processed - Only visible chunks processed
- Memory scales with grid size (chunk textures) - Memory scales with grid size (chunk textures)
--- ---
## FOV Overlay Pattern ## FOV Overlay Pattern
The most common overlay pattern is fog of war using a ColorLayer: The most common overlay pattern is fog of war using a ColorLayer:
```python ```python
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600), layers=[]) grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600), layers=[])
# Background and terrain layers (below entities) # Background and terrain layers (below entities)
bg = mcrfpy.ColorLayer(name="bg", z_index=-2) bg = mcrfpy.ColorLayer(name="bg", z_index=-2)
grid.add_layer(bg) grid.add_layer(bg)
bg.fill(mcrfpy.Color(20, 20, 30, 255)) bg.fill(mcrfpy.Color(20, 20, 30, 255))
# Fog overlay (above entities, z_index >= 0) # Fog overlay (above entities, z_index >= 0)
fog = mcrfpy.ColorLayer(name="fog", z_index=1) fog = mcrfpy.ColorLayer(name="fog", z_index=1)
grid.add_layer(fog) grid.add_layer(fog)
fog.fill(mcrfpy.Color(0, 0, 0, 255)) fog.fill(mcrfpy.Color(0, 0, 0, 255))
# Make cells traversable # Make cells traversable
for x in range(50): for x in range(50):
for y in range(50): for y in range(50):
grid.at(x, y).transparent = True grid.at(x, y).transparent = True
grid.at(x, y).walkable = True grid.at(x, y).walkable = True
# After computing FOV, update fog # After computing FOV, update fog
grid.compute_fov((25, 25), radius=10) grid.compute_fov((25, 25), radius=10)
w, h = grid.grid_size w, h = grid.grid_size
for x in range(w): for x in range(w):
for y in range(h): for y in range(h):
if grid.is_in_fov((x, y)): if grid.is_in_fov((x, y)):
fog.set((x, y), mcrfpy.Color(0, 0, 0, 0)) # Transparent fog.set((x, y), mcrfpy.Color(0, 0, 0, 0)) # Transparent
else: else:
fog.set((x, y), mcrfpy.Color(0, 0, 0, 192)) # Dimmed fog.set((x, y), mcrfpy.Color(0, 0, 0, 192)) # Dimmed
``` ```
--- ---
## Multiple Overlay Layers ## Multiple Overlay Layers
Pre-create multiple overlay layers and toggle between them: Pre-create multiple overlay layers and toggle between them:
```python ```python
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600), layers=[]) grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600), layers=[])
highlight = mcrfpy.ColorLayer(name="highlight", z_index=1) highlight = mcrfpy.ColorLayer(name="highlight", z_index=1)
danger = mcrfpy.ColorLayer(name="danger", z_index=2) danger = mcrfpy.ColorLayer(name="danger", z_index=2)
fog = mcrfpy.ColorLayer(name="fog", z_index=3) fog = mcrfpy.ColorLayer(name="fog", z_index=3)
grid.add_layer(highlight) grid.add_layer(highlight)
grid.add_layer(danger) grid.add_layer(danger)
grid.add_layer(fog) grid.add_layer(fog)
def show_danger_zones(): def show_danger_zones():
highlight.visible = False highlight.visible = False
danger.visible = True danger.visible = True
fog.visible = False fog.visible = False
def show_fog_of_war(): def show_fog_of_war():
highlight.visible = False highlight.visible = False
danger.visible = False danger.visible = False
fog.visible = True fog.visible = True
``` ```
--- ---
## Recreating Legacy Three-Layer Rendering ## Recreating Legacy Three-Layer Rendering
The old system rendered: background color, tile sprite, FOV overlay. Here's the modern equivalent: The old system rendered: background color, tile sprite, FOV overlay. Here's the modern equivalent:
```python ```python
grid = mcrfpy.Grid( grid = mcrfpy.Grid(
grid_size=(50, 50), grid_size=(50, 50),
pos=(100, 100), pos=(100, 100),
size=(400, 400), size=(400, 400),
layers=[] layers=[]
) )
# Layer 1: Background colors (z=-2, behind everything) # Layer 1: Background colors (z=-2, behind everything)
background = mcrfpy.ColorLayer(name="background", z_index=-2) background = mcrfpy.ColorLayer(name="background", z_index=-2)
grid.add_layer(background) grid.add_layer(background)
background.fill(mcrfpy.Color(20, 20, 30, 255)) background.fill(mcrfpy.Color(20, 20, 30, 255))
# Layer 2: Tile sprites (z=-1, above background, below entities) # Layer 2: Tile sprites (z=-1, above background, below entities)
tiles = mcrfpy.TileLayer(name="tiles", z_index=-1, texture=tileset) tiles = mcrfpy.TileLayer(name="tiles", z_index=-1, texture=tileset)
grid.add_layer(tiles) grid.add_layer(tiles)
# Layer 3: FOV overlay (z=+1, above entities) # Layer 3: FOV overlay (z=+1, above entities)
fog = mcrfpy.ColorLayer(name="fog", z_index=1) fog = mcrfpy.ColorLayer(name="fog", z_index=1)
grid.add_layer(fog) grid.add_layer(fog)
# Populate layers # Populate layers
for x in range(50): for x in range(50):
for y in range(50): for y in range(50):
tiles.set((x, y), calculate_tile_index(x, y)) tiles.set((x, y), calculate_tile_index(x, y))
``` ```
--- ---
## Common Issues ## Common Issues
### Issue: Layer Changes Don't Appear ### Issue: Layer Changes Don't Appear
**Cause:** Use layer methods (`set()`, `fill()`) which automatically mark chunks dirty. **Cause:** Use layer methods (`set()`, `fill()`) which automatically mark chunks dirty.
### Issue: Overlay Appears Behind Entities ### Issue: Overlay Appears Behind Entities
**Cause:** Layer z_index is negative. **Cause:** Layer z_index is negative.
**Fix:** Use `z_index >= 0` for overlays: **Fix:** Use `z_index >= 0` for overlays:
```python ```python
overlay = mcrfpy.ColorLayer(name="overlay", z_index=1) # Not z_index=-1 overlay = mcrfpy.ColorLayer(name="overlay", z_index=1) # Not z_index=-1
``` ```
### Issue: Performance Degrades with Many Changes ### Issue: Performance Degrades with Many Changes
**Cause:** Each `set()` call marks a chunk dirty; scattered changes mean many chunk redraws. **Cause:** Each `set()` call marks a chunk dirty; scattered changes mean many chunk redraws.
**Fix:** For bulk updates, use `fill()` for uniform values: **Fix:** For bulk updates, use `fill()` for uniform values:
```python ```python
# Single operation - efficient # Single operation - efficient
layer.fill(mcrfpy.Color(0, 0, 0, 0)) layer.fill(mcrfpy.Color(0, 0, 0, 0))
# Many individual calls - slower but necessary for non-uniform data # Many individual calls - slower but necessary for non-uniform data
for x in range(100): for x in range(100):
for y in range(100): for y in range(100):
layer.set((x, y), compute_value(x, y)) layer.set((x, y), compute_value(x, y))
``` ```
--- ---
## API Quick Reference ## API Quick Reference
**Grid Properties:** **Grid Properties:**
- `center` - Camera position (Vector, pixels in grid space) - `center` - Camera position (Vector, pixels in grid space)
- `zoom` - Scale factor (float) - `zoom` - Scale factor (float)
- `layers` - All layers (tuple, sorted by z_index) - `layers` - All layers (tuple, sorted by z_index)
- `fill_color` - Grid background color (behind all layers) - `fill_color` - Grid background color (behind all layers)
- `grid_size` - Grid dimensions (tuple) - `grid_size` - Grid dimensions (tuple)
**Grid Methods:** **Grid Methods:**
- `add_layer(layer)` - Attach a layer object - `add_layer(layer)` - Attach a layer object
- `remove_layer(layer)` - Detach a layer - `remove_layer(layer)` - Detach a layer
- `layer("name")` - Get layer by name - `layer("name")` - Get layer by name
**Layer Construction:** **Layer Construction:**
- `mcrfpy.ColorLayer(name="...", z_index=N)` - Color overlay - `mcrfpy.ColorLayer(name="...", z_index=N)` - Color overlay
- `mcrfpy.TileLayer(name="...", z_index=N, texture=tex)` - Tile sprites - `mcrfpy.TileLayer(name="...", z_index=N, texture=tex)` - Tile sprites
**Layer Properties:** **Layer Properties:**
- `z_index` - Render order (int) - `z_index` - Render order (int)
- `visible` - Show/hide (bool) - `visible` - Show/hide (bool)
- `grid_size` - Dimensions (tuple, read-only) - `grid_size` - Dimensions (tuple, read-only)
**Layer Methods:** **Layer Methods:**
- `set((x, y), value)` - Set cell (Color or int) - `set((x, y), value)` - Set cell (Color or int)
- `at((x, y))` - Get cell value - `at((x, y))` - Get cell value
- `fill(value)` - Fill all cells - `fill(value)` - Fill all cells
--- ---
**Navigation:** **Navigation:**
- [[Grid-System]] - Parent page, layer concepts - [[Grid-System]] - Parent page, layer concepts
- [[Grid-TCOD-Integration]] - FOV computation details - [[Grid-TCOD-Integration]] - FOV computation details
- [[Performance-and-Profiling]] - Metrics and optimization - [[Performance-and-Profiling]] - Metrics and optimization
- [[Entity-Management]] - Entity rendering - [[Entity-Management]] - Entity rendering