Root cause: when update_fire ignited a neighbor via grid.set(), the new Fire
cell had updated_this_tick=false. If the dirty rect iteration hadn't reached
that cell yet, it was processed in the same tick, igniting ITS neighbors,
creating exponential spread within a single tick.
Fix: set updated_this_tick=true on all newly created Fire cells so they
wait until the next tick before spreading.
Applied to:
- update_fire: igniting flammable neighbors
- lava_interact: lava igniting wood/grass/flesh
- update_flesh: flesh turning to fire from heat
- update_grass: grass turning to fire from heat
- update_water: water turning to steam from heat
- projectile.rs: fireball impact igniting cells
Result: fire spreads 1 cell per tick (linear, not exponential).
171 FPS avg, p99 14ms — no lag when spamming fireballs.
- stream_chunks: max 2 chunk generations per tick (was unlimited)
- stream_chunks: save every 10 ticks, 1 chunk at a time (was every tick)
- stream_chunks: unload every 120 ticks (was 60)
- CA dirty rect limit 4096→2048, subdivide instead of skip
- Layer step dirty rect limit 4096→2048
- update_active_chunks: simplified — radius 2 around player for infinite,
direct activation for bounded, no HashSet allocation
- ensure_chunk: early return after cache load (skip redundant insert)
- 129 FPS avg, p99 11ms (was 50 FPS, p99 265ms)
- UiLayer.font_scale: dynamically computed as (ui_height / 200).clamp(2, 6)
Larger screens get bigger UI text automatically
- draw_text: renders each font pixel as font_scale×font_scale block
(was 1×1, now 2×2 to 6×6 depending on screen size)
- text_width: now instance method, returns scaled width
- draw_health_bar: bar width scales with font_scale
- draw_character_panel: panel dimensions and row spacing scale
- draw_hud: bar height, row spacing, bar width scale
- draw_inventory_overlay: slot sizes scale with font_scale
- draw_messages: line spacing scales with font_height
- draw_death_screen: centered text uses scaled text_width
- All 185 tests + 14 scenarios pass
- Benchmark: 61.9 FPS (was 82.7 — 25% regression from more set_alpha calls)
- Cache active_chunks(): one allocation instead of 5 per tick
- heat_transfer/gas_step/pressure_step: direct chunk array access
(chunk.temps[idx], chunk.gas_*[idx], chunk.pressure[idx]) instead of
grid.get_temp()/set_temp() (eliminates 5 chunk-lookups per cell)
- In-place updated_this_tick reset via get_chunk_mut (no Cell copy, no mark_dirty)
- light_step: gather sources first (one pass), skip if no sources,
reduced radius (lava 25→12, fire 15→6), frequency 10→20 ticks
- gas_step: skip chunks with no gas (gas_density all 0)
- pressure_step: skip chunks with all atmospheric (128), add mark_dirty
- pre_dirty: HashMap instead of Vec (O(1) lookup vs O(n) linear search)
- Cross-chunk heat transfer: edge_temps pre-loaded before mutable borrow
- Spread chunk save I/O: 1 chunk/tick instead of all every 60 ticks
- All 185 tests + 14 scenarios pass
- Benchmark: 82.7 FPS avg, p99 86ms (was 18 FPS, p99 442ms)