Summary
Enu ships with 2D canvas batching disabled — batching/options/use_batching=false (and use_batching_in_editor=false) in app/project.godot. This is the committed baseline, not drift. We should quantify its cost and, most likely, re-enable it.
Why this is on the radar
While chasing the island load stall, profiling the render thread (thread_model=2, VisualServer on its own thread) under load repeatedly showed it saturated on the 2D canvas path, not on voxel work:
render_canvas → _legacy_canvas_item_render_command → _draw_polygon → safe_buffer_sub_data → glBufferSubData → GLDContextRec::getCommandBuffer → new MTLCommandBuffer → semaphore_wait
On Apple Silicon, GLES3 runs through Apple's GL-on-Metal layer, so each un-batched canvas item forces its own glBufferSubData → a fresh Metal command-buffer allocation, and when the pool is exhausted the render thread blocks in semaphore_wait. With batching off, every glyph in the console/stats overlay, every toolbar element, and every turtle-path polygon pays this individually.
This is NOT the load-stall bug (that's a separate intermittent missing-terrain-chunk issue — the support raycast finds no ground under spawn and the gate rides its 30s deadline; being fixed separately). But the batched-vs-unbatched render cost is real and independent, and it directly affects the "30+ fps while the world streams" goal because the 2D UI shares the render thread with voxel mesh uploads.
What to look at
use_batching=false — the main lever. Find out why it's off. Godot 3.5 batching has historically caused 2D artifacts (custom shaders, overlapping items) — it was likely disabled to fix a visual bug, so flipping it on is not free: verify the turtle path, toolbar, and stats/console overlays render correctly before/after.
- TurtleOverlay is its own
Viewport (separate render target inside a ViewportContainer, app/components/TurtleOverlay.gdns). Check it isn't running a full render pass every frame when the turtle isn't drawing.
- Stats/console overlay redraw rate — if it re-emits its whole text canvas every frame, that's a large un-batched item count; redraw-on-change would cut it.
How to quantify (no new instrumentation needed)
ENU_PERF_LOG=1 already emits a PERF line every ~2s (src/game.nim) with exactly the right metrics: fps, frame_ms, draw_calls (RENDER_DRAW_CALLS_IN_FRAME), objects, vertex_mem_kb. draw_calls will show batching's effect most directly.
Suggested A/B: ENU_PERF_LOG=1 nim start with use_batching=false vs true, comparing steady-state (idle in a built level, console/toolbar visible) and while moving during streaming. Report fps + draw_calls deltas.
A crude earlier A/B (during load, not steady state) showed batching-on measurably reduced render-thread pressure — treat as a hint, not a real number.
Context / handoff
Found during a load-perf debugging session on branch island-load-perf. The missing-chunk root cause is being handled separately; this issue is purely the 2D-rendering efficiency workstream. Owner intends this fixed regardless of the fps delta.
Summary
Enu ships with 2D canvas batching disabled —
batching/options/use_batching=false(anduse_batching_in_editor=false) inapp/project.godot. This is the committed baseline, not drift. We should quantify its cost and, most likely, re-enable it.Why this is on the radar
While chasing the island load stall, profiling the render thread (
thread_model=2, VisualServer on its own thread) under load repeatedly showed it saturated on the 2D canvas path, not on voxel work:render_canvas → _legacy_canvas_item_render_command → _draw_polygon → safe_buffer_sub_data → glBufferSubData → GLDContextRec::getCommandBuffer → new MTLCommandBuffer → semaphore_waitOn Apple Silicon, GLES3 runs through Apple's GL-on-Metal layer, so each un-batched canvas item forces its own
glBufferSubData→ a fresh Metal command-buffer allocation, and when the pool is exhausted the render thread blocks insemaphore_wait. With batching off, every glyph in the console/stats overlay, every toolbar element, and every turtle-path polygon pays this individually.This is NOT the load-stall bug (that's a separate intermittent missing-terrain-chunk issue — the support raycast finds no ground under spawn and the gate rides its 30s deadline; being fixed separately). But the batched-vs-unbatched render cost is real and independent, and it directly affects the "30+ fps while the world streams" goal because the 2D UI shares the render thread with voxel mesh uploads.
What to look at
use_batching=false— the main lever. Find out why it's off. Godot 3.5 batching has historically caused 2D artifacts (custom shaders, overlapping items) — it was likely disabled to fix a visual bug, so flipping it on is not free: verify the turtle path, toolbar, and stats/console overlays render correctly before/after.Viewport(separate render target inside aViewportContainer,app/components/TurtleOverlay.gdns). Check it isn't running a full render pass every frame when the turtle isn't drawing.How to quantify (no new instrumentation needed)
ENU_PERF_LOG=1already emits aPERFline every ~2s (src/game.nim) with exactly the right metrics:fps,frame_ms,draw_calls(RENDER_DRAW_CALLS_IN_FRAME),objects,vertex_mem_kb.draw_callswill show batching's effect most directly.Suggested A/B:
ENU_PERF_LOG=1 nim startwithuse_batching=falsevstrue, comparing steady-state (idle in a built level, console/toolbar visible) and while moving during streaming. Report fps + draw_calls deltas.A crude earlier A/B (during load, not steady state) showed batching-on measurably reduced render-thread pressure — treat as a hint, not a real number.
Context / handoff
Found during a load-perf debugging session on branch
island-load-perf. The missing-chunk root cause is being handled separately; this issue is purely the 2D-rendering efficiency workstream. Owner intends this fixed regardless of the fps delta.