Update Grid-Rendering-Pipeline wiki: standalone layer objects, tuple API, current grid_size/compute_fov/is_in_fov signatures

John McCardle 2026-02-07 22:33:32 +00:00
commit e1d3504bf5
2 changed files with 390 additions and 374 deletions

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

@ -1,374 +0,0 @@
# Grid Rendering Pipeline
## 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.
**Parent Page:** [[Grid-System]]
**Related Pages:**
- [[Performance-and-Profiling]] - Metrics and profiling tools
- [[Entity-Management]] - Entity rendering within grids
- [[Grid-TCOD-Integration]] - FOV and pathfinding (partially in transition)
**Key Files:**
- `src/UIGrid.cpp::render()` - Main rendering orchestration
- `src/GridLayers.cpp` - ColorLayer and TileLayer rendering
- `src/UIGrid.cpp::renderChunk()` - Per-chunk RenderTexture management
**Related Issues:**
- [#123](../issues/123) - Chunk-based Grid Rendering (Closed - Implemented)
- [#148](../issues/148) - Dirty Flag RenderTexture Caching (Closed - Implemented)
- [#147](../issues/147) - Dynamic Layer System (Closed - Implemented)
- [#113](../issues/113) - Batch Operations API (Open - includes FOV access discussion)
- [#115](../issues/115) - SpatialHash for entity culling (Open)
---
## Architecture Overview
### 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).
**Render order is determined by z_index:**
- Layers with `z_index < 0` render **below** entities
- Entities render at the z=0 boundary
- Layers with `z_index >= 0` render **above** entities (overlays)
Within each group, lower z_index values render first (behind higher values).
```
z_index: -3 -2 -1 0 +1 +2
↓ ↓ ↓ ↓ ↓ ↓
[background] [tiles] [ENTITIES] [fog] [UI overlay]
```
**Implementation:** See `UIGrid::render()` which iterates layers in z_index order, inserting entity rendering at the 0 boundary.
### Chunk-Based Caching
Large grids are divided into **chunks** (regions of cells). Each chunk maintains its own `sf::RenderTexture` that caches the rendered result.
**Key concepts:**
- Chunk size is implementation-defined (currently ~256 cells per dimension)
- Only chunks intersecting the viewport are considered for rendering
- Each chunk tracks whether its content is "dirty" (needs redraw)
- Static content renders once, then the cached texture is reused
**Implementation:** See `UIGrid::renderChunk()` for chunk texture management.
### Dirty Flag Propagation
When layer content changes, only affected chunks are marked dirty:
1. `layer.set(x, y, value)` marks the containing chunk as dirty
2. On next render, dirty chunks redraw to their RenderTexture
3. Clean chunks simply blit their cached texture
This means a 1000x1000 grid with one changing cell redraws only ~1 chunk, not 1,000,000 cells.
**Implementation:** Dirty flags propagate through `UIGrid::markDirty()` and are checked in `UIGrid::render()`.
---
## Render Pipeline Stages
### Stage 1: Viewport Calculation
Calculate which chunks and cells are visible based on camera position, zoom, and grid dimensions.
**Key properties:**
- `center` / `center_x`, `center_y` - Camera position in pixel coordinates (within grid space)
- `zoom` - Scale factor (1.0 = normal, 2.0 = 2x magnification)
- `size` - Viewport dimensions in screen pixels
**Implementation:** Viewport bounds calculated at start of `UIGrid::render()`.
### Stage 2: Below-Entity Layers
For each layer with `z_index < 0`, sorted by z_index:
1. Determine which chunks intersect viewport
2. For each visible chunk:
- If dirty: redraw layer content to chunk's RenderTexture
- Draw chunk's cached texture to output
### Stage 3: Entity Rendering
Entities render at the z=0 boundary:
1. Iterate entity collection
2. Cull entities outside viewport bounds
3. Draw visible entity sprites at interpolated positions
**Note:** Entity culling is currently O(n). SpatialHash optimization planned in [#115](../issues/115).
### Stage 4: Above-Entity Layers
For each layer with `z_index >= 0`, sorted by z_index:
- Same chunk-based rendering as Stage 2
- These layers appear as overlays (fog, highlights, UI elements)
### Stage 5: Final Compositing
All rendered content exists in the grid's output RenderTexture, which is drawn to the window in a single operation.
---
## Performance Characteristics
**Static Grids:**
- Near-zero CPU cost after initial render
- Cached chunk textures reused frame-to-frame
- Only viewport calculation and texture blitting
**Dynamic Grids:**
- Cost proportional to number of dirty chunks
- Single-cell changes affect only one chunk
- Bulk operations should batch changes before render
**Large Grids:**
- 1000x1000+ grids render efficiently
- Only visible chunks processed
- Memory scales with grid size (chunk textures)
**Profiling:** Use `mcrfpy.getMetrics()` or the F3 overlay to see render times. See [[Performance-and-Profiling]].
---
## FOV and Perspective System (In Transition)
**Current Status:** The FOV computation (`compute_fov()`, `is_in_fov()`) works correctly for pathfinding and visibility queries. However, the automatic fog-of-war overlay system is **not currently connected** to the new layer architecture.
**What works:**
- `grid.compute_fov(x, y, radius)` - Computes which cells are visible
- `grid.is_in_fov(x, y)` - Queries visibility of a specific cell
- Pathfinding uses walkable/transparent properties correctly
**What's in transition:**
- Per-entity perspective (`UIGridPointState`) not yet exposed to Python
- Automatic fog overlay rendering disconnected from layer system
- Batch FOV data access being designed ([#113 discussion](../issues/113))
**Current workaround - Manual fog layer:**
```python
# Create a fog overlay layer (positive z_index = above entities)
fog_layer = grid.add_layer("color", z_index=1)
# After computing FOV, update fog colors
grid.compute_fov(player.grid_x, player.grid_y, radius=10)
for x in range(grid.grid_x):
for y in range(grid.grid_y):
if not grid.is_in_fov(x, y):
# Dim color for non-visible cells
fog_layer.set(x, y, mcrfpy.Color(0, 0, 0, 192))
else:
# Transparent for visible cells
fog_layer.set(x, y, mcrfpy.Color(0, 0, 0, 0))
```
**Limitation:** This O(n²) iteration is inefficient for large grids. Batch operations ([#113](../issues/113)) will address this with patterns like `cells_in_radius()` iterators.
---
## Layer Techniques
### Multiple Overlay Layers
You can pre-create multiple overlay layers and toggle between them:
```python
# Create several overlay options
highlight_layer = grid.add_layer("color", z_index=1)
danger_layer = grid.add_layer("color", z_index=2)
fog_layer = grid.add_layer("color", z_index=3)
# Populate each with different data...
# highlight_layer shows selected cells
# danger_layer shows enemy threat zones
# fog_layer shows visibility
# Toggle visibility to switch which overlay shows
def show_danger_zones():
highlight_layer.visible = False
danger_layer.visible = True
fog_layer.visible = False
def show_fog_of_war():
highlight_layer.visible = False
danger_layer.visible = False
fog_layer.visible = True
```
### Z-Index Reordering
Change layer order at runtime by modifying z_index:
```python
# Bring a layer to front
important_layer.z_index = 100
# Send a layer behind entities
background_layer.z_index = -10
```
**Note:** Changing z_index marks the layer dirty, triggering re-render of affected chunks.
### Constructor `layers={}` Limitations
The `layers={}` constructor argument is convenient but limited:
```python
# This creates layers, but ALL get negative z_index (below entities)
grid = mcrfpy.Grid(
grid_size=(50, 50),
layers={"ground": "color", "terrain": "tile", "overlay": "color"}
)
# Result: ground=-3, terrain=-2, overlay=-1 (all below entities!)
```
**For overlays above entities, use `add_layer()` explicitly:**
```python
grid = mcrfpy.Grid(grid_size=(50, 50), layers={})
ground = grid.add_layer("color", z_index=-2)
terrain = grid.add_layer("tile", z_index=-1)
overlay = grid.add_layer("color", z_index=1) # Above entities!
```
---
## Migration from Legacy API
Prior to the layer system, grids had built-in per-cell rendering via `UIGridPoint` properties. Here's how to recreate that behavior:
### Legacy Pattern (Pre-November 2025)
```python
# OLD API (no longer works):
grid = mcrfpy.Grid(50, 50, 16, 16)
cell = grid.at(x, y)
cell.tilesprite = 42 # Sprite index
cell.color = (255, 0, 0) # Background color
```
### Equivalent Modern Pattern
```python
# NEW API - explicit layers:
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(400, 400))
# Grid creates a default TileLayer; access it:
tile_layer = grid.layers[0]
tile_layer.set(x, y, 42) # Sprite index
# For background colors, add a ColorLayer behind tiles:
color_layer = grid.add_layer("color", z_index=-2) # Behind default tile layer
color_layer.set(x, y, mcrfpy.Color(255, 0, 0))
```
### Recreating Old Three-Layer Rendering
The legacy system rendered: background color → tile sprite → FOV overlay.
```python
# Create grid with no default layers
grid = mcrfpy.Grid(
grid_size=(50, 50),
pos=(100, 100),
size=(400, 400),
texture=tileset,
layers={}
)
# Layer 1: Background colors (z=-2, behind everything)
background = grid.add_layer("color", z_index=-2)
# Layer 2: Tile sprites (z=-1, above background, below entities)
tiles = grid.add_layer("tile", z_index=-1, texture=tileset)
# Layer 3: FOV overlay (z=+1, above entities)
fog = grid.add_layer("color", z_index=1)
# Now populate layers as needed
background.fill(mcrfpy.Color(20, 20, 30)) # Dark blue background
for x in range(50):
for y in range(50):
tiles.set(x, y, calculate_tile_index(x, y))
# FOV overlay updated after compute_fov() calls
```
---
## Common Issues
### Issue: Layer Changes Don't Appear
**Cause:** Layer content changed but chunk not marked dirty.
**Fix:** Use layer methods (`set()`, `fill()`) which automatically mark dirty. Direct property manipulation may bypass dirty flagging.
### Issue: Overlay Appears Behind Entities
**Cause:** Layer z_index is negative.
**Fix:** Use `z_index >= 0` for overlays:
```python
overlay = grid.add_layer("color", z_index=1) # Not z_index=-1
```
### Issue: Performance Degrades with Many Changes
**Cause:** Each `set()` call can mark a chunk dirty; many scattered changes = many chunk redraws.
**Fix:** Batch logically-related changes. For bulk updates, consider:
```python
# Less efficient: 10,000 individual calls
for x in range(100):
for y in range(100):
layer.set(x, y, value)
# More efficient when available: bulk fill
layer.fill(mcrfpy.Color(0, 0, 0)) # Single operation
```
Batch operations API ([#113](../issues/113)) will provide more patterns.
---
## API Quick Reference
**Grid Properties:**
- `center`, `center_x`, `center_y` - Camera position (pixels in grid space)
- `zoom` - Scale factor
- `layers` - List of layer objects, sorted by z_index
- `fill_color` - Grid background (behind all layers)
**Grid Methods:**
- `add_layer(type, z_index, texture)` - Create new layer
- `remove_layer(layer)` - Remove a layer
**Layer Properties:**
- `z_index` - Render order
- `visible` - Show/hide layer
- `grid_size` - Dimensions (read-only)
**Layer Methods:**
- `set(x, y, value)` - Set cell (color or sprite index)
- `at(x, y)` - Get cell value
- `fill(value)` - Fill all cells
---
**Navigation:**
- [[Grid-System]] - Parent page, layer concepts
- [[Grid-TCOD-Integration]] - FOV computation details
- [[Performance-and-Profiling]] - Metrics and optimization
- [[Entity-Management]] - Entity rendering
---
*Last updated: 2025-11-29*