28 KiB
Map Renderer Rewrite Plan
Date: 2026-07-26
Status: Draft
Scope: makepad/widgets/src/map/ (8 files, ~9,000 lines)
Constraint: Preserve Makepad Widget/Script API, all 184+ existing tests, downstream book.rs DSL
Problem Statement
The renderer is a monolithic MapView struct (1,882 lines) that owns viewport, scheduling,
caching, rendering, labels, interaction, and style in one object. The review identified:
- Modular architecture — 8.5/10: Good ideas, but everything lives in one struct
- Performance — 8/10: CPU-bound draw, unnecessary HashMap lookups, per-frame recomputation
- Responsibility boundaries — Scheduler knows about rendering, rendering knows about workers
- Hidden state machine —
TileLoadStatescattered asif loading/if ready/if cached/if dirty - Cache ownership — Who evicts? Who marks stale? Who retries? Not obvious
- Mutable state — Dozens of fields mutated from multiple code paths
- No render graph — Everything funnels into one draw path
- Long functions — 200-500 line methods with deep nesting
Target Architecture
Current Module Structure
map/
mod.rs (13 lines) — pub exports
view.rs (1882 lines) — MapView widget: EVERYTHING
tile.rs (2163 lines) — Tile types + loading + tessellation + disk cache
geometry.rs (1968 lines) — Projection, DP, tessellation (well-factored)
label.rs (1098 lines) — Label extraction, scoring, collision grid
style.rs (496 lines) — Theme system
style_json.rs(3227 lines) — JSON parser + tests (behind map_style feature)
asset_loader.rs (124 lines) — Sprite/glyph caches
sprite.rs (433 lines) — POI icon classification
Proposed Module Structure
map/
mod.rs — pub exports (MapView + subsystems for testing)
view.rs — THIN Widget shell (~400 lines): DSL, #[live] props, delegates to subsystems
viewport.rs — NEW: ViewportState (center, zoom, screen<->world, wrap/clamp)
scheduler.rs — NEW: TileScheduler (request queue, retry, prioritization, cancellation)
cache.rs — NEW: TileCache (HashMap, eviction, dedup, generation, GC)
renderer.rs — NEW: RenderPass (fill, stroke, POI, label passes with explicit ordering)
label_state.rs — NEW: LabelState (all scratch buffers, collision grid, placement)
tile.rs — SLIMMED: Tile types only (TileKey, TileEntry, TileLoadState enum, message types)
tile_decode.rs — NEW: MVT parsing, tessellation, Overpass body processing (extracted from tile.rs)
tile_disk.rs — NEW: Disk cache read/write, mbtiles batch loading (extracted from tile.rs)
geometry.rs — UNCHANGED
label.rs — UNCHANGED (scoring/extraction only)
style.rs — UNCHANGED
style_json.rs — UNCHANGED
asset_loader.rs — UNCHANGED
sprite.rs — UNCHANGED
Total new files: 6 Total modified files: 3 (view.rs, tile.rs, mod.rs) Total unchanged files: 6 (geometry, label, style, style_json, asset_loader, sprite)
New Types — Detailed Design
1. ViewportState (viewport.rs, ~150 lines)
Addresses: Review points #1, #4 (mutable state, coupling)
Extracts all viewport/coordinate logic from view.rs. No dependency on tiles, workers, or rendering.
pub struct ViewportState {
center_norm: Vec2d, // [0,1] Web Mercator
zoom: f64,
min_zoom: f64,
max_zoom: f64,
view_rect: Rect, // pixel rect of widget area
}
// Pure functions, no Cx dependency
impl ViewportState {
pub fn view_zoom(&self) -> f64;
pub fn request_zoom_level(&self, use_local: bool) -> u32;
pub fn world_size(&self) -> f64; // 256 * 2^zoom
pub fn center_world(&self) -> Vec2d; // center_norm * world_size
pub fn map_offset(&self) -> Vec2f; // screen offset for GPU uniforms
pub fn wrap_and_clamp(&mut self);
pub fn set_rect(&mut self, rect: Rect);
// Interaction
pub fn apply_drag(&mut self, delta_pixels: Vec2d);
pub fn apply_pinch(&mut self, initial_zoom: f64, initial_center: Vec2d,
initial_distance: f64, new_distance: f64, center: Vec2d);
pub fn apply_scroll(&mut self, scroll: f64, anchor_abs: Vec2d);
// Coordinate transforms
pub fn screen_to_world(&self, screen_pos: Vec2d) -> Vec2d;
pub fn world_to_screen(&self, world_pos: Vec2d) -> Vec2d;
pub fn tile_world_size_zoom(zoom: f64) -> f64; // static
// Visible tile computation
pub fn visible_tile_keys(&self) -> Vec<TileKey>;
// Property sync
pub fn sync_from_properties(&mut self, center_lon: f64, center_lat: f64, zoom: f64);
}
Current code moving here:
view_zoom(),request_zoom_level()(view.rs:1851-1863)wrap_and_clamp_center()(view.rs:1064-1067)zoom_with_anchor()(view.rs:1069-1100)visible_tile_keys()(view.rs:1185-1226)- Part of
on_after_applythat sets center_norm, zoom - Part of
draw_walkthat computes map_offset - Part of
handle_eventFingerMove/FingerDown/FingerUp interaction
2. TileCache (cache.rs, ~200 lines)
Addresses: Review points #3, #4, #19 (cache ownership, mutable state, memory growth)
Single owner of tile lifetime. Every mutation goes through TileCache methods.
pub struct TileCache {
tiles: HashMap<TileKey, TileEntry>,
frame_counter: u64,
style_epoch: u64,
max_tiles: usize, // default 640
stale_frame_threshold: u64, // 240 frames
}
Explicit TileLoadState enum (review point #5 — replace hidden state machine):
pub enum TileLoadState {
Missing,
LoadingLocal,
LoadingNetwork,
Ready {
fill_geometry: Option<Geometry>,
stroke_geometry: Option<Geometry>,
feature_count: usize,
labels: Vec<Label>,
pois: Vec<PoiFeature>,
},
Failed {
retry_after: u64,
attempts: u8,
},
}
Methods:
impl TileCache {
// Query
pub fn get(&self, key: TileKey) -> Option<&TileEntry>;
pub fn get_mut(&mut self, key: TileKey) -> Option<&mut TileEntry>;
pub fn is_ready(&self, key: TileKey) -> bool;
pub fn is_loading(&self, key: TileKey) -> bool;
pub fn is_failed(&self, key: TileKey) -> bool;
pub fn find_ready_ancestor(&self, key: TileKey) -> Option<TileKey>;
pub fn find_ready_descendants(&self, key: TileKey) -> Vec<TileKey>;
pub fn ready_count(&self, visible: &[TileKey]) -> usize;
pub fn loading_count(&self) -> usize;
// Mutation
pub fn insert_loading(&mut self, key: TileKey, state: TileLoadState);
pub fn insert_ready(&mut self, cx: &mut Cx, key: TileKey, buffers: TileBuffers);
pub fn mark_failed(&mut self, key: TileKey, reason: &str);
pub fn tick(&mut self); // advance frame_counter
// Eviction
pub fn evict(&mut self, visible: &HashSet<TileKey>, target_zoom: u32);
// Theme change
pub fn clear_all(&mut self); // bumps style_epoch, clears everything
pub fn style_epoch(&self) -> u64;
// Status
pub fn status_counts(&self, visible: &[TileKey]) -> TileStatusCounts;
}
Current code moving here:
- All
self.tiles.*operations from view.rs insert_ready_tile()(view.rs:813-846)mark_tile_failed()(view.rs:1039-1062)tile_is_ready(),find_ready_ancestor(),fill_ready_descendants()(view.rs:1256-1296)- Eviction block (view.rs:1163-1181)
apply_theme_change()tile clearing (view.rs:788-797)
3. TileScheduler (scheduler.rs, ~250 lines)
Addresses: Review points #1, #2, #16, #17, #18 (scheduler/renderer coupling, race conditions, worker starvation)
Owns the request queue, retry logic, priority, and cancellation. Does NOT know about rendering.
pub struct TileScheduler {
visible_tiles: Vec<TileKey>,
local_requested: HashSet<TileKey>,
local_missing: HashSet<TileKey>,
request_to_tile: HashMap<LiveId, PendingTileRequest>,
next_request_id: u64,
pending_count: usize,
local_source_missing_logged: bool,
}
Generation tracking (review point #16 — prevent old tiles overwriting new):
Every request carries a generation: u64 that is checked when the result arrives.
The generation is incremented on zoom change and theme change.
pub struct PendingTileRequest {
pub tile_key: TileKey,
pub endpoint: &'static str,
pub generation: u64,
}
pub enum TileAction {
// Caller should dispatch these
LoadLocalBatch {
mbtiles_path: PathBuf,
cache_dir: String,
requested: Vec<TileKey>,
theme: CompiledMapTheme,
style_epoch: u64,
generation: u64,
},
LoadFromDiskCache {
tile_key: TileKey,
cache_path: PathBuf,
theme: CompiledMapTheme,
style_epoch: u64,
generation: u64,
},
LoadFromNetwork {
request_id: LiveId,
http_request: HttpRequest,
tile_key: TileKey,
generation: u64,
},
Nothing,
}
Methods:
impl TileScheduler {
// Core scheduling
pub fn update(&mut self, cache: &TileCache, viewport: &ViewportState,
config: &SchedulerConfig) -> Vec<TileAction>;
pub fn on_batch_loaded(&mut self, requested: Vec<TileKey>, loaded: Vec<TileKey>);
pub fn on_batch_failed(&mut self, requested: Vec<TileKey>);
pub fn on_tile_loaded(&mut self, tile_key: TileKey);
pub fn on_tile_failed(&mut self, tile_key: TileKey);
// HTTP integration
pub fn register_http_request(&mut self, request_id: LiveId, tile_key: TileKey, generation: u64);
pub fn on_http_response(&mut self, request_id: LiveId) -> Option<(TileKey, u64)>;
pub fn on_http_error(&mut self, request_id: LiveId) -> Option<(TileKey, u64)>;
// Theme change
pub fn reset_generation(&mut self); // on theme change
// Visible tile management
pub fn visible_tiles(&self) -> &[TileKey];
}
pub struct SchedulerConfig {
pub use_network: bool,
pub use_local_mbtiles: bool,
pub max_pending_requests: usize, // default 2
pub max_local_tile_batch: usize, // default 10
pub max_tile_retries: u8, // default 6
pub local_mbtiles_path: String,
pub local_tile_cache_dir: String,
}
Current code moving here:
request_visible_tiles_from_local_source()(view.rs:961-1037)request_tile()(view.rs:1298-1375)- Part of
ensure_visible_tiles()that does retry/request logic (view.rs:1109-1181) handle_tile_worker_messages()dispatch logic (view.rs:848-936)handle_http_response()dispatch logic (view.rs:658-727)handle_http_request_error()dispatch logic (view.rs:729-748)
4. RenderPass (renderer.rs, ~200 lines)
Addresses: Review points #11 (render graph), #8 (CPU bound)
Explicit ordered passes with clear separation.
pub struct RenderPass;
impl RenderPass {
pub fn draw_fill(
cx: &mut Cx2d,
draw_map: &mut DrawMapVector,
cache: &TileCache,
draw_keys: &[TileKey],
view_zoom: f64,
map_offset: Vec2f,
);
pub fn draw_stroke(
cx: &mut Cx2d,
draw_map: &mut DrawMapVector,
cache: &TileCache,
draw_keys: &[TileKey],
view_zoom: f64,
map_offset: Vec2f,
);
pub fn draw_pois(
cx: &mut Cx2d,
draw_poi: &mut DrawColor,
cache: &TileCache,
draw_keys: &[TileKey],
view_zoom: f64,
map_offset: Vec2f,
min_zoom: f64,
);
// Labels handled by LabelState::draw()
/// Build the list of tile keys to actually draw, with ancestor/descendant fallback
pub fn build_draw_keys(
cache: &TileCache,
visible_tiles: &[TileKey],
draw_seen: &mut HashSet<TileKey>,
draw_tiles: &mut Vec<TileKey>,
descendant_scratch: &mut Vec<TileKey>,
);
}
Current code moving here:
- Fill pass loop (view.rs:569-586)
- Stroke pass loop (view.rs:588-608)
- POI pass loop (view.rs:610-640)
fill_draw_tile_keys()(view.rs:1228-1254)
5. LabelState (label_state.rs, ~300 lines)
Addresses: Review points #4, #7 (mutable state, too many responsibilities)
All scratch buffers for label placement in one struct. Keeps label.rs unchanged as
the pure extraction/scoring module.
pub struct LabelState {
// Scratch buffers (reuse across frames)
scratch_candidates: Vec<LabelCandidate>,
scratch_accepted_centers: HashMap<String, Vec<Vec2d>>,
scratch_accepted_bounds: Vec<Rect>,
scratch_accepted_plans: Vec<(f64, usize, usize)>,
scratch_collision_grid: HashMap<(i32, i32), Vec<usize>>,
scratch_screen_path: Vec<Vec2d>,
scratch_cumulative: Vec<f64>,
scratch_smooth_a: Vec<Vec2d>,
scratch_smooth_b: Vec<Vec2d>,
path_glyphs: Vec<PathGlyphInstance>,
// Performance tracking
perf: LabelPerfStats,
prev_perf: LabelPerfStats,
}
Methods:
impl LabelState {
pub fn place_and_draw(
&mut self,
cx: &mut Cx2d,
cache: &TileCache,
draw_label: &mut DrawRotatedText,
draw_keys: &[TileKey],
view_zoom: f64,
map_offset: Vec2f,
rect: Rect,
);
// Internally:
fn collect_candidates(/* ... */);
fn build_placement(/* ... */);
}
Current code moving here:
place_and_draw_labels()(view.rs:1377-1533)collect_label_candidates()(view.rs:1535-1659)build_label_placement()(view.rs:1661-1759)
6. Tile module split
Addresses: Review point #7 (too many responsibilities per file)
tile.rs currently handles types, MVT parsing, tessellation, Overpass queries, disk caching,
mbtiles batch loading — all in 2,163 lines. Split into:
tile.rs (~300 lines) — Types and tile lifecycle only:
TileKey,TileEntry,TileLoadState(the explicit enum)TileBuffers,TileWorkerMessagePendingTileRequestTileStatusCountsis_descendant_tile()overpass_query(),overpass_endpoint()
tile_decode.rs (~800 lines) — Parsing and tessellation:
build_tile_buffers_from_body()build_tile_buffers_from_response()build_tile_buffers_from_response_owned()tessellate_tile_buffers()- MVT protobuf decode functions
mbtiles_tile_to_overpass_response()- Node projection, way grouping, polygon tessellation
tile_disk.rs (~300 lines) — Disk I/O:
tile_data_cache_path_for()store_tile_data_cache_on_disk()load_local_tile_batch()- Path resolution helpers
Revised view.rs (~400 lines)
The widget becomes a thin coordinator:
pub struct MapView {
// Framework
uid: WidgetUid,
source: ScriptObjectRef,
walk: Walk,
layout: Layout,
// Draw objects (#[redraw] #[live])
draw_bg: DrawColor,
draw_map: DrawMapVector,
draw_label: DrawRotatedText,
draw_poi: DrawColor,
draw_text: DrawText,
// DSL-configurable properties (#[live])
center_lon: f64,
center_lat: f64,
zoom: f64,
min_zoom: f64,
max_zoom: f64,
dark_theme: bool,
style_light: MapThemeStyle,
style_dark: MapThemeStyle,
use_network: bool,
use_local_mbtiles: bool,
local_mbtiles_path: String,
local_tile_cache_dir: String,
// Subsystems (#[rust])
viewport: ViewportState,
cache: TileCache,
scheduler: TileScheduler,
label_state: LabelState,
renderer: RenderPass,
// Style (#[rust])
compiled_style_light: CompiledMapTheme,
compiled_style_dark: CompiledMapTheme,
applied_dark_theme: Option<bool>,
#[cfg(feature = "map_style")]
style_json_light: Option<StyleJson>,
// Worker thread (#[rust])
tile_worker_rx: ToUIReceiver<TileWorkerMessage>,
tile_thread_pool: Option<TagThreadPool<TileKey>>,
// Interaction (#[rust]) — kept minimal, delegates to viewport
drag_start_abs: Option<Vec2d>,
drag_start_center_norm: Vec2d,
active_fingers: HashMap<DigitId, Vec2d>,
pinch_initial_distance: Option<f64>,
pinch_initial_zoom: f64,
pinch_initial_center_norm: Vec2d,
}
Method count reduction:
| Before | After | Change |
|---|---|---|
| ~30 methods on MapView | ~15 methods on MapView | -50% |
| 1,882 lines | ~400 lines | -79% |
draw_walk becomes:
fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk: Walk) -> DrawStep {
let rect = cx.walk_turtle(walk);
self.viewport.set_rect(rect);
self.draw_bg.draw_abs(cx, rect);
// Schedule tiles (this was previously mixed into the draw path)
let actions = self.scheduler.update(&self.cache, &self.viewport, &self.config());
self.execute_actions(cx, actions);
// Render
let map_offset = self.viewport.map_offset();
let view_zoom = self.viewport.view_zoom();
RenderPass::build_draw_keys(&self.cache, self.scheduler.visible_tiles(), ...);
RenderPass::draw_fill(cx, &mut self.draw_map, &self.cache, &draw_tiles, view_zoom, map_offset);
RenderPass::draw_stroke(cx, &mut self.draw_map, &self.cache, &draw_tiles, view_zoom, map_offset);
RenderPass::draw_pois(cx, &mut self.draw_poi, &self.cache, &draw_tiles, view_zoom, map_offset, 13.0);
if view_zoom >= 13.0 {
self.label_state.place_and_draw(
cx, &self.cache, &mut self.draw_label,
&draw_tiles, view_zoom, map_offset, rect
);
}
DrawStep::done()
}
handle_event becomes:
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
// 1. Worker messages → cache
self.handle_tile_worker_messages(cx);
// 2. HTTP → scheduler
self.widget_match_event(cx, event, scope);
// 3. Keyboard
if let Event::KeyDown(KeyDownEvent { key_code: KeyCode::KeyT, .. }) = event {
self.set_dark_theme(cx, !self.dark_theme);
}
// 4. Touch interaction → viewport
for actions in cx.read_hit_actions(&self.draw_bg.area, hit::Event::Hit(e)) {
match actions {
Hit::FingerDown(pe) => self.handle_finger_down(pe),
Hit::FingerMove(pe) => self.handle_finger_move(pe),
Hit::FingerUp(pe) => self.handle_finger_up(pe),
Hit::FingerScroll(fe) => self.viewport.apply_scroll(fe.scroll.y, fe.abs),
_ => {}
}
}
}
Phase Plan
Phase 1: Extract types (no behavior change)
Goal: Move types into new files without changing any behavior. Tests must pass.
Steps:
-
Create
tile.rsslim version — moveTileKey,TileEntry,TileLoadState,TileBuffers,TileWorkerMessage,PendingTileRequestinto type-only definitions. Keep existingtile.rsastile_decode.rs+tile_disk.rstemporarily. -
Create
viewport.rs— extractViewportStatewith all viewport methods. Wire it intoMapViewas a field. MapView methods delegate to viewport. -
Create
cache.rs— extractTileCachewrapping theHashMap<TileKey, TileEntry>and all methods that operate on it. Wire into MapView. -
Create
label_state.rs— extractLabelStatewith all scratch buffers and label methods. Wire into MapView. -
Create
renderer.rs— extractRenderPasswith fill/stroke/POI draw loops. Wire into MapView.
Verification: cargo test -p makepad-widgets --features maps --lib — all 184+ tests pass.
Risk: Makepad #[derive(Widget)] macro may have constraints on field types.
Mitigation: subsystems are plain Rust structs, no derive macros needed.
Phase 2: Extract scheduler
Goal: Separate tile scheduling from rendering.
Steps:
- Create
scheduler.rs— extractTileSchedulerwith request/retry/priority logic. - Add generation tracking to
PendingTileRequestandTileWorkerMessage. - Make
scheduler.update()returnVec<TileAction>instead of directly issuing requests. - MapView
execute_actions()dispatches actions to thread pool / HTTP. - Add
SchedulerConfigto decouple scheduler from live properties.
Verification: All tests pass. Manual test: pan around Nairobi, verify tiles load correctly.
Phase 3: Split tile.rs
Goal: Break the 2,163-line tile.rs into focused modules.
Steps:
- Move MVT parsing + tessellation to
tile_decode.rs. - Move disk cache + mbtiles batch to
tile_disk.rs. - Keep
tile.rsas type definitions + Overpass query generation.
Verification: All tests pass (tile.rs has 49 tests — they move with the functions).
Phase 4: Optimize
Address: Review points #6, #7, #8, #9, #10, #19.
Steps:
-
Cache visible tile set — Don't recompute
visible_tile_keys()every frame. Store result inViewportState, only recompute when viewport actually changes. Add adirtyflag. -
Reduce HashMap traffic in draw loop —
RenderPassmethods take&TileCacheand do single lookups per tile. The current code doesself.tiles.get(key)multiple times per tile in the draw loop. Cache the entry reference. -
Frame allocation audit — Add debug assertions that no allocation happens during normal frame rendering (all scratch buffers pre-allocated).
-
Generation-based stale detection — Worker messages carry generation. Cache checks generation before accepting results. Prevents zoom-10 results overwriting zoom-11 requests.
-
Worker priority — Visible tiles get higher priority in the thread pool. Use
TagThreadPoolwith priority tags or a priority queue wrapper. -
Memory stabilization — TileCache eviction runs every frame if count > threshold. Verify memory stabilizes during continuous panning. Add a
memory_bytes()estimation method for monitoring.
Phase 5: Render pass ordering (optional, future)
Address: Review point #11 (render graph).
Currently fill/stroke/POI/label are hardcoded loops. Future improvement:
enum Pass {
Background,
WaterFill,
LanduseFill,
BuildingFill,
RoadCasing,
RoadCenter,
Railway,
Waterway,
Label,
Poi,
Selection,
Debug,
}
Each pass has a draw() method and a z_order for sorting. This enables:
- Easy insertion of new passes (3D buildings, terrain, traffic)
- Per-pass culling
- Debug visualization (show only one pass)
Not implementing now — the current fill/stroke/POI/label ordering works. This is documented for future reference.
File-by-File Changes
view.rs (1,882 → ~400 lines)
Removed:
ViewportStatefields and methods →viewport.rsTileCachefields and methods →cache.rsTileSchedulerfields and methods →scheduler.rsLabelStatefields and methods →label_state.rsRenderPassmethods →renderer.rs- Tile type definitions →
tile.rs
Kept:
#[derive(Script, Widget)]struct with subsystem fieldsScriptHook::on_after_apply(thin: sync properties → viewport, rebuild styles)Widget::handle_event(thin: delegates to subsystems)Widget::draw_walk(thin: delegates to RenderPass + LabelState)WidgetMatchEvent(thin: delegates HTTP to scheduler)set_dark_theme,apply_theme_change,apply_theme_paletteload_style_json,recompile_style_for_zoomresolve_mbtiles_path(could move to SchedulerConfig)
tile.rs (2,163 → ~300 lines + 2 new files)
Keeps: Type definitions, overpass_query, overpass_endpoint, is_descendant_tile, constants
Moves to tile_decode.rs: MVT protobuf, tessellation, body processing
Moves to tile_disk.rs: Disk cache, mbtiles batch loading
New files (6 files, ~1,450 lines total)
| File | Lines | Responsibility |
|---|---|---|
viewport.rs |
~150 | Coordinate math, interaction, visible tiles |
cache.rs |
~200 | Tile storage, lifecycle, eviction |
scheduler.rs |
~250 | Request queue, retry, priority, generation |
renderer.rs |
~200 | Fill/stroke/POI draw passes |
label_state.rs |
~300 | Label scratch buffers, placement, drawing |
tile_decode.rs |
~800 | MVT parsing, tessellation |
tile_disk.rs |
~300 | Disk I/O, mbtiles batch |
Unchanged files (6)
geometry.rs(1,968 lines)label.rs(1,098 lines)style.rs(496 lines)style_json.rs(3,227 lines)asset_loader.rs(124 lines)sprite.rs(433 lines)
Downstream Compatibility
Public API (must not change)
// Widget trait — framework calls these
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope);
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep;
// ScriptHook — DSL properties flow in here
fn on_after_apply(&mut self, vm: &mut ScriptVm, apply: &Apply, scope: &mut Scope, value: ScriptValue);
// WidgetMatchEvent — HTTP responses
fn handle_http_response(&mut self, cx: &mut Cx, request_id: LiveId, response: &HttpResponse, scope: &mut Scope);
fn handle_http_request_error(&mut self, cx: &mut Cx, request_id: LiveId, err: &HttpError, scope: &mut Scope);
// Public methods
fn load_style_json(&mut self, json_str: &str) -> Result<(), String>;
fn recompile_style_for_zoom(&mut self, zoom: f64);
DSL (book.rs must not change)
map_layer := MapView {
width: Fill
height: Fill
center_lon: 36.8219
center_lat: -1.2921
zoom: 14.0
min_zoom: 10.0
max_zoom: 18.0
use_local_mbtiles: true
use_network: false
local_mbtiles_path: "kenya-shortbread-1.0.mbtiles"
}
mbtile_reader crate (must not change)
Separate crate at makepad/libs/mbtile_reader/. API:
MbtileReader::open()get_tiles_at_zoom()get_tiles_filtered()Value,Errortypes
Test Strategy
Existing tests (184+)
| Module | Tests | Status |
|---|---|---|
| geometry.rs | 54 | UNCHANGED — no code movement |
| tile.rs | 49 | MOVE with functions to tile_decode.rs / tile_disk.rs |
| label.rs | 69 | UNCHANGED — no code movement |
| style_json.rs | 101 | UNCHANGED — no code movement |
| sprite.rs | 25 | UNCHANGED — no code movement |
| mbtile_reader | 61 | SEPARATE CRATE — no change |
Zero test breakage — all tests move with their functions, module paths update in #[cfg(test)] blocks.
New tests per phase
Phase 1 (types):
viewport.rs: screen<->world transforms, wrap/clamp, visible tile keyscache.rs: insert/evict/mark-failed, generation trackingrenderer.rs: build_draw_keys with ancestor/descendant fallbacklabel_state.rs: (already covered by label.rs tests)
Phase 2 (scheduler):
scheduler.rs: request prioritization, retry timing, generation staleness- Action generation for various cache states
Phase 3 (tile split):
- Verify all existing tile.rs tests pass with new module paths
Phase 4 (optimize):
- Visible tile cache: verify no recomputation when viewport unchanged
- Frame allocation: debug assertion that scratch buffers don't reallocate
Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Makepad derive macro incompatibility | Low | High | Subsystems are plain structs, not widgets |
| Thread pool API changes | Low | Medium | Pin to current TagThreadPool<TileKey> API |
| Performance regression from indirection | Medium | Low | Profile after each phase; subsystems are #[inline] candidates |
| Test coverage gaps in view.rs | High | Medium | No tests exist for view.rs today — add integration tests in Phase 1 |
| merge conflicts with upstream makepad | Medium | Medium | Minimize changes to files outside map/ directory |
Decision Log
| Decision | Rationale |
|---|---|
Keep geometry.rs, label.rs, style.rs unchanged |
Already well-factored; 297 tests pass |
| ViewportState has no Cx dependency | Enables unit testing without Makepad context |
| TileScheduler returns actions, doesn't execute | Decouples scheduling from I/O execution |
| TileCache owns the HashMap | Single clear owner for tile lifecycle |
| RenderPass takes &TileCache, doesn't own tiles | Read-only during draw, no mutation in render path |
| LabelState keeps scratch buffers | Avoids per-frame allocation; keeps label.rs pure |
| Generation tracking via u64 counter | Simple, no ABA problem at realistic rates |
| SchedulerConfig struct for constants | Makes scheduling testable without live properties |
| Split tile.rs into 3 files | Each file has one responsibility |
| Phase 1 is purely mechanical extraction | Zero risk of behavior change; builds confidence |