Table of Contents
- Grid Rendering Pipeline
- Overview
- Architecture Overview
- Render Pipeline Stages
- Stage 1: Viewport Calculation
- Stage 2: Below-Entity Layers
- Stage 3: Entity Rendering
- Stage 4: Above-Entity Layers
- Stage 5: Final Compositing
- Creating and Managing Layers
- Layer Operations
- Performance Characteristics
- FOV Overlay Pattern
- Multiple Overlay Layers
- Recreating Legacy Three-Layer Rendering
- Common Issues
- Issue: Layer Changes Don't Appear
- Issue: Overlay Appears Behind Entities
- Issue: Performance Degrades with Many Changes
- API Quick Reference
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 orchestrationsrc/GridLayers.cpp- ColorLayer and TileLayer renderingsrc/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 < 0render below entities - Entities render at the z=0 boundary
- Layers with
z_index >= 0render 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:
layer.set((x, y), value)marks the containing chunk as dirty- On next render, dirty chunks redraw to their RenderTexture
- 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:
- Determine which chunks intersect viewport
- 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:
- Iterate entity collection
- Cull entities outside viewport bounds
- 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:
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:
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
# 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
grid.remove_layer(fog_layer)
Layer Operations
ColorLayer
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
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
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:
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:
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:
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:
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:
# 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 objectremove_layer(layer)- Detach a layerlayer("name")- Get layer by name
Layer Construction:
mcrfpy.ColorLayer(name="...", z_index=N)- Color overlaymcrfpy.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 valuefill(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