diff --git a/.gitignore b/.gitignore index b2d7b6da..317a3cec 100644 --- a/.gitignore +++ b/.gitignore @@ -147,3 +147,6 @@ bin # End of https://www.gitignore.io/api/linux,cpp,c,cmake,macos,opengl imgui.ini + +# Planning +docs/plans/ diff --git a/crates/lambda-rs-platform/src/wgpu/gpu.rs b/crates/lambda-rs-platform/src/wgpu/gpu.rs index 6e80961b..67c39516 100644 --- a/crates/lambda-rs-platform/src/wgpu/gpu.rs +++ b/crates/lambda-rs-platform/src/wgpu/gpu.rs @@ -66,6 +66,8 @@ impl std::ops::BitOr for Features { pub struct GpuLimits { pub max_uniform_buffer_binding_size: u64, pub max_bind_groups: u32, + pub max_vertex_buffers: u32, + pub max_vertex_attributes: u32, pub min_uniform_buffer_offset_alignment: u32, } @@ -250,6 +252,8 @@ impl Gpu { .max_uniform_buffer_binding_size .into(), max_bind_groups: self.limits.max_bind_groups, + max_vertex_buffers: self.limits.max_vertex_buffers, + max_vertex_attributes: self.limits.max_vertex_attributes, min_uniform_buffer_offset_alignment: self .limits .min_uniform_buffer_offset_alignment, diff --git a/crates/lambda-rs-platform/src/wgpu/render_pass.rs b/crates/lambda-rs-platform/src/wgpu/render_pass.rs index bbaed6c6..b153f072 100644 --- a/crates/lambda-rs-platform/src/wgpu/render_pass.rs +++ b/crates/lambda-rs-platform/src/wgpu/render_pass.rs @@ -187,8 +187,12 @@ impl<'a> RenderPass<'a> { } /// Issue a non-indexed draw over a vertex range. - pub fn draw(&mut self, vertices: std::ops::Range) { - self.raw.draw(vertices, 0..1); + pub fn draw( + &mut self, + vertices: std::ops::Range, + instances: std::ops::Range, + ) { + self.raw.draw(vertices, instances); } /// Issue an indexed draw with a base vertex applied. @@ -196,8 +200,9 @@ impl<'a> RenderPass<'a> { &mut self, indices: std::ops::Range, base_vertex: i32, + instances: std::ops::Range, ) { - self.raw.draw_indexed(indices, base_vertex, 0..1); + self.raw.draw_indexed(indices, base_vertex, instances); } } diff --git a/crates/lambda-rs/examples/indexed_multi_vertex_buffers.rs b/crates/lambda-rs/examples/indexed_multi_vertex_buffers.rs new file mode 100644 index 00000000..ba5283a6 --- /dev/null +++ b/crates/lambda-rs/examples/indexed_multi_vertex_buffers.rs @@ -0,0 +1,354 @@ +#![allow(clippy::needless_return)] + +//! Example: Indexed draw with multiple vertex buffers. +//! +//! This example renders a simple quad composed from two triangles using +//! separate vertex buffers for positions and colors plus a 16-bit index +//! buffer. It exercises `BindVertexBuffer` for multiple slots and +//! `BindIndexBuffer`/`DrawIndexed` in the render command stream. + +use lambda::{ + component::Component, + events::WindowEvent, + logging, + render::{ + buffer::{ + BufferBuilder, + BufferType, + Properties, + Usage, + }, + command::{ + IndexFormat, + RenderCommand, + }, + pipeline::{ + CullingMode, + RenderPipelineBuilder, + }, + render_pass::RenderPassBuilder, + shader::{ + Shader, + ShaderBuilder, + ShaderKind, + VirtualShader, + }, + vertex::{ + ColorFormat, + VertexAttribute, + VertexElement, + }, + viewport, + RenderContext, + ResourceId, + }, + runtime::start_runtime, + runtimes::{ + application::ComponentResult, + ApplicationRuntime, + ApplicationRuntimeBuilder, + }, +}; + +// ------------------------------ SHADER SOURCE -------------------------------- + +const VERTEX_SHADER_SOURCE: &str = r#" +#version 450 + +layout (location = 0) in vec3 vertex_position; +layout (location = 1) in vec3 vertex_color; + +layout (location = 0) out vec3 frag_color; + +void main() { + gl_Position = vec4(vertex_position, 1.0); + frag_color = vertex_color; +} + +"#; + +const FRAGMENT_SHADER_SOURCE: &str = r#" +#version 450 + +layout (location = 0) in vec3 frag_color; +layout (location = 0) out vec4 fragment_color; + +void main() { + fragment_color = vec4(frag_color, 1.0); +} + +"#; + +// ------------------------------- VERTEX TYPES -------------------------------- + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +struct PositionVertex { + position: [f32; 3], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +struct ColorVertex { + color: [f32; 3], +} + +// --------------------------------- COMPONENT --------------------------------- + +pub struct IndexedMultiBufferExample { + vertex_shader: Shader, + fragment_shader: Shader, + render_pass_id: Option, + render_pipeline_id: Option, + index_buffer_id: Option, + index_count: u32, + width: u32, + height: u32, +} + +impl Component for IndexedMultiBufferExample { + fn on_attach( + &mut self, + render_context: &mut RenderContext, + ) -> Result { + let render_pass = RenderPassBuilder::new().build(render_context); + + // Quad composed from two triangles in clip space. + let positions: Vec = vec![ + PositionVertex { + position: [-0.5, -0.5, 0.0], + }, + PositionVertex { + position: [0.5, -0.5, 0.0], + }, + PositionVertex { + position: [0.5, 0.5, 0.0], + }, + PositionVertex { + position: [-0.5, 0.5, 0.0], + }, + ]; + + let colors: Vec = vec![ + ColorVertex { + color: [1.0, 0.0, 0.0], + }, + ColorVertex { + color: [0.0, 1.0, 0.0], + }, + ColorVertex { + color: [0.0, 0.0, 1.0], + }, + ColorVertex { + color: [1.0, 1.0, 1.0], + }, + ]; + + let indices: Vec = vec![0, 1, 2, 2, 3, 0]; + let index_count = indices.len() as u32; + + // Build vertex buffers for positions and colors in separate slots. + let position_buffer = BufferBuilder::new() + .with_usage(Usage::VERTEX) + .with_properties(Properties::DEVICE_LOCAL) + .with_buffer_type(BufferType::Vertex) + .with_label("indexed-positions") + .build(render_context, positions) + .map_err(|e| e.to_string())?; + + let color_buffer = BufferBuilder::new() + .with_usage(Usage::VERTEX) + .with_properties(Properties::DEVICE_LOCAL) + .with_buffer_type(BufferType::Vertex) + .with_label("indexed-colors") + .build(render_context, colors) + .map_err(|e| e.to_string())?; + + // Build a 16-bit index buffer. + let index_buffer = BufferBuilder::new() + .with_usage(Usage::INDEX) + .with_properties(Properties::DEVICE_LOCAL) + .with_buffer_type(BufferType::Index) + .with_label("indexed-indices") + .build(render_context, indices) + .map_err(|e| e.to_string())?; + + let pipeline = RenderPipelineBuilder::new() + .with_culling(CullingMode::Back) + .with_buffer( + position_buffer, + vec![VertexAttribute { + location: 0, + offset: 0, + element: VertexElement { + format: ColorFormat::Rgb32Sfloat, + offset: 0, + }, + }], + ) + .with_buffer( + color_buffer, + vec![VertexAttribute { + location: 1, + offset: 0, + element: VertexElement { + format: ColorFormat::Rgb32Sfloat, + offset: 0, + }, + }], + ) + .build( + render_context, + &render_pass, + &self.vertex_shader, + Some(&self.fragment_shader), + ); + + self.render_pass_id = Some(render_context.attach_render_pass(render_pass)); + self.render_pipeline_id = Some(render_context.attach_pipeline(pipeline)); + self.index_buffer_id = Some(render_context.attach_buffer(index_buffer)); + self.index_count = index_count; + + logging::info!("Indexed multi-vertex-buffer example attached"); + return Ok(ComponentResult::Success); + } + + fn on_detach( + &mut self, + _render_context: &mut RenderContext, + ) -> Result { + logging::info!("Indexed multi-vertex-buffer example detached"); + return Ok(ComponentResult::Success); + } + + fn on_event( + &mut self, + event: lambda::events::Events, + ) -> Result { + match event { + lambda::events::Events::Window { event, .. } => match event { + WindowEvent::Resize { width, height } => { + self.width = width; + self.height = height; + logging::info!("Window resized to {}x{}", width, height); + } + _ => {} + }, + _ => {} + } + return Ok(ComponentResult::Success); + } + + fn on_update( + &mut self, + _last_frame: &std::time::Duration, + ) -> Result { + return Ok(ComponentResult::Success); + } + + fn on_render( + &mut self, + _render_context: &mut RenderContext, + ) -> Vec { + let viewport = + viewport::ViewportBuilder::new().build(self.width, self.height); + + let render_pass_id = self + .render_pass_id + .expect("Render pass must be attached before rendering"); + let pipeline_id = self + .render_pipeline_id + .expect("Pipeline must be attached before rendering"); + let index_buffer_id = self + .index_buffer_id + .expect("Index buffer must be attached before rendering"); + + return vec![ + RenderCommand::BeginRenderPass { + render_pass: render_pass_id, + viewport: viewport.clone(), + }, + RenderCommand::SetPipeline { + pipeline: pipeline_id, + }, + RenderCommand::SetViewports { + start_at: 0, + viewports: vec![viewport.clone()], + }, + RenderCommand::SetScissors { + start_at: 0, + viewports: vec![viewport.clone()], + }, + RenderCommand::BindVertexBuffer { + pipeline: pipeline_id, + buffer: 0, + }, + RenderCommand::BindVertexBuffer { + pipeline: pipeline_id, + buffer: 1, + }, + RenderCommand::BindIndexBuffer { + buffer: index_buffer_id, + format: IndexFormat::Uint16, + }, + RenderCommand::DrawIndexed { + indices: 0..self.index_count, + base_vertex: 0, + instances: 0..1, + }, + RenderCommand::EndRenderPass, + ]; + } +} + +impl Default for IndexedMultiBufferExample { + fn default() -> Self { + let vertex_virtual_shader = VirtualShader::Source { + source: VERTEX_SHADER_SOURCE.to_string(), + kind: ShaderKind::Vertex, + entry_point: "main".to_string(), + name: "indexed_multi_vertex_buffers".to_string(), + }; + + let fragment_virtual_shader = VirtualShader::Source { + source: FRAGMENT_SHADER_SOURCE.to_string(), + kind: ShaderKind::Fragment, + entry_point: "main".to_string(), + name: "indexed_multi_vertex_buffers".to_string(), + }; + + let mut builder = ShaderBuilder::new(); + let vertex_shader = builder.build(vertex_virtual_shader); + let fragment_shader = builder.build(fragment_virtual_shader); + + return Self { + vertex_shader, + fragment_shader, + render_pass_id: None, + render_pipeline_id: None, + index_buffer_id: None, + index_count: 0, + width: 800, + height: 600, + }; + } +} + +fn main() { + let runtime = + ApplicationRuntimeBuilder::new("Indexed Multi-Vertex-Buffer Example") + .with_window_configured_as(move |window_builder| { + return window_builder + .with_dimensions(800, 600) + .with_name("Indexed Multi-Vertex-Buffer Example"); + }) + .with_renderer_configured_as(|renderer_builder| { + return renderer_builder.with_render_timeout(1_000_000_000); + }) + .with_component(move |runtime, example: IndexedMultiBufferExample| { + return (runtime, example); + }) + .build(); + + start_runtime(runtime); +} diff --git a/crates/lambda-rs/examples/push_constants.rs b/crates/lambda-rs/examples/push_constants.rs index dda5eeb0..15ea6712 100644 --- a/crates/lambda-rs/examples/push_constants.rs +++ b/crates/lambda-rs/examples/push_constants.rs @@ -302,6 +302,7 @@ impl Component for PushConstantsExample { }, RenderCommand::Draw { vertices: 0..self.mesh.as_ref().unwrap().vertices().len() as u32, + instances: 0..1, }, RenderCommand::EndRenderPass, ]; diff --git a/crates/lambda-rs/examples/reflective_room.rs b/crates/lambda-rs/examples/reflective_room.rs index 54fa7f27..c7ac60fa 100644 --- a/crates/lambda-rs/examples/reflective_room.rs +++ b/crates/lambda-rs/examples/reflective_room.rs @@ -417,6 +417,7 @@ impl Component for ReflectiveRoomExample { }); cmds.push(RenderCommand::Draw { vertices: 0..floor_vertex_count, + instances: 0..1, }); cmds.push(RenderCommand::EndRenderPass); } @@ -450,6 +451,7 @@ impl Component for ReflectiveRoomExample { }); cmds.push(RenderCommand::Draw { vertices: 0..cube_vertex_count, + instances: 0..1, }); } } @@ -476,6 +478,7 @@ impl Component for ReflectiveRoomExample { }); cmds.push(RenderCommand::Draw { vertices: 0..floor_vertex_count, + instances: 0..1, }); } @@ -499,6 +502,7 @@ impl Component for ReflectiveRoomExample { }); cmds.push(RenderCommand::Draw { vertices: 0..cube_vertex_count, + instances: 0..1, }); cmds.push(RenderCommand::EndRenderPass); diff --git a/crates/lambda-rs/examples/textured_cube.rs b/crates/lambda-rs/examples/textured_cube.rs index 4822da42..4e012f56 100644 --- a/crates/lambda-rs/examples/textured_cube.rs +++ b/crates/lambda-rs/examples/textured_cube.rs @@ -458,6 +458,7 @@ impl Component for TexturedCubeExample { }, RenderCommand::Draw { vertices: 0..mesh_len, + instances: 0..1, }, RenderCommand::EndRenderPass, ]; diff --git a/crates/lambda-rs/examples/textured_quad.rs b/crates/lambda-rs/examples/textured_quad.rs index 1049a1b9..e4da291f 100644 --- a/crates/lambda-rs/examples/textured_quad.rs +++ b/crates/lambda-rs/examples/textured_quad.rs @@ -305,7 +305,10 @@ impl Component for TexturedQuadExample { pipeline: self.render_pipeline.expect("pipeline not set"), buffer: 0, }); - commands.push(RenderCommand::Draw { vertices: 0..6 }); + commands.push(RenderCommand::Draw { + vertices: 0..6, + instances: 0..1, + }); commands.push(RenderCommand::EndRenderPass); return commands; } diff --git a/crates/lambda-rs/examples/triangle.rs b/crates/lambda-rs/examples/triangle.rs index 2ee9aa59..650a26a3 100644 --- a/crates/lambda-rs/examples/triangle.rs +++ b/crates/lambda-rs/examples/triangle.rs @@ -161,7 +161,10 @@ impl Component for DemoComponent { start_at: 0, viewports: vec![viewport.clone()], }, - RenderCommand::Draw { vertices: 0..3 }, + RenderCommand::Draw { + vertices: 0..3, + instances: 0..1, + }, RenderCommand::EndRenderPass, ]; } diff --git a/crates/lambda-rs/examples/triangles.rs b/crates/lambda-rs/examples/triangles.rs index 9815e012..0b4b8e6b 100644 --- a/crates/lambda-rs/examples/triangles.rs +++ b/crates/lambda-rs/examples/triangles.rs @@ -144,7 +144,10 @@ impl Component for TrianglesComponent { offset: 0, bytes: Vec::from(push_constants_to_bytes(triangle)), }); - commands.push(RenderCommand::Draw { vertices: 0..3 }); + commands.push(RenderCommand::Draw { + vertices: 0..3, + instances: 0..1, + }); } commands.push(RenderCommand::EndRenderPass); diff --git a/crates/lambda-rs/examples/uniform_buffer_triangle.rs b/crates/lambda-rs/examples/uniform_buffer_triangle.rs index cf99917f..ab74c6cf 100644 --- a/crates/lambda-rs/examples/uniform_buffer_triangle.rs +++ b/crates/lambda-rs/examples/uniform_buffer_triangle.rs @@ -352,6 +352,7 @@ impl Component for UniformBufferExample { }, RenderCommand::Draw { vertices: 0..self.mesh.as_ref().unwrap().vertices().len() as u32, + instances: 0..1, }, RenderCommand::EndRenderPass, ]; diff --git a/crates/lambda-rs/src/render/command.rs b/crates/lambda-rs/src/render/command.rs index df4d12b7..be31ba69 100644 --- a/crates/lambda-rs/src/render/command.rs +++ b/crates/lambda-rs/src/render/command.rs @@ -12,6 +12,24 @@ use super::{ viewport::Viewport, }; +/// Engine-level index format for indexed drawing. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum IndexFormat { + Uint16, + Uint32, +} + +impl IndexFormat { + pub(crate) fn to_platform( + self, + ) -> lambda_platform::wgpu::buffer::IndexFormat { + match self { + IndexFormat::Uint16 => lambda_platform::wgpu::buffer::IndexFormat::Uint16, + IndexFormat::Uint32 => lambda_platform::wgpu::buffer::IndexFormat::Uint32, + } + } +} + /// Commands recorded and executed by the `RenderContext` to produce a frame. /// /// Order and validity are enforced by the encoder where possible. Invalid @@ -64,14 +82,18 @@ pub enum RenderCommand { /// Resource identifier returned by `RenderContext::attach_buffer`. buffer: super::ResourceId, /// Index format for this buffer. - format: lambda_platform::wgpu::buffer::IndexFormat, + format: IndexFormat, }, /// Issue a non‑indexed draw for the provided vertex range. - Draw { vertices: Range }, + Draw { + vertices: Range, + instances: Range, + }, /// Issue an indexed draw for the provided index range. DrawIndexed { indices: Range, base_vertex: i32, + instances: Range, }, /// Bind a previously created bind group to a set index with optional @@ -87,3 +109,23 @@ pub enum RenderCommand { dynamic_offsets: Vec, }, } + +#[cfg(test)] +mod tests { + use super::IndexFormat; + + #[test] + fn index_format_maps_to_platform() { + let u16_platform = IndexFormat::Uint16.to_platform(); + let u32_platform = IndexFormat::Uint32.to_platform(); + + assert_eq!( + u16_platform, + lambda_platform::wgpu::buffer::IndexFormat::Uint16 + ); + assert_eq!( + u32_platform, + lambda_platform::wgpu::buffer::IndexFormat::Uint32 + ); + } +} diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index d8e23ce5..e499a2f8 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -380,6 +380,16 @@ impl RenderContext { return self.gpu.limits().max_bind_groups; } + /// Device limit: maximum number of vertex buffers that can be bound. + pub fn limit_max_vertex_buffers(&self) -> u32 { + return self.gpu.limits().max_vertex_buffers; + } + + /// Device limit: maximum number of vertex attributes that can be declared. + pub fn limit_max_vertex_attributes(&self) -> u32 { + return self.gpu.limits().max_vertex_attributes; + } + /// Device limit: required alignment in bytes for dynamic uniform buffer offsets. pub fn limit_min_uniform_buffer_offset_alignment(&self) -> u32 { return self.gpu.limits().min_uniform_buffer_offset_alignment; @@ -597,19 +607,26 @@ impl RenderContext { } /// Encode a single render pass and consume commands until `EndRenderPass`. - fn encode_pass( + fn encode_pass( &self, pass: &mut platform::render_pass::RenderPass<'_>, uses_color: bool, pass_has_depth_attachment: bool, pass_has_stencil: bool, initial_viewport: viewport::Viewport, - commands: &mut I, + commands: &mut Commands, ) -> Result<(), RenderError> where - I: Iterator, + Commands: Iterator, { Self::apply_viewport(pass, &initial_viewport); + + #[cfg(any(debug_assertions, feature = "render-validation-encoder",))] + let mut current_pipeline: Option = None; + + #[cfg(any(debug_assertions, feature = "render-validation-encoder",))] + let mut bound_index_buffer: Option<(usize, u32)> = None; + // De-duplicate advisories within this pass #[cfg(any( debug_assertions, @@ -617,6 +634,7 @@ impl RenderContext { feature = "render-validation-stencil", ))] let mut warned_no_stencil_for_pipeline: HashSet = HashSet::new(); + #[cfg(any( debug_assertions, feature = "render-validation-depth", @@ -637,6 +655,7 @@ impl RenderContext { "Unknown pipeline {pipeline}" )); })?; + // Validate pass/pipeline compatibility before deferring to the platform. #[cfg(any( debug_assertions, @@ -668,6 +687,14 @@ impl RenderContext { ))); } } + + // Keep track of the current pipeline to ensure that draw calls + // happen only after a pipeline is set. + #[cfg(any(debug_assertions, feature = "render-validation-encoder",))] + { + current_pipeline = Some(pipeline); + } + // Advisory checks to help reason about stencil/depth behavior. #[cfg(any( debug_assertions, @@ -687,6 +714,8 @@ impl RenderContext { ); util::warn_once(&key, &msg); } + + // Warn if pipeline uses stencil but pass has no stencil ops. if !pass_has_stencil && pipeline_ref.uses_stencil() { let label = pipeline_ref.pipeline().label().unwrap_or("unnamed"); let key = format!("stencil:pass_no_operations:{}", label); @@ -696,6 +725,8 @@ impl RenderContext { ); util::warn_once(&key, &msg); } + + // Warn if pass has depth attachment but pipeline does not test/write depth. if pass_has_depth_attachment && !pipeline_ref.expects_depth_stencil() && warned_no_depth_for_pipeline.insert(pipeline) @@ -709,6 +740,7 @@ impl RenderContext { util::warn_once(&key, &msg); } } + pass.set_pipeline(pipeline_ref.pipeline()); } RenderCommand::SetViewports { viewports, .. } => { @@ -769,15 +801,44 @@ impl RenderContext { buffer )); })?; - // Soft validation: encourage correct logical type. - if buffer_ref.buffer_type() != buffer::BufferType::Index { - logging::warn!( - "Binding buffer id {} as index but logical type is {:?}", - buffer, - buffer_ref.buffer_type() - ); + #[cfg(any(debug_assertions, feature = "render-validation-encoder",))] + { + if buffer_ref.buffer_type() != buffer::BufferType::Index { + return Err(RenderError::Configuration(format!( + "Binding buffer id {} as index but logical type is {:?}; expected BufferType::Index", + buffer, + buffer_ref.buffer_type() + ))); + } + let element_size = match format { + command::IndexFormat::Uint16 => 2u64, + command::IndexFormat::Uint32 => 4u64, + }; + let stride = buffer_ref.stride(); + if stride != element_size { + return Err(RenderError::Configuration(format!( + "Index buffer id {} has element stride {} bytes but BindIndexBuffer specified format {:?} ({} bytes)", + buffer, + stride, + format, + element_size + ))); + } + let buffer_size = buffer_ref.raw().size(); + if buffer_size % element_size != 0 { + return Err(RenderError::Configuration(format!( + "Index buffer id {} has size {} bytes which is not a multiple of element size {} for format {:?}", + buffer, + buffer_size, + element_size, + format + ))); + } + let max_indices = + (buffer_size / element_size).min(u32::MAX as u64) as u32; + bound_index_buffer = Some((buffer, max_indices)); } - pass.set_index_buffer(buffer_ref.raw(), format); + pass.set_index_buffer(buffer_ref.raw(), format.to_platform()); } RenderCommand::PushConstants { pipeline, @@ -798,14 +859,62 @@ impl RenderContext { }; pass.set_push_constants(stage, offset, slice); } - RenderCommand::Draw { vertices } => { - pass.draw(vertices); + RenderCommand::Draw { + vertices, + instances, + } => { + #[cfg(any(debug_assertions, feature = "render-validation-encoder",))] + { + if current_pipeline.is_none() { + return Err(RenderError::Configuration( + "Draw command encountered before any pipeline was set in this render pass" + .to_string(), + )); + } + } + pass.draw(vertices, instances); } RenderCommand::DrawIndexed { indices, base_vertex, + instances, } => { - pass.draw_indexed(indices, base_vertex); + #[cfg(any(debug_assertions, feature = "render-validation-encoder",))] + { + if current_pipeline.is_none() { + return Err(RenderError::Configuration( + "DrawIndexed command encountered before any pipeline was set in this render pass" + .to_string(), + )); + } + let (buffer_id, max_indices) = match bound_index_buffer { + Some(state) => state, + None => { + return Err(RenderError::Configuration( + "DrawIndexed command encountered without a bound index buffer in this render pass" + .to_string(), + )); + } + }; + if indices.start > indices.end { + return Err(RenderError::Configuration(format!( + "DrawIndexed index range start {} is greater than end {} for index buffer id {}", + indices.start, + indices.end, + buffer_id + ))); + } + if indices.end > max_indices { + return Err(RenderError::Configuration(format!( + "DrawIndexed index range {}..{} exceeds bound index buffer id {} capacity {}", + indices.start, + indices.end, + buffer_id, + max_indices + ))); + } + } + pass.draw_indexed(indices, base_vertex, instances); } RenderCommand::BeginRenderPass { .. } => { return Err(RenderError::Configuration( diff --git a/crates/lambda-rs/src/render/pipeline.rs b/crates/lambda-rs/src/render/pipeline.rs index 9b40e1c0..0d032ae5 100644 --- a/crates/lambda-rs/src/render/pipeline.rs +++ b/crates/lambda-rs/src/render/pipeline.rs @@ -38,7 +38,10 @@ use logging; use super::{ bind, - buffer::Buffer, + buffer::{ + Buffer, + BufferType, + }, render_pass::RenderPass, shader::Shader, texture, @@ -266,6 +269,15 @@ impl RenderPipelineBuilder { buffer: Buffer, attributes: Vec, ) -> Self { + #[cfg(any(debug_assertions, feature = "render-validation-encoder",))] + { + if buffer.buffer_type() != BufferType::Vertex { + logging::error!( + "RenderPipelineBuilder::with_buffer called with a non-vertex buffer type {:?}; expected BufferType::Vertex", + buffer.buffer_type() + ); + } + } self.bindings.push(BufferBinding { buffer: Rc::new(buffer), attributes, @@ -409,6 +421,43 @@ impl RenderPipelineBuilder { max_bind_groups ); + // Vertex buffer slot and attribute count limit checks. + let max_vertex_buffers = render_context.limit_max_vertex_buffers() as usize; + if self.bindings.len() > max_vertex_buffers { + logging::error!( + "Pipeline declares {} vertex buffers, exceeds device max {}", + self.bindings.len(), + max_vertex_buffers + ); + } + debug_assert!( + self.bindings.len() <= max_vertex_buffers, + "Pipeline declares {} vertex buffers, exceeds device max {}", + self.bindings.len(), + max_vertex_buffers + ); + + let total_vertex_attributes: usize = self + .bindings + .iter() + .map(|binding| binding.attributes.len()) + .sum(); + let max_vertex_attributes = + render_context.limit_max_vertex_attributes() as usize; + if total_vertex_attributes > max_vertex_attributes { + logging::error!( + "Pipeline declares {} vertex attributes across all vertex buffers, exceeds device max {}", + total_vertex_attributes, + max_vertex_attributes + ); + } + debug_assert!( + total_vertex_attributes <= max_vertex_attributes, + "Pipeline declares {} vertex attributes across all vertex buffers, exceeds device max {}", + total_vertex_attributes, + max_vertex_attributes + ); + // Pipeline layout via platform let bgl_platform: Vec<&lambda_platform::wgpu::bind::BindGroupLayout> = self .bind_group_layouts diff --git a/docs/specs/indexed-draws-and-multiple-vertex-buffers.md b/docs/specs/indexed-draws-and-multiple-vertex-buffers.md new file mode 100644 index 00000000..701a7b70 --- /dev/null +++ b/docs/specs/indexed-draws-and-multiple-vertex-buffers.md @@ -0,0 +1,282 @@ +--- +title: "Indexed Draws and Multiple Vertex Buffers" +document_id: "indexed-draws-multiple-vertex-buffers-2025-11-22" +status: "draft" +created: "2025-11-22T00:00:00Z" +last_updated: "2025-11-23T00:00:00Z" +version: "0.1.2" +engine_workspace_version: "2023.1.30" +wgpu_version: "26.0.1" +shader_backend_default: "naga" +winit_version: "0.29.10" +repo_commit: "db7fa78d143e5ff69028413fe86c948be9ba76ee" +owners: ["lambda-sh"] +reviewers: ["engine", "rendering"] +tags: ["spec", "rendering", "vertex-input", "indexed-draws"] +--- + +# Indexed Draws and Multiple Vertex Buffers + +## Table of Contents +- [Summary](#summary) +- [Scope](#scope) +- [Terminology](#terminology) +- [Architecture Overview](#architecture-overview) +- [Design](#design) + - [API Surface](#api-surface) + - [Behavior](#behavior) + - [Validation and Errors](#validation-and-errors) +- [Constraints and Rules](#constraints-and-rules) +- [Performance Considerations](#performance-considerations) +- [Requirements Checklist](#requirements-checklist) +- [Verification and Testing](#verification-and-testing) +- [Compatibility and Migration](#compatibility-and-migration) +- [Changelog](#changelog) + +## Summary + +- Specify indexed draw commands and index buffers as first-class concepts in the high-level rendering API while keeping `wgpu` types encapsulated in `lambda-rs-platform`. +- Define multiple vertex buffer support in `RenderPipelineBuilder` and the render command stream, enabling flexible vertex input layouts while preserving simple defaults. +- Rationale: Many scenes require indexed meshes and split vertex streams (for example, positions and per-instance data) for memory efficiency and performance; supporting these paths in a structured way aligns with modern GPU APIs. + +## Scope + +### Goals + +- Expose index buffers and indexed draw commands on the high-level API without leaking backend types. +- Support multiple vertex buffers per pipeline and bind them by slot in the render command stream. +- Maintain compatibility with existing single-vertex-buffer and non-indexed draw workflows. +- Integrate with existing validation features for encoder- and pass-level safety checks. + +### Non-Goals + +- Multi-draw indirect and indirect command buffers. +- Per-instance rate vertex buffer layouts and advanced instancing patterns beyond simple instance ranges. +- New mesh or asset file formats; mesh containers may evolve separately. + +## Terminology + +- Vertex buffer: A GPU buffer containing per-vertex attribute streams consumed by the vertex shader. +- Vertex attribute: A contiguous region within a vertex buffer that feeds a shader input at a specific `location`. +- Vertex buffer slot: A numbered binding point (starting at zero) used to bind a vertex buffer layout and buffer to a pipeline. +- Index buffer: A GPU buffer containing compact integer indices that reference vertices in one or more vertex buffers. +- Index format: The integer width used to encode indices in an index buffer (16-bit or 32-bit unsigned). +- Indexed draw: A draw call that emits primitives by reading indices from an index buffer instead of traversing vertices linearly. + +## Architecture Overview + +- High-level layer (`lambda-rs`) + - `RenderPipelineBuilder` declares one or more vertex buffers with associated `VertexAttribute` descriptions. + - The render command stream includes commands to bind vertex buffers, bind an index buffer, and issue indexed or non-indexed draws. + - Public types represent index formats and buffer classifications; backend-specific details remain internal to `lambda-rs-platform`. +- Platform layer (`lambda-rs-platform`) + - Wraps `wgpu` vertex and index buffer usage, index formats, and render pass draw calls. + - Provides `set_vertex_buffer`, `set_index_buffer`, `draw`, and `draw_indexed` helpers around the `wgpu::RenderPass`. +- Data flow + +``` +App Code + └── lambda-rs + ├── BufferBuilder (BufferType::Vertex / BufferType::Index) + ├── RenderPipelineBuilder::with_buffer(..) + └── RenderCommand::{BindVertexBuffer, BindIndexBuffer, Draw, DrawIndexed} + └── RenderContext encoder + └── lambda-rs-platform (vertex/index binding, draw calls) + └── wgpu::RenderPass::{set_vertex_buffer, set_index_buffer, draw, draw_indexed} +``` + +## Design + +### API Surface + +- Platform layer (`lambda-rs-platform`, module `lambda_platform::wgpu::buffer`) + - Types + - `enum IndexFormat { Uint16, Uint32 }` (maps to `wgpu::IndexFormat`). + - Render pass integration (module `lambda_platform::wgpu::render_pass`) + - `fn set_vertex_buffer(&mut self, slot: u32, buffer: &buffer::Buffer)`. + - `fn set_index_buffer(&mut self, buffer: &buffer::Buffer, format: buffer::IndexFormat)`. + - `fn draw(&mut self, vertices: Range)`. + - `fn draw_indexed(&mut self, indices: Range, base_vertex: i32)`. +- High-level layer (`lambda-rs`) + - Buffer classification and creation (`lambda::render::buffer`) + - `enum BufferType { Vertex, Index, Uniform, Storage }`. + - `struct Buffer { buffer_type: BufferType, stride: u64, .. }`. + - `struct Usage(platform_buffer::Usage)` with `Usage::VERTEX` and `Usage::INDEX` for vertex and index buffers. + - `struct BufferBuilder`: + - `fn with_usage(self, usage: Usage) -> Self`. + - `fn with_buffer_type(self, buffer_type: BufferType) -> Self`. + - `fn build(self, render_context: &mut RenderContext, data: Vec) -> Result`. + - `fn build_from_mesh(self, render_context: &mut RenderContext, mesh: Mesh) -> Result` for convenience. + - Vertex input definition (`lambda::render::vertex` and `lambda::render::pipeline`) + - `struct VertexAttribute { location: u32, offset: u32, element: VertexElement }`. + - `RenderPipelineBuilder::with_buffer(buffer: Buffer, attributes: Vec) -> Self`: + - Each call declares a vertex buffer slot with a stride and attribute list. + - Slots are assigned in call order starting at zero. + - Index format and commands (`lambda::render::command`) + - Introduce an engine-level index format: + - `enum IndexFormat { Uint16, Uint32 }`. + - Render commands: + - `BindVertexBuffer { pipeline: ResourceId, buffer: u32 }` binds a vertex buffer slot declared on the pipeline. + - `BindIndexBuffer { buffer: ResourceId, format: IndexFormat }` binds an index buffer with a specific format. + - `Draw { vertices: Range, instances: Range }` issues a non-indexed draw with an explicit instance range. + - `DrawIndexed { indices: Range, base_vertex: i32, instances: Range }` issues an indexed draw with a signed base vertex and explicit instance range. + +Example (high-level usage) + +```rust +use lambda::render::{ + buffer::{BufferBuilder, BufferType, Usage}, + command::RenderCommand, + pipeline::RenderPipelineBuilder, +}; + +let vertex_buffer_positions = BufferBuilder::new() + .with_usage(Usage::VERTEX) + .with_buffer_type(BufferType::Vertex) + .with_label("positions") + .build(&mut render_context, position_vertices)?; + +let vertex_buffer_colors = BufferBuilder::new() + .with_usage(Usage::VERTEX) + .with_buffer_type(BufferType::Vertex) + .with_label("colors") + .build(&mut render_context, color_vertices)?; + +let index_buffer = BufferBuilder::new() + .with_usage(Usage::INDEX) + .with_buffer_type(BufferType::Index) + .with_label("indices") + .build(&mut render_context, indices)?; + +let pipeline = RenderPipelineBuilder::new() + .with_buffer(vertex_buffer_positions, position_attributes) + .with_buffer(vertex_buffer_colors, color_attributes) + .build(&mut render_context, &render_pass, &vertex_shader, Some(&fragment_shader)); + +let commands = vec![ + RenderCommand::BeginRenderPass { render_pass: render_pass_id, viewport }, + RenderCommand::SetPipeline { pipeline: pipeline_id }, + RenderCommand::BindVertexBuffer { pipeline: pipeline_id, buffer: 0 }, + RenderCommand::BindVertexBuffer { pipeline: pipeline_id, buffer: 1 }, + RenderCommand::BindIndexBuffer { buffer: index_buffer_id, format: IndexFormat::Uint16 }, + RenderCommand::DrawIndexed { indices: 0..index_count, base_vertex: 0, instances: 0..1 }, + RenderCommand::EndRenderPass, +]; +``` + +### Behavior + +- Vertex buffers and slots + - `RenderPipelineBuilder` collects vertex buffer layouts in call order. Slot `0` corresponds to the first `with_buffer` call, slot `1` to the second, and so on. + - A vertex buffer MUST have `BufferType::Vertex` and include at least one `VertexAttribute`. + - Attributes in a slot MUST have distinct `location` values and their offsets MUST be within the buffer stride. + - The render context uses the pipeline’s recorded buffer layouts to derive vertex buffer strides and attribute descriptors when building the platform pipeline. +- Index buffers and formats + - Index buffers are created with `BufferType::Index` and `Usage::INDEX`. The element type of the index buffer data MUST match the `IndexFormat` passed in `BindIndexBuffer`. + - Supported index formats are `Uint16` and `Uint32`. The engine MUST reject or log an error for any attempt to use unsupported formats. + - At most one index buffer is considered active at a time for a render pass; subsequent `BindIndexBuffer` commands replace the previous binding. +- Draw commands + - `Draw` uses the currently bound vertex buffers and does not require an index buffer. The `instances` range controls how many instances are emitted; a default of `0..1` preserves prior single-instance behavior. + - `DrawIndexed` uses the currently bound index buffer and vertex buffers. If no index buffer is bound when `DrawIndexed` is encoded, the engine MUST treat this as a configuration error. The `instances` range controls how many instances are emitted. + - `Draw` and `DrawIndexed` operate only inside an active render pass and after a pipeline has been set. + - `base_vertex` shifts the vertex index computed from each index; this is forwarded to the platform layer and MUST be preserved exactly. +- Feature flags + - Existing validation flags in `lambda-rs` apply: + - `render-validation-pass-compat`: validates pipeline versus pass configuration (color/depth/stencil) and MAY be extended to ensure vertex buffer usage is compatible with the pass. + - `render-validation-encoder`: enables per-command checks for correct binding order and buffer types. + - Debug builds (`debug_assertions`) MAY enable additional checks regardless of feature flags. + +### Validation and Errors + +- Command ordering + - `BeginRenderPass` MUST precede any `SetPipeline`, `BindVertexBuffer`, `BindIndexBuffer`, `Draw`, or `DrawIndexed` commands. + - `EndRenderPass` MUST terminate the pass; commands after `EndRenderPass` that require a pass are considered invalid. +- Vertex buffer binding + - The vertex buffer slot index in `BindVertexBuffer` MUST be less than the number of buffers declared on the pipeline. + - When `render-validation-encoder` is enabled, the engine SHOULD: + - Reject or log a configuration error if the bound buffer’s `BufferType` is not `Vertex`. + - Emit diagnostics when a slot is bound more than once without being used for any draws. +- Index buffer binding + - `BindIndexBuffer` MUST reference a buffer created with `BufferType::Index`. With `render-validation-encoder` enabled, the engine SHOULD validate this invariant and log a clear error when it is violated. + - The `IndexFormat` passed in the command MUST match the element width used when creating the index data. Mismatches are undefined at the GPU level; the engine SHOULD provide validation where type information is available. +- Draw calls + - With `render-validation-encoder` enabled, the engine SHOULD: + - Validate that a pipeline and (for `DrawIndexed`) an index buffer are bound before encoding draw commands. + - Validate that the index range does not exceed the logical count of indices provided at buffer creation when that length is tracked. + - Errors SHOULD be reported with actionable messages, including the command index and pipeline label where available. + +## Constraints and Rules + +- Index buffers + - Index data MUST be tightly packed according to the selected `IndexFormat` (no gaps or padding between indices). + - Index buffers MUST be created with `Usage::INDEX`; additional usages MAY be combined when necessary (for example, `Usage::INDEX | Usage::STORAGE`). + - Index ranges for `DrawIndexed` MUST be expressed in units of indices, not bytes. +- Vertex buffers + - Each vertex buffer slot has exactly one stride in bytes, derived from the vertex type used at creation time. + - Attribute offsets and formats MUST respect platform alignment requirements for the underlying GPU. + - When multiple vertex buffers are in use, attributes for a given shader input `location` MUST appear in exactly one slot. +- Backend limits + - The number of vertex buffer slots per pipeline MUST NOT exceed the device limit exposed through `lambda-rs-platform`. + - The engine MUST clamp or reject configurations that exceed the maximum number of vertex buffers or vertex attributes per pipeline, logging errors when validation features are enabled. + +## Performance Considerations + +- Prefer 16-bit indices where possible. + - Rationale: `Uint16` indices reduce index buffer size and memory bandwidth relative to `Uint32` when the vertex count permits it. +- Group data by update frequency across vertex buffers. + - Rationale: Placing static geometry in one buffer and frequently updated per-instance data in another allows partial updates and reduces bandwidth. +- Avoid redundant buffer bindings. + - Rationale: Rebinding the same buffer and slot between draws increases command traffic and validation cost without changing the GPU state. +- Use contiguous index ranges for cache-friendly access. + - Rationale: Locality in index sequences improves vertex cache efficiency on the GPU and reduces redundant vertex shader invocations. + +## Requirements Checklist + +- Functionality + - [x] Indexed draws (`DrawIndexed`) integrated with the render command stream. + - [x] Index buffers (`BufferType::Index`, `Usage::INDEX`) created and bound through `BindIndexBuffer`. + - [x] Multiple vertex buffers declared on `RenderPipelineBuilder` and bound via `BindVertexBuffer`. + - [x] Edge cases handled for missing bindings and invalid ranges. +- API Surface + - [x] Engine-level `IndexFormat` type exposed without leaking backend enums. + - [x] Buffer builders support vertex and index usage/configuration. + - [x] Render commands align with pipeline vertex buffer declarations. +- Validation and Errors + - [x] Encoder ordering checks for pipeline, vertex buffers, and index buffers. + - [x] Index range and buffer-type validation under `render-validation-encoder`. + - [x] Device limit checks for vertex buffer slots and attributes. +- Performance + - [x] Guidance documented in this section. + - [ ] Indexed and non-indexed paths characterized for representative meshes. +- Documentation and Examples + - [x] Example scene using indexed draws and multiple vertex buffers (for + example, a mesh with separate position and color streams). + +## Verification and Testing + +- Unit Tests + - Validate mapping from engine-level `IndexFormat` to platform index formats. + - Validate command ordering rules and encoder-side checks when `render-validation-encoder` is enabled. + - Validate vertex buffer slot bounds and buffer type checks in the encoder. + - Commands: `cargo test -p lambda-rs -- --nocapture` +- Integration Tests and Examples + - Example: an indexed mesh rendered with two vertex buffers (positions and colors) and a 16-bit index buffer. + - Example: fall back to non-indexed draws for simple meshes to ensure both paths remain valid. + - Commands: + - `cargo run -p lambda-rs --example indexed_multi_vertex_buffers` + - `cargo test --workspace` +- Manual Checks (optional) + - Render a mesh with and without indexed draws and visually confirm identical geometry. + - Toggle between single and multiple vertex buffer configurations for the same mesh and confirm consistent output. + +## Compatibility and Migration + +- Existing pipelines that declare a single vertex buffer and use non-indexed draws remain valid; no code changes are required. +- Introducing an engine-level `IndexFormat` type for `BindIndexBuffer` is a source-compatible change when a re-export is provided for the previous platform type; call sites SHOULD migrate to the new type explicitly. +- Applications that currently emulate indexed draws by duplicating vertices MAY migrate to indexed meshes to reduce vertex buffer size and improve cache utilization. + +## Changelog + +- 2025-11-22 (v0.1.0) — Initial draft specifying indexed draws and multiple vertex buffers, including API surface, behavior, validation hooks, performance guidance, and verification plan. +- 2025-11-22 (v0.1.1) — Added engine-level `IndexFormat`, instance ranges to `Draw`/`DrawIndexed`, encoder-side validation for pipeline and index buffer bindings, and updated requirements checklist. +- 2025-11-23 (v0.1.2) — Added index buffer stride and range validation, device limit checks for vertex buffer slots and attributes, an example scene with indexed draws and multiple vertex buffers, and updated the requirements checklist. diff --git a/docs/tutorials/indexed-draws-and-multiple-vertex-buffers.md b/docs/tutorials/indexed-draws-and-multiple-vertex-buffers.md new file mode 100644 index 00000000..94bca1e5 --- /dev/null +++ b/docs/tutorials/indexed-draws-and-multiple-vertex-buffers.md @@ -0,0 +1,488 @@ +--- +title: "Indexed Draws and Multiple Vertex Buffers" +document_id: "indexed-draws-multiple-vertex-buffers-tutorial-2025-11-22" +status: "draft" +created: "2025-11-22T00:00:00Z" +last_updated: "2025-11-23T00:00:00Z" +version: "0.2.0" +engine_workspace_version: "2023.1.30" +wgpu_version: "26.0.1" +shader_backend_default: "naga" +winit_version: "0.29.10" +repo_commit: "db7fa78d143e5ff69028413fe86c948be9ba76ee" +owners: ["lambda-sh"] +reviewers: ["engine", "rendering"] +tags: ["tutorial", "graphics", "indexed-draws", "vertex-buffers", "rust", "wgpu"] +--- + +## Overview +This tutorial constructs a small scene rendered with indexed geometry and multiple vertex buffers. The example separates per-vertex positions from per-vertex colors and draws the result using the engine’s high-level buffer and command builders. + +Reference implementation: `crates/lambda-rs/examples/indexed_multi_vertex_buffers.rs`. + +## Table of Contents +- [Overview](#overview) +- [Goals](#goals) +- [Prerequisites](#prerequisites) +- [Requirements and Constraints](#requirements-and-constraints) +- [Data Flow](#data-flow) +- [Implementation Steps](#implementation-steps) + - [Step 1 — Shaders and Vertex Types](#step-1) + - [Step 2 — Component State and Shader Construction](#step-2) + - [Step 3 — Render Pass, Vertex Data, Buffers, and Pipeline](#step-3) + - [Step 4 — Resize Handling and Updates](#step-4) + - [Step 5 — Render Commands and Runtime Entry Point](#step-5) +- [Validation](#validation) +- [Notes](#notes) +- [Conclusion](#conclusion) +- [Exercises](#exercises) +- [Changelog](#changelog) + +## Goals + +- Render indexed geometry using an index buffer and `DrawIndexed` commands. +- Demonstrate multiple vertex buffers bound to a single pipeline (for example, positions in one buffer and colors in another). +- Show how the engine associates vertex buffer slots with shader locations and how those slots are bound via render commands. +- Reinforce correct buffer usage flags and buffer types for vertex and index data. + +## Prerequisites + +- The workspace builds successfully: `cargo build --workspace`. +- Familiarity with the basics of the runtime and component model. +- Ability to run examples and tutorials: + - `cargo run --example minimal` + - `cargo run -p lambda-rs --example textured_quad` + +## Requirements and Constraints + +- Vertex buffer layouts in the pipeline MUST match shader attribute `location` and format declarations. +- Index data MUST be tightly packed in the chosen index format (`u16` or `u32`) and the `IndexFormat` passed to the command MUST correspond to the element width. +- Vertex buffers used for geometry MUST be created with `Usage::VERTEX` and an appropriate `BufferType` value; index buffers MUST use `Usage::INDEX` and `BufferType::Index`. +- Draw commands that rely on indexed geometry MUST bind a pipeline, vertex buffers, and an index buffer inside an active render pass before issuing `DrawIndexed`. + +## Data Flow + +- CPU prepares vertex data (positions, colors) and index data. +- Buffers and pipeline layouts are constructed using the builder APIs. +- At render time, commands bind the pipeline, vertex buffers, and index buffer, then issue indexed draws. + +ASCII diagram + +``` +CPU (positions, colors, indices) + │ upload via BufferBuilder + ▼ +Vertex Buffers (slots 0, 1) Index Buffer + │ │ + ├───────────────┐ │ + ▼ ▼ ▼ +RenderPipeline (vertex layouts) RenderCommand::BindIndexBuffer + │ +RenderCommand::{BindVertexBuffer, DrawIndexed} + │ +Render Pass → wgpu::RenderPass::{set_vertex_buffer, set_index_buffer, draw_indexed} +``` + +## Implementation Steps + +### Step 1 — Shaders and Vertex Types +Step 1 defines the shader interface and vertex structures used by the example. The shaders consume positions and colors at locations `0` and `1`, and the vertex types store those attributes as three-component floating-point arrays. + +```glsl +#version 450 + +layout (location = 0) in vec3 vertex_position; +layout (location = 1) in vec3 vertex_color; + +layout (location = 0) out vec3 frag_color; + +void main() { + gl_Position = vec4(vertex_position, 1.0); + frag_color = vertex_color; +} +``` + +```glsl +#version 450 + +layout (location = 0) in vec3 frag_color; +layout (location = 0) out vec4 fragment_color; + +void main() { + fragment_color = vec4(frag_color, 1.0); +} +``` + +```rust +#[repr(C)] +#[derive(Clone, Copy, Debug)] +struct PositionVertex { + position: [f32; 3], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +struct ColorVertex { + color: [f32; 3], +} +``` + +The shader `location` qualifiers match the vertex buffer layouts declared on the pipeline, and the `PositionVertex` and `ColorVertex` types mirror the `vec3` inputs as `[f32; 3]` arrays in Rust. + +### Step 2 — Component State and Shader Construction +Step 2 introduces the `IndexedMultiBufferExample` component and its `Default` implementation, which builds shader objects from the GLSL source and initializes render-resource fields and window dimensions. + +```rust +use lambda::render::{ + shader::{ + Shader, + ShaderBuilder, + ShaderKind, + VirtualShader, + }, + RenderContext, + ResourceId, +}; + +pub struct IndexedMultiBufferExample { + vertex_shader: Shader, + fragment_shader: Shader, + render_pass_id: Option, + render_pipeline_id: Option, + index_buffer_id: Option, + index_count: u32, + width: u32, + height: u32, +} + +impl Default for IndexedMultiBufferExample { + fn default() -> Self { + let vertex_virtual_shader = VirtualShader::Source { + source: VERTEX_SHADER_SOURCE.to_string(), + kind: ShaderKind::Vertex, + entry_point: "main".to_string(), + name: "indexed_multi_vertex_buffers".to_string(), + }; + + let fragment_virtual_shader = VirtualShader::Source { + source: FRAGMENT_SHADER_SOURCE.to_string(), + kind: ShaderKind::Fragment, + entry_point: "main".to_string(), + name: "indexed_multi_vertex_buffers".to_string(), + }; + + let mut builder = ShaderBuilder::new(); + let vertex_shader = builder.build(vertex_virtual_shader); + let fragment_shader = builder.build(fragment_virtual_shader); + + return Self { + vertex_shader, + fragment_shader, + render_pass_id: None, + render_pipeline_id: None, + index_buffer_id: None, + index_count: 0, + width: 800, + height: 600, + }; + } +} +``` + +This `Default` implementation ensures that the component has valid shaders and initial dimensions before it attaches to the render context. + +### Step 3 — Render Pass, Vertex Data, Buffers, and Pipeline +Step 3 implements `on_attach` to create the render pass, vertex and index data, GPU buffers, and the render pipeline, then attaches them to the `RenderContext`. + +```rust +use lambda::render::buffer::{ + BufferBuilder, + BufferType, + Properties, + Usage, +}; + +use lambda::render::{ + pipeline::{ + CullingMode, + RenderPipelineBuilder, + }, + render_pass::RenderPassBuilder, + vertex::{ + ColorFormat, + VertexAttribute, + VertexElement, + }, +}; + +fn on_attach( + &mut self, + render_context: &mut RenderContext, +) -> Result { + let render_pass = RenderPassBuilder::new().build(render_context); + + let positions: Vec = vec![ + PositionVertex { + position: [-0.5, -0.5, 0.0], + }, + PositionVertex { + position: [0.5, -0.5, 0.0], + }, + PositionVertex { + position: [0.5, 0.5, 0.0], + }, + PositionVertex { + position: [-0.5, 0.5, 0.0], + }, + ]; + + let colors: Vec = vec![ + ColorVertex { + color: [1.0, 0.0, 0.0], + }, + ColorVertex { + color: [0.0, 1.0, 0.0], + }, + ColorVertex { + color: [0.0, 0.0, 1.0], + }, + ColorVertex { + color: [1.0, 1.0, 1.0], + }, + ]; + + let indices: Vec = vec![0, 1, 2, 2, 3, 0]; + let index_count = indices.len() as u32; + + let position_buffer = BufferBuilder::new() + .with_usage(Usage::VERTEX) + .with_properties(Properties::DEVICE_LOCAL) + .with_buffer_type(BufferType::Vertex) + .with_label("indexed-positions") + .build(render_context, positions) + .map_err(|error| error.to_string())?; + + let color_buffer = BufferBuilder::new() + .with_usage(Usage::VERTEX) + .with_properties(Properties::DEVICE_LOCAL) + .with_buffer_type(BufferType::Vertex) + .with_label("indexed-colors") + .build(render_context, colors) + .map_err(|error| error.to_string())?; + + let index_buffer = BufferBuilder::new() + .with_usage(Usage::INDEX) + .with_properties(Properties::DEVICE_LOCAL) + .with_buffer_type(BufferType::Index) + .with_label("indexed-indices") + .build(render_context, indices) + .map_err(|error| error.to_string())?; + + let pipeline = RenderPipelineBuilder::new() + .with_culling(CullingMode::Back) + .with_buffer( + position_buffer, + vec![VertexAttribute { + location: 0, + offset: 0, + element: VertexElement { + format: ColorFormat::Rgb32Sfloat, + offset: 0, + }, + }], + ) + .with_buffer( + color_buffer, + vec![VertexAttribute { + location: 1, + offset: 0, + element: VertexElement { + format: ColorFormat::Rgb32Sfloat, + offset: 0, + }, + }], + ) + .build( + render_context, + &render_pass, + &self.vertex_shader, + Some(&self.fragment_shader), + ); + + self.render_pass_id = Some(render_context.attach_render_pass(render_pass)); + self.render_pipeline_id = Some(render_context.attach_pipeline(pipeline)); + self.index_buffer_id = Some(render_context.attach_buffer(index_buffer)); + self.index_count = index_count; + + logging::info!("Indexed multi-vertex-buffer example attached"); + return Ok(ComponentResult::Success); +} +``` + +The pipeline uses the order of `with_buffer` calls to assign vertex buffer slots. The first buffer occupies slot `0` and provides attributes at location `0`, while the second buffer occupies slot `1` and provides attributes at location `1`. The component stores attached resource identifiers and the index count for use during rendering. + +### Step 4 — Resize Handling and Updates +Step 4 wires window resize events into the component and implements detach and update hooks. The resize handler keeps `width` and `height` in sync with the window so that the viewport matches the surface size. + +```rust +fn on_detach( + &mut self, + _render_context: &mut RenderContext, +) -> Result { + logging::info!("Indexed multi-vertex-buffer example detached"); + return Ok(ComponentResult::Success); +} + +fn on_event( + &mut self, + event: lambda::events::Events, +) -> Result { + match event { + lambda::events::Events::Window { event, .. } => match event { + WindowEvent::Resize { width, height } => { + self.width = width; + self.height = height; + logging::info!("Window resized to {}x{}", width, height); + } + _ => {} + }, + _ => {} + } + return Ok(ComponentResult::Success); +} + +fn on_update( + &mut self, + _last_frame: &std::time::Duration, +) -> Result { + return Ok(ComponentResult::Success); +} +``` + +The resize path is the only dynamic input in this example. The update hook is a no-op that keeps the component interface aligned with other examples. + +### Step 5 — Render Commands and Runtime Entry Point +Step 5 records the render commands that bind the pipeline, vertex buffers, and index buffer, and then wires the component into the runtime as a windowed application. + +```rust +use lambda::render::{ + command::{ + IndexFormat, + RenderCommand, + }, + viewport, +}; + +fn on_render( + &mut self, + _render_context: &mut RenderContext, +) -> Vec { + let viewport = + viewport::ViewportBuilder::new().build(self.width, self.height); + + let render_pass_id = self + .render_pass_id + .expect("Render pass must be attached before rendering"); + let pipeline_id = self + .render_pipeline_id + .expect("Pipeline must be attached before rendering"); + let index_buffer_id = self + .index_buffer_id + .expect("Index buffer must be attached before rendering"); + + return vec![ + RenderCommand::BeginRenderPass { + render_pass: render_pass_id, + viewport: viewport.clone(), + }, + RenderCommand::SetPipeline { + pipeline: pipeline_id, + }, + RenderCommand::SetViewports { + start_at: 0, + viewports: vec![viewport.clone()], + }, + RenderCommand::SetScissors { + start_at: 0, + viewports: vec![viewport.clone()], + }, + RenderCommand::BindVertexBuffer { + pipeline: pipeline_id, + buffer: 0, + }, + RenderCommand::BindVertexBuffer { + pipeline: pipeline_id, + buffer: 1, + }, + RenderCommand::BindIndexBuffer { + buffer: index_buffer_id, + format: IndexFormat::Uint16, + }, + RenderCommand::DrawIndexed { + indices: 0..self.index_count, + base_vertex: 0, + instances: 0..1, + }, + RenderCommand::EndRenderPass, + ]; +} + +use lambda::{ + component::Component, + runtime::start_runtime, + runtimes::application::ApplicationRuntimeBuilder, +}; + +fn main() { + let runtime = + ApplicationRuntimeBuilder::new("Indexed Multi-Vertex-Buffer Example") + .with_window_configured_as(move |window_builder| { + return window_builder + .with_dimensions(800, 600) + .with_name("Indexed Multi-Vertex-Buffer Example"); + }) + .with_renderer_configured_as(|renderer_builder| { + return renderer_builder.with_render_timeout(1_000_000_000); + }) + .with_component(move |runtime, example: IndexedMultiBufferExample| { + return (runtime, example); + }) + .build(); + + start_runtime(runtime); +} +``` + +The commands bind both vertex buffers and the index buffer before issuing `DrawIndexed`. The runtime builder configures the window and renderer and installs the component so that the engine drives `on_attach`, `on_event`, `on_update`, and `on_render` each frame. + +## Validation + +- Commands: + - `cargo run -p lambda-rs --example indexed_multi_vertex_buffers` + - `cargo test -p lambda-rs -- --nocapture` +- Expected behavior: + - Indexed geometry renders correctly with distinct colors sourced from a second vertex buffer. + - Switching between indexed and non-indexed paths SHOULD produce visually consistent geometry for the same mesh. + +## Notes + +- Vertex buffer slot indices MUST remain consistent between pipeline construction and binding commands. +- Index ranges for `DrawIndexed` MUST remain within the logical count of indices provided when the index buffer is created. +- Validation features such as `render-validation-encoder` SHOULD be enabled when developing new render paths to catch ordering and binding issues early. + +## Conclusion + +This tutorial demonstrates how indexed draws and multiple vertex buffers combine to render geometry efficiently while keeping the engine’s high-level abstractions simple. The example in `crates/lambda-rs/examples/indexed_multi_vertex_buffers.rs` provides a concrete reference for applications that require indexed meshes or split vertex streams. + +## Exercises + +- Extend the example to render multiple meshes that share the same index buffer but use different color data. +- Add a per-instance transform buffer and demonstrate instanced drawing by varying transforms while reusing positions and indices. +- Introduce a wireframe mode that uses the same vertex and index buffers but modifies pipeline state to emphasize edge connectivity. +- Experiment with `u16` versus `u32` indices and measure the effect on buffer size and performance for larger meshes. +- Add a debug mode that binds an incorrect index format intentionally and observe how validation features report the error. + +## Changelog + +- 2025-11-23 (v0.2.0) — Filled in the implementation steps for the indexed draws and multiple vertex buffers tutorial and aligned the narrative with the `indexed_multi_vertex_buffers` example. +- 2025-11-22 (v0.1.0) — Initial skeleton for the indexed draws and multiple vertex buffers tutorial; content placeholders added for future implementation. diff --git a/docs/tutorials/reflective-room.md b/docs/tutorials/reflective-room.md index b4d7e4eb..7cdc74a2 100644 --- a/docs/tutorials/reflective-room.md +++ b/docs/tutorials/reflective-room.md @@ -342,7 +342,7 @@ cmds.push(RenderCommand::SetPipeline { pipeline: pipe_floor_mask }); cmds.push(RenderCommand::SetStencilReference { reference: 1 }); cmds.push(RenderCommand::BindVertexBuffer { pipeline: pipe_floor_mask, buffer: 0 }); cmds.push(RenderCommand::PushConstants { pipeline: pipe_floor_mask, stage: PipelineStage::VERTEX, offset: 0, bytes: Vec::from(push_constants_to_words(&PushConstant { mvp: mvp_floor.transpose(), model: model_floor.transpose() })) }); -cmds.push(RenderCommand::Draw { vertices: 0..floor_vertex_count }); +cmds.push(RenderCommand::Draw { vertices: 0..floor_vertex_count, instances: 0..1 }); cmds.push(RenderCommand::EndRenderPass); // Pass 2: reflected cube (stencil test == 1) @@ -351,19 +351,19 @@ cmds.push(RenderCommand::SetPipeline { pipeline: pipe_reflected }); cmds.push(RenderCommand::SetStencilReference { reference: 1 }); cmds.push(RenderCommand::BindVertexBuffer { pipeline: pipe_reflected, buffer: 0 }); cmds.push(RenderCommand::PushConstants { pipeline: pipe_reflected, stage: PipelineStage::VERTEX, offset: 0, bytes: Vec::from(push_constants_to_words(&PushConstant { mvp: mvp_reflect.transpose(), model: model_reflect.transpose() })) }); -cmds.push(RenderCommand::Draw { vertices: 0..cube_vertex_count }); +cmds.push(RenderCommand::Draw { vertices: 0..cube_vertex_count, instances: 0..1 }); // Pass 3: floor visual (tinted) cmds.push(RenderCommand::SetPipeline { pipeline: pipe_floor_visual }); cmds.push(RenderCommand::BindVertexBuffer { pipeline: pipe_floor_visual, buffer: 0 }); cmds.push(RenderCommand::PushConstants { pipeline: pipe_floor_visual, stage: PipelineStage::VERTEX, offset: 0, bytes: Vec::from(push_constants_to_words(&PushConstant { mvp: mvp_floor.transpose(), model: model_floor.transpose() })) }); -cmds.push(RenderCommand::Draw { vertices: 0..floor_vertex_count }); +cmds.push(RenderCommand::Draw { vertices: 0..floor_vertex_count, instances: 0..1 }); // Pass 4: normal cube cmds.push(RenderCommand::SetPipeline { pipeline: pipe_normal }); cmds.push(RenderCommand::BindVertexBuffer { pipeline: pipe_normal, buffer: 0 }); cmds.push(RenderCommand::PushConstants { pipeline: pipe_normal, stage: PipelineStage::VERTEX, offset: 0, bytes: Vec::from(push_constants_to_words(&PushConstant { mvp: mvp.transpose(), model: model.transpose() })) }); -cmds.push(RenderCommand::Draw { vertices: 0..cube_vertex_count }); +cmds.push(RenderCommand::Draw { vertices: 0..cube_vertex_count, instances: 0..1 }); cmds.push(RenderCommand::EndRenderPass); ``` diff --git a/docs/tutorials/textured-cube.md b/docs/tutorials/textured-cube.md index 45cc52f1..6506ac3b 100644 --- a/docs/tutorials/textured-cube.md +++ b/docs/tutorials/textured-cube.md @@ -457,7 +457,7 @@ let commands = vec![ model: model.transpose(), })), }, - RenderCommand::Draw { vertices: 0..mesh_len }, + RenderCommand::Draw { vertices: 0..mesh_len, instances: 0..1 }, RenderCommand::EndRenderPass, ]; ``` diff --git a/docs/tutorials/textured-quad.md b/docs/tutorials/textured-quad.md index d9ffbe82..cc828dcc 100644 --- a/docs/tutorials/textured-quad.md +++ b/docs/tutorials/textured-quad.md @@ -410,7 +410,7 @@ let commands = vec![ pipeline: self.render_pipeline.expect("pipeline not set"), buffer: 0, }, - RenderCommand::Draw { vertices: 0..6 }, + RenderCommand::Draw { vertices: 0..6, instances: 0..1 }, RenderCommand::EndRenderPass, ]; ``` diff --git a/docs/tutorials/uniform-buffers.md b/docs/tutorials/uniform-buffers.md index c399cd5b..e779b397 100644 --- a/docs/tutorials/uniform-buffers.md +++ b/docs/tutorials/uniform-buffers.md @@ -336,7 +336,7 @@ let commands = vec![ RenderCommand::SetScissors { start_at: 0, viewports: vec![viewport.clone()] }, RenderCommand::BindVertexBuffer { pipeline: pipeline_id, buffer: 0 }, RenderCommand::SetBindGroup { set: 0, group: bind_group_id, dynamic_offsets: vec![] }, - RenderCommand::Draw { vertices: 0..mesh.vertices().len() as u32 }, + RenderCommand::Draw { vertices: 0..mesh.vertices().len() as u32, instances: 0..1 }, RenderCommand::EndRenderPass, ]; ``` diff --git a/tools/obj_loader/src/main.rs b/tools/obj_loader/src/main.rs index 6801ca74..566810c2 100644 --- a/tools/obj_loader/src/main.rs +++ b/tools/obj_loader/src/main.rs @@ -308,6 +308,7 @@ impl Component for ObjLoader { }, RenderCommand::Draw { vertices: 0..self.mesh.as_ref().unwrap().vertices().len() as u32, + instances: 0..1, }, RenderCommand::EndRenderPass, ];