# Draw2D: immediate-mode 2D drawing `simvx.graphics.draw2d.Draw2D` is the immediate-mode 2D API used by every `on_draw` callback. It owns one mutable per-frame buffer (`Draw2D._ops`) and the renderer walks that buffer in order each frame. ## Submission order is the GPU order Every draw call (`draw_rect`, `draw_line`, `draw_circle`, `draw_text`, `draw_texture`, `draw_texture_region`, `draw_nine_patch`, `fill_*`) appends one `Op` to `Draw2D._ops`. The renderer (`Draw2DPass`) walks that list in order and emits draws in the same order: **there is no other ordering mechanism**. ```python def on_draw(self, renderer): renderer.draw_texture(self.bg_texture, 0, 0, 320, 240) renderer.draw_rect((0, 0), (320, 240), filled=True, colour=(0, 0, 0, 0.7)) renderer.draw_text("Paused", (140, 110), colour=(1, 1, 1, 1), scale=2.0) ``` The texture paints first, then a dim rect dims the texture, then the text sits on top of both. This works regardless of whether the calls are fills, lines, text, or textures: the submission sequence is preserved end-to-end. Tree-level z-ordering still uses `Node2D.z_index` for ordering sibling node visits. Within a single `on_draw` body, call order is the contract. ## What's coalesced Adjacent ops sharing the same `(kind, clip, tex_id)` collapse into a single GPU draw. Typical scenes that group sprites by atlas pay one draw per atlas; ordering different kinds (rect → texture → rect) pays one draw per kind transition. Pipeline binding only happens on kind changes; scissor only on clip changes. ## Clipping `Draw2D.push_clip(x, y, w, h)` / `pop_clip()` / `reset_clip()` mutate a clip stack. Nested `push_clip` calls intersect with the current clip. Each op snapshots the current clip at submission time: a clip change after an op is appended doesn't affect that op. ```python renderer.push_clip(0, 0, 200, 200) renderer.draw_rect((-50, -50), (300, 300), filled=True, colour=(1, 0, 0, 1)) renderer.pop_clip() ``` ## What was removed `new_layer()` is gone. It used to be the escape hatch for "force a flush" when the renderer reordered draws within a batch. The renderer no longer reorders, so the escape hatch isn't needed. If you find old code that calls `renderer.new_layer()`, delete the call: submission order suffices. The 4 legacy per-kind staging lists (`_fill_verts`, `_line_verts`, `_text_verts`, `_textured_quads`) and the batch tuple format (`_get_batches`, `_flush_batch`, `_batches`) are also gone. The single `_ops` list is the canonical mutable state.