- 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.
10 KiB
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
RenderPasstrait with methods:pass_type()- Returns the pass typez_order()- Returns draw order (lower = drawn first)is_enabled()- Whether pass is activemin_zoom()/max_zoom()- Zoom range gatingshould_execute(zoom)- Combined checkexecute(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
RenderContextstruct to pass all necessary state to passes:cx: &mut Cx2d- Makepad drawing contextcache: &TileCache- Tile cachedraw_tiles: &[TileKey]- Tiles to drawentries: &[(TileKey, &TileEntry, f32)]- Pre-fetched entries with scalesview_zoom: f64- Current zoom levelmap_offset: Vec2f- Map offset for renderingrect: Rect- Viewport rectangledraw_bg,draw_map,draw_poi,draw_label- Drawing primitiveslabel_state: &mut LabelState- Label collision staterender_scratch: &mut RenderScratch- Scratch buffers
-
Implemented
RenderGraphstruct with methods:new()- Creates graph with default passesadd_pass(pass)- Adds a custom passremove_pass(pass_type)- Removes a passget_pass(pass_type)- Gets a pass by typeexecute(ctx)- Executes all enabled passes in z_ordertotal_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:
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:
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:
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:
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):
trait RenderPass {
fn dependencies(&self) -> Vec<PassType> { vec![] }
}
3. Conditional Pass Execution
Allow passes to skip execution based on runtime conditions:
trait RenderPass {
fn should_skip(&self, ctx: &RenderContext) -> bool { false }
}
4. Pass Profiling
Add timing information to PassStats:
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.rsreduced by 150 lines
This sets the foundation for future enhancements like custom passes, pass dependencies, and advanced profiling.
Next Steps
- Commit and push the Phase 3 changes
- Test the implementation with the full UI
- Document the new API for users
- Consider Phase 4 enhancements (pass configuration API, profiling)