- 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.
287 lines
10 KiB
Markdown
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)
|