Command Encoding
CommandEncoder records GPU rendering commands without executing them. It is completely lock-free and does not touch the GPU backend — you can create and fill encoders on any thread. The actual GPU work happens when you submit the commands through Frame::render() or RenderTarget::render().
Recording Commands
#![allow(unused)] fn main() { use goldy::{CommandEncoder, Color}; let mut encoder = CommandEncoder::new(); { let mut pass = encoder.begin_render_pass(); pass.clear(Color::CORNFLOWER_BLUE); pass.set_pipeline(&pipeline); pass.set_vertex_buffer(0, &vertices); pass.draw(0..3, 0..1); } // pass ends when dropped let commands = encoder.finish(); }
Render Pass
A RenderPass is a borrow of the encoder that groups drawing commands. It begins with begin_render_pass() and ends when the RenderPass value is dropped.
#![allow(unused)] fn main() { let mut encoder = CommandEncoder::new(); { let mut pass = encoder.begin_render_pass(); // all draw commands go here } }
Commands within a pass execute in recorded order.
Clearing
Clear the color attachment, the depth buffer, or both:
#![allow(unused)] fn main() { pass.clear(Color::BLACK); pass.clear_depth(1.0); // standard depth clear (far plane) pass.clear_depth(0.0); // reverse-Z depth clear }
Setting the Pipeline
Bind the active RenderPipeline. You can switch pipelines within the same pass.
#![allow(unused)] fn main() { pass.set_pipeline(&scene_pipeline); // ... draw scene ... pass.set_pipeline(&ui_pipeline); // ... draw UI ... }
Vertex and Index Buffers
Bind vertex data to a numbered slot. Both Buffer and BufferView are accepted — for pool-allocated views, the parent buffer and offset are resolved automatically.
#![allow(unused)] fn main() { pass.set_vertex_buffer(0, &vertex_buffer); // With an explicit additional offset: pass.set_vertex_buffer_offset(0, &vertex_buffer, byte_offset); }
Bind an index buffer for indexed drawing:
#![allow(unused)] fn main() { use goldy::IndexFormat; pass.set_index_buffer(&index_buffer, IndexFormat::Uint16); // With an additional offset: pass.set_index_buffer_offset(&index_buffer, byte_offset, IndexFormat::Uint32); }
Binding Resources
Goldy's bindless model passes resource indices to shaders through push constants. There are three binding methods:
Typed handles (preferred for new code) — each handle carries its BindlessCategory, enabling validation against shader reflection:
#![allow(unused)] fn main() { let tex = texture.bindless_handle().unwrap(); let samp = sampler.bindless_handle().unwrap(); pass.bind_resources_typed(&[tex, samp]); }
Buffer references — extracts bindless indices from Buffer objects:
#![allow(unused)] fn main() { pass.bind_resources(&[&uniform_buffer, &data_buffer]); }
Raw indices — for manual control or when mixing resource types:
#![allow(unused)] fn main() { let tex_idx = texture.bindless_index().unwrap(); let samp_idx = sampler.bindless_index().unwrap(); pass.bind_resources_raw(&[tex_idx, samp_idx]); }
Raw indices can also carry user scalars alongside bindless indices:
#![allow(unused)] fn main() { pass.bind_resources_raw_with_user( &[buf_idx, tex_idx], // bindless indices (region A) &[frame_number], // user scalars (region B) ); }
Draw Calls
draw
Draw non-indexed primitives:
#![allow(unused)] fn main() { // draw(vertex_range, instance_range) pass.draw(0..3, 0..1); // 3 vertices, 1 instance pass.draw(0..6, 0..10); // 6 vertices, 10 instances pass.draw(100..106, 0..1); // 6 vertices starting at vertex 100 }
draw_indexed
Draw indexed primitives. Requires a prior set_index_buffer() call.
#![allow(unused)] fn main() { // draw_indexed(index_range, base_vertex, instance_range) pass.draw_indexed(0..36, 0, 0..1); // base_vertex is added to each index before vertex fetch pass.draw_indexed(0..6, 1000, 0..1); // negative base_vertex is allowed pass.draw_indexed(0..3, -50, 0..1); }
draw_fullscreen
Draw a fullscreen triangle (3 vertices, no vertex buffer needed). Pair with vs_fullscreen_triangle() from goldy_exp.vertex or fullscreen_position()/fullscreen_uv() from goldy_exp.primitives.
#![allow(unused)] fn main() { pass.set_pipeline(&fullscreen_pipeline); pass.bind_resources(&[&uniform_buffer]); pass.draw_fullscreen(); }
This is more efficient than a fullscreen quad (3 vertices vs 6) and eliminates vertex buffer overhead entirely.
draw_quads
Draw N instanced quads (6 vertices each, no vertex buffer needed). The shader reads per-instance data from a buffer and uses quad_position() from goldy_exp.primitives to generate vertex positions.
#![allow(unused)] fn main() { pass.set_pipeline(&instanced_pipeline); pass.bind_resources(&[&instance_buffer]); pass.draw_quads(400); // draw 400 quads }
Submitting Commands
After recording, submit the encoder to a surface frame or render target:
#![allow(unused)] fn main() { // Surface presentation let frame = surface.begin()?; frame.render(encoder)?; frame.present()?; // Headless render target target.render(encoder)?; }
Complete Example
#![allow(unused)] fn main() { let mut encoder = CommandEncoder::new(); { let mut pass = encoder.begin_render_pass(); pass.clear(Color::BLACK); pass.clear_depth(1.0); // Draw opaque geometry pass.set_pipeline(&scene_pipeline); pass.set_vertex_buffer(0, &mesh_vertices); pass.set_index_buffer(&mesh_indices, IndexFormat::Uint32); pass.bind_resources(&[&camera_uniforms]); pass.draw_indexed(0..index_count, 0, 0..1); // Draw fullscreen post-process pass.set_pipeline(&post_pipeline); pass.bind_resources(&[&post_uniforms]); pass.draw_fullscreen(); } let frame = surface.begin()?; frame.render(encoder)?; frame.present()?; }
Best Practices
- Batch draws by pipeline. Pipeline switches are cheap but not free. Group objects that share the same pipeline.
- Clear once per pass. Issue
clear()at the start, then draw everything. - Use convenience methods.
draw_fullscreen()anddraw_quads()avoid unnecessary vertex buffer allocations. - Encode on any thread.
CommandEncoderis lock-free; build command buffers in parallel if needed.