diff --git a/Grid-Rendering-Pipeline.-.md b/Grid-Rendering-Pipeline.-.md new file mode 100644 index 0000000..59c4e71 --- /dev/null +++ b/Grid-Rendering-Pipeline.-.md @@ -0,0 +1,519 @@ +# Grid Rendering Pipeline + +## Overview + +The Grid rendering pipeline is McRogueFace's most complex rendering subsystem. It handles viewport culling, zoom, entity rendering, and FOV overlays - all within a single `sf::RenderTexture` for optimal performance. + +**Parent Page:** [[Grid-System]] + +**Related Pages:** +- [[Performance-and-Profiling]] - ScopedTimer instrumentation points +- [[Entity-Management]] - Entity rendering within grids +- [[UI-Component-Hierarchy]] - Where Grid fits in render hierarchy + +**Key Files:** +- `src/UIGrid.cpp::render()` - Main rendering entry point (lines 97-310) +- `src/GameEngine.cpp` - ProfilingMetrics struct for tracking render times +- `src/Profiler.h` - ScopedTimer RAII helper + +**Related Issues:** +- [#116](../../issues/116) - Dirty flag system (avoid re-rendering static content) +- [#115](../../issues/115) - SpatialHash for entity culling optimization +- [#113](../../issues/113) - Batch operations API + +--- + +## Render Pipeline Stages + +The pipeline executes in **4 stages** during each frame: + +``` +1. Viewport Calculation (culling bounds) + ↓ +2. Base Layer Rendering (tiles + background colors) + ↓ +3. Entity Layer Rendering (visible entities) + ↓ +4. FOV Overlay Rendering (discovered/visible states) +``` + +All rendering happens into an `sf::RenderTexture`, then the final texture is drawn to the window in a single operation. + +--- + +## Stage 1: Viewport Calculation + +### Purpose +Calculate which cells are visible based on camera position, zoom, and grid size. + +### Implementation +```cpp +// Get cell dimensions from texture +int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH; +int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT; + +// Camera center in grid coordinates +float center_x_sq = center_x / cell_width; +float center_y_sq = center_y / cell_height; + +// Viewport size in cells +float width_sq = box.getSize().x / (cell_width * zoom); +float height_sq = box.getSize().y / (cell_height * zoom); + +// Visible cell range +float left_edge = center_x_sq - (width_sq / 2.0); +float top_edge = center_y_sq - (height_sq / 2.0); +int x_limit = left_edge + width_sq + 2; // +2 for margin +int y_limit = top_edge + height_sq + 2; +``` + +**Key Insight:** The +2 margin ensures partially-visible cells at viewport edges are rendered completely. + +### Camera Properties + +- `center_x`, `center_y` - Camera position in pixel coordinates +- `zoom` - Scale factor (1.0 = normal, 2.0 = 2x zoom) +- `box.getSize()` - Viewport dimensions in pixels + +### Python API + +```python +grid.center_x = player.x * cell_width # Follow player +grid.center_y = player.y * cell_height +grid.zoom = 2.0 # Zoom in +``` + +--- + +## Stage 2: Base Layer Rendering + +### Purpose +Render the "ground" layer: background colors and tile sprites. + +### Loop Structure +```cpp +// Profile this stage +ScopedTimer gridTimer(Resources::game->metrics.gridRenderTime); + +int cellsRendered = 0; +for (int x = left_edge - 1; x < x_limit; x++) { + for (int y = top_edge - 1; y < y_limit; y++) { + // Calculate pixel position within renderTexture + auto pixel_pos = sf::Vector2f( + (x * cell_width - left_spritepixels) * zoom, + (y * cell_height - top_spritepixels) * zoom + ); + + auto gridpoint = at(x, y); + + // 1. Draw background color + r.setPosition(pixel_pos); + r.setFillColor(gridpoint.color); + renderTexture.draw(r); + + // 2. Draw tile sprite (if texture exists and tilesprite set) + if (ptex && gridpoint.tilesprite != -1) { + sprite = ptex->sprite(gridpoint.tilesprite, pixel_pos, + sf::Vector2f(zoom, zoom)); + renderTexture.draw(sprite); + } + + cellsRendered++; + } +} + +// Record metrics for profiling overlay +Resources::game->metrics.gridCellsRendered += cellsRendered; +``` + +### Per-Cell Data + +Each `UIGridPoint` has: +- `color` - RGBA background color (drawn first) +- `tilesprite` - Sprite index from texture sheet (-1 = no sprite) +- `walkable` - Affects pathfinding (not used in rendering) + +### Performance Characteristics + +**Current Performance:** +- 100x100 grid, ~1000 visible cells +- Grid render time: 8-12ms (without dirty flags) +- Target: <2ms after dirty flag optimization (#116) + +**Bottlenecks:** +- Individual `renderTexture.draw()` calls per cell +- No caching of static content +- Python/C++ boundary crossings for `at(x, y)` calls + +--- + +## Stage 3: Entity Layer Rendering + +### Purpose +Render entities that exist on the grid at their interpolated positions. + +### Loop Structure +```cpp +{ + ScopedTimer entityTimer(Resources::game->metrics.entityRenderTime); + int entitiesRendered = 0; + int totalEntities = entities->size(); + + for (auto e : *entities) { + // Viewport culling: skip out-of-bounds entities + if (e->position.x < left_edge - 1 || + e->position.x >= left_edge + width_sq + 1 || + e->position.y < top_edge - 1 || + e->position.y >= top_edge + height_sq + 1) { + continue; + } + + // Get entity's sprite + auto& drawent = e->sprite; + drawent.setScale(sf::Vector2f(zoom, zoom)); + + // Calculate pixel position (supports fractional positions) + auto pixel_pos = sf::Vector2f( + (e->position.x * cell_width - left_spritepixels) * zoom, + (e->position.y * cell_height - top_spritepixels) * zoom + ); + + // Render entity sprite + drawent.render(pixel_pos, renderTexture); + + entitiesRendered++; + } + + // Record metrics + Resources::game->metrics.entitiesRendered += entitiesRendered; + Resources::game->metrics.totalEntities += totalEntities; +} +``` + +### Entity Positions + +Entities have **floating-point positions** (`entity.position.x`, `entity.position.y`), allowing smooth sub-cell movement: + +```python +# Entity at fractional position +entity.x = 10.5 # Halfway between cells 10 and 11 +entity.y = 20.75 +``` + +### Performance Considerations + +**Current Performance:** +- 50 entities: ~2-3ms render time +- 500 entities: ~20-30ms (all visible) +- With culling: only visible entities rendered + +**Optimization Opportunities:** +- **#115**: SpatialHash for O(1) visibility queries +- **#117**: Memory pool to reduce allocation overhead +- Sort by Z-order before rendering (not yet implemented) + +--- + +## Stage 4: FOV Overlay Rendering + +### Purpose +If `perspective_enabled` is true, overlay cells based on entity's discovered/visible state. + +### Three Visibility States + +1. **Visible** - Entity can currently see this cell → No overlay +2. **Discovered but not visible** - Entity has seen this cell before → Dark gray overlay (RGBA: 32, 32, 40, 192) +3. **Undiscovered** - Entity has never seen this cell → Black overlay (RGBA: 0, 0, 0, 255) + +### Loop Structure +```cpp +if (perspective_enabled) { + ScopedTimer fovTimer(Resources::game->metrics.fovOverlayTime); + auto entity = perspective_entity.lock(); // weak_ptr to entity + + sf::RectangleShape overlay; + overlay.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom)); + + if (entity) { + // Valid entity - use its gridstate + for (int x = left_edge - 1; x < x_limit; x++) { + for (int y = top_edge - 1; y < y_limit; y++) { + int idx = y * grid_x + x; + const auto& state = entity->gridstate[idx]; + + overlay.setPosition(pixel_pos); + + if (!state.discovered) { + // Black - never seen + overlay.setFillColor(sf::Color(0, 0, 0, 255)); + renderTexture.draw(overlay); + } else if (!state.visible) { + // Dark gray - seen before, not currently visible + overlay.setFillColor(sf::Color(32, 32, 40, 192)); + renderTexture.draw(overlay); + } + // else: visible and discovered - no overlay + } + } + } else { + // Entity destroyed/invalid - all cells black + // ... render full black overlays ... + } +} +// else: omniscient view (perspective_enabled = false) - no overlays +``` + +### Perspective System + +See [[Grid-TCOD-Integration]] for details on how `compute_fov()` updates entity gridstate. + +**Python API:** +```python +# Enable perspective view from entity's POV +grid.set_perspective(player_entity) + +# Disable perspective (omniscient view) +grid.clear_perspective() + +# Check if perspective enabled +if grid.perspective_enabled: + print("Using entity perspective") +``` + +--- + +## Final Output + +### RenderTexture to Window + +After all 4 stages complete: + +```cpp +renderTexture.display(); // Finalize texture + +// output sprite uses the renderTexture +output.setTextureRect(sf::IntRect(0, 0, box.getSize().x, box.getSize().y)); +output.setPosition(box.getPosition() + offset); + +// Single draw call to window +target.draw(output); +``` + +**Performance Benefit:** Entire grid (1000s of cells + entities + overlays) rendered to window in **one draw call**. + +--- + +## Profiling Integration + +The rendering pipeline is instrumented with `ScopedTimer` for F3 overlay: + +```cpp +void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) { + ScopedTimer gridTimer(Resources::game->metrics.gridRenderTime); + + // Stage 2: Base layer + // ... rendering ... + + // Stage 3: Entities + { + ScopedTimer entityTimer(Resources::game->metrics.entityRenderTime); + // ... entity rendering ... + } + + // Stage 4: FOV overlay + if (perspective_enabled) { + ScopedTimer fovTimer(Resources::game->metrics.fovOverlayTime); + // ... overlay rendering ... + } +} +``` + +Press **F3** in-game to see: +- Grid render time +- Entity render time +- FOV overlay time +- Cells/entities rendered + +See [[Performance-and-Profiling]] for details on using the profiler. + +--- + +## Performance Optimization Workflow + +### Current Bottlenecks (October 2025) + +1. **Grid re-renders every frame** even if static + - **Solution**: #116 (Dirty flag system) + - Expected improvement: 8-12ms → <2ms for static content + +2. **Entity culling is O(n)** + - **Solution**: #115 (SpatialHash) + - Expected improvement: 10,000 entities with 50 visible → 1-2ms + +3. **Individual draw calls per cell** + - **Solution**: Vertex arrays or instanced rendering + - Expected improvement: 50% reduction in draw call overhead + +### Profiling a Grid Performance Issue + +Example workflow using [[Performance-Optimization-Workflow]]: + +```python +# 1. Create benchmark scene +import mcrfpy + +mcrfpy.createScene("test") +grid = mcrfpy.Grid(grid_size=(100, 100), pos=(0, 0), size=(1024, 768)) + +# Fill grid with varied content +for x in range(100): + for y in range(100): + cell = grid.at((x, y)) + cell.tilesprite = (x + y) % 10 + cell.color = (50 + x, 50 + y, 100, 255) + +ui = mcrfpy.sceneUI("test") +ui.append(grid) +mcrfpy.setScene("test") + +# 2. Press F3 to see profiler overlay +# 3. Check metrics: +# - Grid render time: X ms +# - Cells rendered: Y +# - FPS: Z +``` + +**Interpreting Results:** +- Grid render time > 10ms → Consider dirty flags +- Entities rendered << total entities → Culling is working +- FOV overlay time > 5ms → Large visible area or complex state + +--- + +## Common Issues + +### Issue: Grid Renders Off-Center + +**Cause:** `center_x`/`center_y` values incorrect relative to camera follow logic. + +**Fix:** +```python +# Ensure center is in pixel coordinates, not cell coordinates +grid.center_x = player.x * cell_width +grid.center_y = player.y * cell_height +``` + +### Issue: Entities Flicker at Viewport Edge + +**Cause:** Culling margin too small (entities disappear before fully off-screen). + +**Fix:** The -1/+1 margins in culling logic should handle this, but if needed: +```cpp +// Increase margin from 1 to 2 +if (e->position.x < left_edge - 2 || e->position.x >= left_edge + width_sq + 2) +``` + +### Issue: Zoom Breaks Entity Positions + +**Cause:** `zoom` not applied consistently to entity pixel positions. + +**Fix:** Ensure both entity position calculation AND sprite scale use zoom: +```cpp +auto pixel_pos = sf::Vector2f( + (e->position.x * cell_width - left_spritepixels) * zoom, + (e->position.y * cell_height - top_spritepixels) * zoom +); +drawent.setScale(sf::Vector2f(zoom, zoom)); +``` + +### Issue: FOV Overlay Has Gaps + +**Cause:** `gridstate` not updated after `compute_fov()`. + +**Fix:** See [[Grid-TCOD-Integration]] for proper FOV computation sequence. + +--- + +## Future Improvements + +### Dirty Flag System (#116) + +Only re-render cells that changed: + +```cpp +// Proposed implementation +bool grid_dirty = true; // Set when cells change + +void UIGrid::render() { + if (!grid_dirty && cachedTexture.valid()) { + // Use cached texture + target.draw(cachedOutput); + return; + } + + // Full re-render + // ... existing pipeline ... + + grid_dirty = false; +} +``` + +**Benefits:** +- Static grids: 8-12ms → <1ms (just copy cached texture) +- Animated grids: Only re-render changed cells + +### Batch Cell Updates (#113) + +Expose NumPy-style batch operations: + +```python +# Current: O(n) Python/C++ crossings +for x in range(100): + for y in range(100): + grid.at((x, y)).color = (255, 0, 0, 255) + +# Proposed: O(1) crossings +grid.set_colors(colors_array) # NumPy array or Python list +``` + +### SpatialHash Entity Culling (#115) + +Replace linear entity scan with spatial queries: + +```cpp +// Current: O(n) +for (auto e : *entities) { + if (in_viewport(e)) render(e); +} + +// Proposed: O(visible) +auto visible_entities = spatial_hash.query(viewport_bounds); +for (auto e : visible_entities) { + render(e); +} +``` + +--- + +## API Reference + +See [`docs/api_reference_dynamic.html`](../../src/branch/master/docs/api_reference_dynamic.html) for complete Grid API. + +**Key Properties:** +- `grid.center_x`, `grid.center_y` - Camera position (pixels) +- `grid.zoom` - Zoom level (float, default 1.0) +- `grid.perspective_enabled` - Read-only, check if FOV overlay active + +**Key Methods:** +- `grid.set_perspective(entity)` - Enable FOV from entity's POV +- `grid.clear_perspective()` - Disable FOV overlay +- `grid.at((x, y))` - Get cell for modification + +--- + +**Navigation:** +- [[Grid-System]] - Parent page +- [[Grid-TCOD-Integration]] - FOV computation details +- [[Grid-Entity-Lifecycle]] - How entities interact with rendering +- [[Performance-and-Profiling]] - Using ScopedTimer and F3 overlay +- [[Entity-Management]] - Entity rendering specifics