gpu is a frame intent orchestrator. It is not a UI toolkit, scene graph, particle engine, or raw backend API. Hosts decide what visible things mean; gpu turns already-decided drawing and dispatch intent into coherent backend work.
host producers
UI / compositor / scene / particles / debug
-> draw and dispatch packets
gpu
-> frame IR
-> validation
-> legal planning
-> backend lowering
backend
-> Vulkan / D3D11 / D3D12 / OpenGL / future software
The authoritative runtime direction is a planned-IR executor: validation produces diagnostics, planning orders commands, and backend lowering walks the planned IR to issue backend operations. A gated backend-native planned draw path now handles materialized IR buffers, render passes, shaders, graphics pipelines, descriptor sets, direct indexed draws, direct non-indexed draws, true per-instance vertex buffers for pipelines that declare an instance layout, single-command indirect indexed/non-indexed draws, supported borrowed imported mesh buffers, lit imported meshes, imported rect clips, the generated 2D rect/line/circle slice, generated textured quads with concrete texture IDs, and generated glyph quads with texture-backed materials. The current runtime translator that maps remaining generated/imported producer forms into legacy 2D/3D renderer calls is migration-only. New backend parity work must not add feature-specific translator branches unless the branch is a temporary, tracked bridge with a removal path.
Runtime lowering must not fail silently. If a command cannot be lowered, gpu records a structured diagnostic with the command handle and reason, then fails submission or mode classification explicitly.
Host owns semantic visibility and policy:
gpu owns render-mechanical work:
Rule: if visibility requires meaning, host handles it. If visibility only requires geometry/render state, gpu may handle it.
Backends must preserve GPU object lifetime until every command list that references the object has completed. Destroying a buffer, texture, shader, pipeline, framebuffer attachment, or temporary upload allocation while frames are in flight retires the backend object behind the completed fence that covers the already-recorded work. wait_idle drains all retired objects.
CPU mapping is only valid for host-visible buffers. GPU-local/default buffers are not mappable through map_buffer or get_buffer_mapped; backends must report that explicitly and return nil. Host-visible upload buffers may stay persistently mapped when the backend memory model allows it.
compute_barrier(ctx) is a conservative/global in-frame barrier for compute-written storage/UAV data consumed by later backend work in the same frame. Its consumer scope is later compute shader reads/writes, vertex/fragment shader reads, sampled texture reads, vertex/index/instance fetch, and indirect command reads. It is intentionally over-synchronized for correctness; it is not a render-target transition substitute, CPU readback fence, cross-frame lifetime rule, or feedback-loop validator. Backends that lack a complete storage descriptor path must report that separately through capabilities or diagnostics instead of pretending the barrier enables unsupported binding.
Resource access diagnostics use backend-neutral access categories: color/depth attachment write, sampled texture read, storage read/write, vertex/index/instance stream read, indirect argument read, copy/upload write, and CPU map/read/write. Permanent invariant: a command must not sample a concrete Resource_Handle that is also bound as a render target in the same pass. Current limitation: sampled-input feedback diagnostics are exact only when sampled inputs are represented as Resource_Handles; descriptor/material paths that only carry runtime texture IDs are marked as missing access metadata until those bindings become resource-backed. Current limitation: the runtime treats the default present pass as final, so offscreen work after present is rejected with a command diagnostic rather than silently reordered.
Indirect draw argument buffers use the backend-neutral Indirect_Draw_Args and Indirect_Draw_Indexed_Args ABI. The layouts are intentionally byte-identical to Vulkan, D3D11, D3D12, and OpenGL single-command indirect arguments: 16 bytes for non-indexed draws and 20 bytes for indexed draws. In IR, packet.instances.indirect is the argument-buffer resource, packet.instances.offset is the byte offset, packet.instances.stride is zero-or-ABI-size for indirect arguments, and packet.instances.count is limited to one command in the current executor.
Per-instance vertex streams use the normal packet.instances.resource field, not the indirect argument field. A planned instance-buffer draw requires a graphics pipeline with has_instance_layout = true, a nonzero packet.instances.stride, and a prepared instance buffer. Slot 0 is the per-vertex stream; slot 1 is the per-instance stream bound with packet.instances.offset and packet.instances.stride. Vulkan, D3D11, D3D12, and OpenGL all lower the same backend-neutral Vertex_Binding.input_rate and Vertex_Attribute.binding metadata; OpenGL uses the DSA vertex-attrib-binding/divisor path internally. Indirect draws may also bind an instance stream: the indirect buffer controls counts/firsts, while the instance buffer controls per-instance attributes.
D3D12 runtime fence behavior cannot be executed by Linux CI. D3D12 lifetime and transition work is accepted by static backend checks, pure fence-order tests where available, and explicit Windows runtime verification debt.
Current backend sync matrix:
| Backend | Compute barrier scope | Sampled target feedback | Runtime proof |
|---|---|---|---|
| Vulkan | Conservative compute-write barrier for later shader reads/writes, sampled texture reads, vertex/index/instance fetch, and indirect command reads. | Render pass final layouts come from target usage; descriptor layout is caller-declared. | Static checks now; device smoke gate still needed. |
| D3D11 | Immediate context resolves many SRV/RTV/UAV conflicts by auto-unbinding and debug warnings. Manual hazard cleanup needs slot tracking before it is safe. | No explicit image states; render-target/SRV overlap needs future warning-proof validation, not broad unbinds. | Windows runtime deferred. |
| D3D12 | Global UAV barrier for compute storage visibility; indirect draws transition argument buffers to INDIRECT_ARGUMENT. | Render/depth targets transition to write at pass begin and to shader-resource lazily when bound as sampled descriptors. | Windows runtime deferred. |
| OpenGL | Storage, texture-fetch, framebuffer, and command/indirect barriers are emitted after compute when requested. | Feedback-loop validation is a future diagnostics gap; current backend does not track attached texture handles at descriptor bind. | Linux device smoke gate still needed. |
Target API shape:
ctx := gpu.init_context(surface, width, height, title)
frame := gpu.begin_frame(ctx)
target := gpu.begin_target(frame, gpu.present_target(ctx), clear)
gpu.push_layer(frame, gpu.Layer_Desc{name = "ui", sort_base = 10_000, order = .Strict})
gpu.draw_packet(frame, packet)
gpu.pop_layer(frame)
gpu.push_layer(frame, gpu.Layer_Desc{name = "world", sort_base = 0, order = .Depth})
gpu.draw_mesh(frame, mesh)
gpu.draw_instances(frame, instances)
gpu.dispatch(frame, dispatch)
gpu.pop_layer(frame)
gpu.end_target(frame, target)
gpu.submit(frame)
Core handles:
Context_Handle: device/backend/resource owner.Frame_Handle: per-frame packet builder and transient lifetime root.Target_Handle: present surface or offscreen target.Layer_Handle: ordering scope within a target.Clip_Handle: scissor/clip scope.Material_Handle: shader, blend/depth/cull/topology, bindings.Geometry_Handle or Geometry_View: mesh/transient vertices/quad stream/glyph quads.Instance_View: instance buffer/range/count/stride or indirect args.Order_Mode :: enum {
Strict,
Layered,
Sortable,
Depth,
}
Planning precedence:
target -> pass/phase -> layer sort_base -> order mode -> sort_key/depth -> stable sequence
Invariants:
sequence when appended.Strict preserves sequence exactly. No texture/material batching may cross strict sequence.Layered preserves layer boundaries. Sorting inside a layer requires layer opt-in.Sortable may stable-sort by explicit sort_key and compatible material/state.Depth uses the host-provided depth sort_key and compatible material/state. gpu does not infer scene depth from semantic objects.sequence.Draw_Packet :: struct {
kind: Draw_Kind,
target: Target_Handle,
layer: Layer_Handle,
sort_key: u64,
sequence: u64,
bounds: Bounds3,
clip: Clip_Handle,
transform: Mat4,
material: Material_Handle,
geometry: Geometry_View,
instances: Instance_View,
flags: Draw_Flags,
order: Order_Mode,
}
Draw_Kind stays graphical and generic:
No particle emitter, UI widget, window, entity, or scene-node draw kind belongs in core gpu.
Compute is core because producers need it for simulation, culling, sorting, and buffer generation.
Dispatch_Packet :: struct {
target: Target_Handle,
layer: Layer_Handle,
sort_key: u64,
sequence: u64,
pipeline: Material_Handle,
groups: [3]u32,
resources: Resource_Binding_View,
push_data: []byte,
barrier_after: bool,
}
Dispatch may feed later draw packets through storage buffers or indirect draw buffers. Producer semantics stay outside core.
Particles:
gpu must not expose particle-specific system/config handles as final API.Text:
UI/compositor/scene:
Targets can be present or offscreen. Target desc owns size, format, load/store, clear values, and sample/present usage.
Clips start with rect/scissor. Mask clips are semantic Clip_Handle records whose source is geometry coverage rendered into stencil. The backend stencil contract exists below the IR: render pass descriptors carry stencil load/store, packed depth-stencil formats expose a stencil aspect, pipeline state carries front/back compare/ops/masks/ref, and each backend maps that state to native stencil state. Planned mask lowering is implemented as stencil-write plus stencil-test draw variants, and proof scenes exist for trusted readback validation.
gpu may drop packets fully outside target bounds or clip bounds. It must not decide semantic hidden state.
Backend capabilities must be queried through gpu-neutral flags:
Capabilities describe implemented gpu support, not raw device support. A backend must only report a flag or limit when the public gpu API, IR lowering, and backend implementation can execute that behavior end-to-end. Raw driver limits may be higher; those remain internal until the gpu executor can use them safely.
Current conservative limits:
max_color_targets reports the implemented color attachment count. Vulkan, D3D11, D3D12, and OpenGL currently report 8 because planned IR lowering, render pass/framebuffer arrays, pipeline color-attachment metadata, and backend RTV/FBO/render-pass binding support are implemented across all targets.max_push_constant_size reports the implemented push-constant byte limit for the selected backend. Pipeline and compute-shader creation must reject sizes above this limit.Multiple_Render_Targets, Indirect_Draws, and Stencil_Clips are implemented for the planned backend path. Stencil_Clips means the public IR, planned lowering, and backend stencil state contract are wired end-to-end; the runtime parity ledger remains open until trusted readback matrix rows pass across the required Vulkan, OpenGL, D3D11, and D3D12 backends. Texture-alpha/discard masking does not count as stencil clipping.Public API must not expose Vulkan/D3D-specific terms except through backend-only layers.
OpenGL is a backend target, not a separate public API. It requires OpenGL 4.5 core, uses the OpenGL GLSL 450 Luma target, keeps one thread-affine current context for backend calls, flattens (group, binding) resource coordinates into documented GL binding points, and reserves one UBO binding for push-constant emulation.
Clean break. Migrate examples and tests to new API in small green slices. Do not preserve old Raylib-style public API as compatibility surface. Temporary internal bridge code is allowed only to keep intermediate snapshots buildable and must be removed before final cleanup.
Required tests:
Visual/runtime proof: