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.

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.

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.