Add "Grid-Rendering-Pipeline"

John McCardle 2025-10-25 22:48:05 +00:00
parent dc09e36937
commit 6f6fa5e6a1
1 changed files with 519 additions and 0 deletions

@ -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