nigig-org/PHASE3_RENDERGRAPH_SUMMARY.md
andodeki 0d43d61049 refactor(map): implement trait-based render graph (Phase 3)
- Define RenderPass trait with execute(), z_order(), zoom gating
- Implement 5 concrete passes: Background, Fill, Stroke, Poi, Label
- Create RenderContext to pass state to passes
- Refactor view.rs draw_walk() to use render_graph.execute()
- Reduce draw_walk() from 153 lines to 50 lines (-67%)
- Remove all hardcoded if-statements for pass execution
- Add 13 integration tests for render graph
- Enable true extensibility: new passes require only trait implementation

Benefits:
- Extensible: add custom passes without modifying view.rs
- Maintainable: each pass is self-contained
- Testable: passes can be tested independently
- Clean: view.rs reduced by 150 lines

Architecture:
  NigigMapView.draw_walk()
    -> creates RenderContext
    -> render_graph.execute(&mut ctx)
       -> iterates passes in z_order
       -> calls pass.execute(ctx) for each enabled pass

This completes Phase 3 of the Makepad codebase improvement plan.
2026-07-27 16:17:51 +00:00

287 lines
10 KiB
Markdown

# Phase 3: True Render Graph - Complete
## Summary
Successfully refactored the map renderer from hardcoded if-statements to a trait-based render graph system. This enables true extensibility without modifying `view.rs`.
## Changes Made
### 1. Created Trait-Based Render Pass System
**File: `crates/apps/map/src/render_graph.rs`**
- Defined `RenderPass` trait with methods:
- `pass_type()` - Returns the pass type
- `z_order()` - Returns draw order (lower = drawn first)
- `is_enabled()` - Whether pass is active
- `min_zoom()` / `max_zoom()` - Zoom range gating
- `should_execute(zoom)` - Combined check
- `execute(ctx)` - Actual rendering logic
- Implemented 5 concrete pass structs:
- `BackgroundPass` (z_order: 0)
- `FillPass` (z_order: 100)
- `StrokePass` (z_order: 200)
- `PoiPass` (z_order: 300, min_zoom: 13.0)
- `LabelPass` (z_order: 400, min_zoom: 13.0)
- Created `RenderContext` struct to pass all necessary state to passes:
- `cx: &mut Cx2d` - Makepad drawing context
- `cache: &TileCache` - Tile cache
- `draw_tiles: &[TileKey]` - Tiles to draw
- `entries: &[(TileKey, &TileEntry, f32)]` - Pre-fetched entries with scales
- `view_zoom: f64` - Current zoom level
- `map_offset: Vec2f` - Map offset for rendering
- `rect: Rect` - Viewport rectangle
- `draw_bg`, `draw_map`, `draw_poi`, `draw_label` - Drawing primitives
- `label_state: &mut LabelState` - Label collision state
- `render_scratch: &mut RenderScratch` - Scratch buffers
- Implemented `RenderGraph` struct with methods:
- `new()` - Creates graph with default passes
- `add_pass(pass)` - Adds a custom pass
- `remove_pass(pass_type)` - Removes a pass
- `get_pass(pass_type)` - Gets a pass by type
- `execute(ctx)` - Executes all enabled passes in z_order
- `total_tiles_drawn()` / `total_features_drawn()` - Stats
### 2. Refactored `view.rs`
**File: `crates/apps/map/src/view.rs`**
- Removed ~150 lines of hardcoded if-statements from `draw_walk()`
- Replaced with single call to `render_graph.execute(&mut ctx)`
- Reduced `draw_walk()` from ~153 lines to ~50 lines (67% reduction)
**Before:**
```rust
fn draw_walk(&mut self, cx: &mut Cx2d, ...) -> DrawStep {
// ... setup code ...
// Background pass
if self.render_graph.pass(PassType::Background).map_or(true, |p| p.should_execute(view_zoom)) {
// 5 lines of drawing code
}
// Fill pass
if self.render_graph.pass(PassType::Fill).map_or(true, |p| p.should_execute(view_zoom)) {
// 15 lines of drawing code
}
// Stroke pass
if self.render_graph.pass(PassType::Stroke).map_or(true, |p| p.should_execute(view_zoom)) {
// 15 lines of drawing code
}
// POI pass
if self.render_graph.pass(PassType::Poi).map_or(true, |p| p.should_execute(view_zoom)) {
// 25 lines of drawing code
}
// Label pass
if self.render_graph.pass(PassType::Label).map_or(true, |p| p.should_execute(view_zoom)) {
// 10 lines of drawing code
}
// ... cleanup code ...
}
```
**After:**
```rust
fn draw_walk(&mut self, cx: &mut Cx2d, ...) -> DrawStep {
// ... setup code ...
let mut ctx = super::render_graph::RenderContext {
cx,
cache: &self.cache,
draw_tiles: &draw_tiles,
entries: &entries,
view_zoom,
map_offset,
rect,
draw_bg: &mut self.draw_bg,
draw_map: &mut self.draw_map,
draw_poi: &mut self.draw_poi,
draw_label: &mut self.draw_label,
label_state: &mut self.label_state,
render_scratch: &mut self.render,
};
self.render_graph.execute(&mut ctx);
// ... cleanup code ...
}
```
### 3. Created Comprehensive Tests
**File: `crates/apps/map/tests/render_graph_tests.rs`**
- 13 integration tests covering:
- Default pass creation and ordering
- Pass type properties
- Zoom gating behavior
- Enable/disable functionality
- Pass retrieval and removal
- Custom pass ordering
- Stats collection
- Zoom range customization
- Pass type names and z_orders
## Benefits
### 1. Extensibility
Adding a new render pass now requires only:
```rust
struct MyCustomPass;
impl RenderPass for MyCustomPass {
fn pass_type(&self) -> PassType { PassType::Debug }
fn z_order(&self) -> i32 { 500 }
fn is_enabled(&self) -> bool { true }
fn min_zoom(&self) -> f64 { 0.0 }
fn max_zoom(&self) -> f64 { 30.0 }
fn execute(&self, ctx: &mut RenderContext) -> PassStats {
// Custom rendering logic
PassStats::default()
}
}
// Add to graph
graph.add_pass(Box::new(MyCustomPass));
```
No modifications to `view.rs` required!
### 2. Maintainability
- Each pass is self-contained in its own struct
- Logic is localized to the pass implementation
- Easier to understand and modify individual passes
### 3. Testability
- Each pass can be tested independently
- Render graph behavior can be tested without full UI
- Mock passes can be created for testing
### 4. Performance
- Passes are sorted by z_order once at creation
- Zoom gating is checked per-pass, not globally
- Pre-fetched entries are reused across all passes
## Architecture Diagram
```
┌─────────────────────────────────────────────────────────────┐
│ NigigMapView │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ draw_walk() │ │
│ │ │ │
│ │ 1. Setup (viewport, tiles, entries) │ │
│ │ 2. Create RenderContext │ │
│ │ 3. render_graph.execute(&mut ctx) ─────────────┐ │ │
│ │ 4. Cleanup │ │ │
│ └────────────────────────────────────────────────────┼───┘ │
│ │ │
│ ┌────────────────────────────────────────────────────┼───┐ │
│ │ RenderGraph │ │ │
│ │ │ │ │
│ │ passes: Vec<Box<dyn RenderPass>> │ │ │
│ │ ├── BackgroundPass (z_order: 0) │ │ │
│ │ ├── FillPass (z_order: 100) │ │ │
│ │ ├── StrokePass (z_order: 200) │ │ │
│ │ ├── PoiPass (z_order: 300, min_zoom: 13.0) │ │ │
│ │ └── LabelPass (z_order: 400, min_zoom: 13.0) │ │ │
│ │ │ │ │
│ │ execute(ctx) { │ │ │
│ │ for pass in passes (sorted by z_order) { │ │ │
│ │ if pass.should_execute(ctx.view_zoom) { │ │ │
│ │ pass.execute(ctx); ◄────────────────────────┘ │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Metrics
| Metric | Before | After | Change |
|--------|--------|-------|--------|
| `view.rs` lines | 1180 | ~1030 | -150 lines (-13%) |
| `draw_walk()` lines | 153 | 50 | -103 lines (-67%) |
| Hardcoded if-statements | 5 | 0 | -5 (100% removed) |
| Extensibility | Poor (modify view.rs) | Excellent (implement trait) | Major improvement |
| Testability | Poor (monolithic) | Good (modular) | Major improvement |
## Testing
### Unit Tests (in `render_graph.rs`)
- Pass creation and properties
- Zoom gating logic
- Pass ordering
- Enable/disable behavior
### Integration Tests (in `tests/render_graph_tests.rs`)
- 13 comprehensive tests
- Covers all public API
- Tests custom pass creation
- Validates z_order sorting
## Future Enhancements
### 1. Pass Configuration API
Currently passes are created with default settings. Future work could add:
```rust
graph.configure_pass(PassType::Poi, |pass| {
pass.min_zoom = 10.0;
pass.enabled = false;
});
```
### 2. Pass Dependencies
Some passes might depend on others (e.g., labels might need fill pass to complete first):
```rust
trait RenderPass {
fn dependencies(&self) -> Vec<PassType> { vec![] }
}
```
### 3. Conditional Pass Execution
Allow passes to skip execution based on runtime conditions:
```rust
trait RenderPass {
fn should_skip(&self, ctx: &RenderContext) -> bool { false }
}
```
### 4. Pass Profiling
Add timing information to PassStats:
```rust
pub struct PassStats {
pub pass_type: Option<PassType>,
pub tiles_drawn: usize,
pub features_drawn: usize,
pub execution_time_us: u64, // New field
pub skipped: bool,
pub reason: Option<SkipReason>,
}
```
## Conclusion
Phase 3 successfully transformed the render graph from a configuration-based system with hardcoded execution to a true trait-based system with extensible passes. The code is now:
- **More maintainable**: Each pass is self-contained
- **More extensible**: New passes require only trait implementation
- **More testable**: Passes can be tested independently
- **Cleaner**: `view.rs` reduced by 150 lines
This sets the foundation for future enhancements like custom passes, pass dependencies, and advanced profiling.
## Next Steps
1. **Commit and push** the Phase 3 changes
2. **Test** the implementation with the full UI
3. **Document** the new API for users
4. **Consider** Phase 4 enhancements (pass configuration API, profiling)