# 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> │ │ │ │ │ ├── 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 { 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, pub tiles_drawn: usize, pub features_drawn: usize, pub execution_time_us: u64, // New field pub skipped: bool, pub reason: Option, } ``` ## 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)