diff --git a/Grid-Rendering-Pipeline.-.md b/Grid-Rendering-Pipeline.-.md new file mode 100644 index 0000000..f8c1408 --- /dev/null +++ b/Grid-Rendering-Pipeline.-.md @@ -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 \ No newline at end of file diff --git a/Grid-Rendering-Pipeline.md b/Grid-Rendering-Pipeline.md deleted file mode 100644 index 2b8d72f..0000000 --- a/Grid-Rendering-Pipeline.md +++ /dev/null @@ -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* \ No newline at end of file