From 84c73fbac8ce660827189fda1de96e50b5c8a9d5 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 24 Nov 2025 15:31:59 -0800 Subject: [PATCH 01/10] [add] initial instanced rendering specification. --- docs/specs/instanced-rendering.md | 368 ++++++++++++++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 docs/specs/instanced-rendering.md diff --git a/docs/specs/instanced-rendering.md b/docs/specs/instanced-rendering.md new file mode 100644 index 00000000..03ad017b --- /dev/null +++ b/docs/specs/instanced-rendering.md @@ -0,0 +1,368 @@ +--- +title: "Instanced Rendering" +document_id: "instanced-rendering-2025-11-23" +status: "draft" +created: "2025-11-23T00:00:00Z" +last_updated: "2025-11-23T00:00:00Z" +version: "0.1.0" +engine_workspace_version: "2023.1.30" +wgpu_version: "26.0.1" +shader_backend_default: "naga" +winit_version: "0.29.10" +repo_commit: "afabc0597de11b66124e937b4346923e25da3159" +owners: ["lambda-sh"] +reviewers: ["engine", "rendering"] +tags: ["spec", "rendering", "instancing", "vertex-input"] +--- + +# Instanced Rendering + +## 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 + +- Introduce instanced rendering as a first-class capability in the high-level + rendering API, enabling efficient drawing of many copies of shared geometry + with per-instance data while keeping `wgpu` types encapsulated in + `lambda-rs-platform`. +- Extend vertex input descriptions and draw commands to support instance-rate + vertex buffers and explicit instance ranges without breaking existing + non-instanced pipelines or render paths. +- Provide validation and feature-gated checks so instanced rendering failures + are actionable in development while imposing minimal overhead in release + builds. + +## Scope + +### Goals + +- Define instance-rate vertex buffer semantics in the high-level vertex input + model using engine-level types. +- Allow per-instance data (for example, transforms and colors) to be supplied + through buffers and consumed by vertex shaders using existing binding + patterns. +- Clarify the semantics of the `instances: Range` field on draw commands + and propagate instance ranges through the platform layer to `wgpu`. +- Add feature-gated validation for instance ranges, buffer usage, and + configuration ordering that integrates with existing rendering validation + features. +- Require at least one example and runnable scenario that demonstrates + instanced rendering with a visible, repeatable outcome. + +### Non-Goals + +- GPU-driven rendering techniques such as indirect draw buffers, automatic + culling, or multi-draw indirect command generation. +- New mesh or asset file formats; instanced rendering consumes existing vertex + and index data representations. +- Per-instance bind group fan-out or per-instance pipeline specialization. +- Scene graph or batching abstractions; instanced rendering remains a low-level + rendering primitive that higher-level systems MAY build upon separately. + +## Terminology + +- Instance: one logical copy of a drawable object emitted by a draw call. +- Instanced rendering: a draw technique where a single draw command emits many + instances of the same geometry, each instance optionally using distinct + per-instance data. +- Instance buffer: a vertex buffer whose attributes advance once per instance + rather than once per vertex. +- Step mode: a configuration that determines whether vertex input attributes + are stepped per vertex or per instance. +- Instance range: a `Range` that specifies the first instance index and + the number of instances to draw. +- Instance index: the built-in shader input that identifies the current + instance (for example, `@builtin(instance_index)` in `wgpu`-style shaders). + +## Architecture Overview + +- High-level layer (`lambda-rs`) + - `RenderPipelineBuilder` declares vertex buffer layouts, including a step + mode that describes whether a buffer is per-vertex or per-instance. + - The render command stream uses existing `Draw` and `DrawIndexed` + commands with an `instances: Range` field to control instance count + and first instance. + - Public types represent step modes and instance-aware vertex buffer layouts; + backend-specific details remain internal to `lambda-rs-platform`. +- Platform layer (`lambda-rs-platform`) + - Wraps `wgpu` vertex buffer layouts with an engine-level `VertexStepMode` + and forwards instance ranges to `wgpu::RenderPass::draw` and + `wgpu::RenderPass::draw_indexed`. + - Exposes draw helpers that accept both vertex or index ranges and instance + ranges, ensuring that engine-level commands can express instance counts and + first instance indices. +- Data flow + +``` +App Code + └── lambda-rs + ├── BufferBuilder (BufferType::Vertex) + ├── RenderPipelineBuilder::with_buffer(.., step_mode) + └── RenderCommand::{BindVertexBuffer, Draw, DrawIndexed} + └── RenderContext encoder + └── lambda-rs-platform (vertex layouts, draw calls) + └── wgpu::RenderPass::{set_vertex_buffer, + draw, draw_indexed} +``` + +## Design + +### API Surface + +- Platform layer (`lambda-rs-platform`) + - Vertex input types (module `lambda_platform::wgpu::vertex`) + - `enum VertexStepMode { Vertex, Instance }` + - Maps directly to `wgpu::VertexStepMode::{Vertex, Instance}`. + - `struct VertexBufferLayout { stride: u64, step_mode: VertexStepMode, /* attributes */ }` + - The `step_mode` field controls whether attributes sourced from this + buffer advance per vertex or per instance. + - Render pass integration (module `lambda_platform::wgpu::render_pass`) + - `fn draw(&mut self, vertices: Range, instances: Range)`. + - `fn draw_indexed(&mut self, indices: Range, base_vertex: i32, + instances: Range)`. + - These functions map `instances.start` to `first_instance` and + `instances.end - instances.start` to `instance_count` on the underlying + `wgpu::RenderPass`. + +- High-level layer (`lambda-rs`) + - Vertex buffer layouts (module `lambda::render::vertex`) + - `enum VertexStepMode { PerVertex, PerInstance }` + - High-level mirror of the platform `VertexStepMode`. + - `struct VertexBufferLayout { stride: u64, step_mode: VertexStepMode, + /* attributes */ }` + - `step_mode` defaults to `PerVertex` when not explicitly set. + - Pipeline builder (module `lambda::render::pipeline`) + - `RenderPipelineBuilder::with_buffer(buffer, attributes) -> Self` + - Existing function; continues to configure a per-vertex buffer and + implicitly sets `step_mode` to `PerVertex`. + - `RenderPipelineBuilder::with_buffer_step_mode(buffer, attributes, + step_mode: VertexStepMode) + -> Self` + - New builder method that configures a buffer with an explicit step mode. + - MUST accept both `PerVertex` and `PerInstance` and attach the step mode + to the vertex buffer layout for the given slot. + - `RenderPipelineBuilder::with_instance_buffer(buffer, attributes) -> Self` + - Convenience method equivalent to calling `with_buffer_step_mode` with + `step_mode = VertexStepMode::PerInstance`. + - Render commands (module `lambda::render::command`) + - `enum RenderCommand { /* existing variants */, Draw { vertices: + Range, instances: Range }, DrawIndexed { indices: + Range, base_vertex: i32, instances: Range }, /* ... */ }` + - `Draw` and `DrawIndexed` remain the single entry points for emitting + primitives; instanced rendering is expressed entirely through the + `instances` range. + - The engine MUST treat `instances = 0..1` as the default single-instance + behavior used by existing rendering paths. + - Feature flags (`lambda-rs`) + - `render-instancing-validation` + - Owning crate: `lambda-rs`. + - Default: disabled in release builds; MAY be enabled in debug builds or + by opt-in. + - Summary: enables additional validation of instance ranges, step modes, + and buffer bindings for instanced draws. + - Runtime cost: additional checks per draw command when instancing is + used; no effect when instancing is unused. + +### Behavior + +- Step modes and vertex consumption + - Buffers configured with `VertexStepMode::PerVertex` advance attribute + indices once per vertex and are indexed by the vertex index. + - Buffers configured with `VertexStepMode::PerInstance` advance attribute + indices once per instance and are indexed by the instance index. + - The pipeline vertex input layout MUST include exactly one step mode per + buffer slot; a buffer slot cannot mix per-vertex and per-instance step + modes. +- Draw commands and instance ranges + - The `instances` range on `Draw` and `DrawIndexed` commands controls the + number of instances emitted and the first instance index: + - `first_instance = instances.start`. + - `instance_count = instances.end - instances.start`. + - When `instance_count == 0`, the engine SHOULD treat the draw as a no-op and + MAY log a debug-level diagnostic when instancing validation is enabled. + - When no buffers are configured with `PerInstance` step mode, instanced + draws remain valid and expose the instance index only through the shader + built-in. + - Existing rendering paths that omit explicit `instances` ranges MUST + continue to behave as single-instance draws by using `0..1`. +- Buffer bindings and slots + - Buffer slots are shared between per-vertex and per-instance buffers; the + step mode recorded on the pipeline layout determines how the backend steps + each buffer slot. + - A buffer bound to a slot whose layout uses `PerInstance` step mode is an + instance buffer; a buffer bound to a slot whose layout uses `PerVertex` + step mode is a vertex buffer. + - The render context MUST bind all buffers required by the pipeline (vertex + and instance) before issuing `Draw` or `DrawIndexed` commands that rely on + those slots. +- Validation behavior + - When `render-instancing-validation` and `render-validation-encoder` are + enabled, the engine SHOULD: + - Verify that all buffer slots used by per-instance attributes are bound + before a draw that uses those attributes. + - Emit a clear error when a draw is issued with an `instances` range whose + upper bound exceeds engine-configured expectations for the instance + buffer size, when this information is available. + - Check that `instances.start <= instances.end` and treat negative-length + ranges as configuration errors. + +### Validation and Errors + +- Command ordering + - `BeginRenderPass` MUST precede any `SetPipeline`, `BindVertexBuffer`, + `Draw`, or `DrawIndexed` commands. + - `EndRenderPass` MUST terminate the pass; commands that require an active + pass and are encoded after `EndRenderPass` SHOULD be rejected and logged as + configuration errors. +- Vertex and instance buffer binding + - `BindVertexBuffer` MUST reference a buffer created with `BufferType::Vertex` + and a slot index that is less than the number of vertex buffer layouts + declared on the pipeline. + - When `render-instancing-validation` is enabled, the engine SHOULD: + - Verify that the set of bound buffers covers all pipeline slots that + declare per-instance attributes before a draw is issued. + - Log an error if a draw is issued with a per-instance attribute whose slot + has not been bound. +- Instance range validation + - An `instances` range with `start > end` MUST be rejected as invalid, and + the engine SHOULD log a clear diagnostic that includes the offending + range. + - For `start == end`, the draw SHOULD be treated as a no-op and MAY log a + debug-level message when instancing validation is enabled. + - Extremely large instance counts MAY be clamped or rejected based on device + limits; see Constraints and Rules. + +## Constraints and Rules + +- Device support + - Instanced rendering is a core capability of the `wgpu` backend; the engine + MAY assume basic support for instancing on all supported devices. + - If a backend without instancing support is ever introduced, instanced + draws MUST fail fast at pipeline creation or command encoding with a clear + diagnostic. +- Data layout + - Per-instance attributes follow the same alignment and format rules as + per-vertex attributes; attribute offsets MUST be within the buffer stride + and aligned to the format size. + - Buffers used for per-instance data MUST be created with vertex usage flags + consistent with existing vertex buffers. + - Instance buffer sizes SHOULD be chosen to accommodate the maximum expected + instance count for the associated draw paths. +- Limits + - The engine SHOULD respect and document any `wgpu` limits on maximum vertex + buffer stride, attribute count, and instance count. + - Instanced draws that exceed device limits MUST be rejected and logged + rather than silently truncated. + +## Performance Considerations + +- Recommendations + - Prefer instanced rendering over many small, identical draw calls when + rendering repeated geometry. + - Rationale: instancing reduces CPU overhead and command buffer size by + amortizing state setup across many instances. + - Pack frequently updated per-instance attributes into a small number of + tightly packed instance buffers. + - Rationale: fewer, contiguous buffers improve cache locality and reduce + binding overhead. + - Avoid using instanced rendering for very small numbers of instances when + it complicates shader logic without measurable benefit. + - Rationale: the complexity overhead MAY outweigh the performance gain for + a handful of instances. + - Use validation features only in development builds. + - Rationale: instancing validation introduces per-draw checks that are + valuable for debugging but unnecessary in production. + +## Requirements Checklist + +- Functionality + - [ ] Instance-aware vertex buffer layouts defined in `lambda-rs` and + `lambda-rs-platform`. + - [ ] Draw helpers in `lambda-rs-platform` accept and forward instance + ranges. + - [ ] Existing draw paths continue to function with `instances = 0..1`. +- API Surface + - [ ] `VertexStepMode` exposed at engine and platform layers. + - [ ] `RenderPipelineBuilder` supports explicit per-instance buffers via + `with_buffer_step_mode` and `with_instance_buffer`. + - [ ] Instancing validation feature flag defined in `lambda-rs`. +- Validation and Errors + - [ ] Command ordering checks cover instanced draws. + - [ ] Instance range validation implemented and feature-gated. + - [ ] Buffer binding diagnostics cover per-instance attributes. +- Performance + - [ ] Critical instanced draw paths reasoned about or profiled. + - [ ] Memory usage for instance buffers characterized for example scenes. + - [ ] Performance recommendations documented for instanced rendering usage. +- Documentation and Examples + - [ ] User-facing rendering docs updated to describe instanced rendering and + usage patterns. + - [ ] At least one example or runnable scenario added that demonstrates + instanced rendering. + - [ ] Any necessary migration notes captured in `docs/rendering.md` or + related documentation. + +For each checked item, include a reference to a commit, pull request, or file +path that demonstrates the implementation. + +## Verification and Testing + +- Unit Tests + - Verify that vertex buffer layouts correctly map `VertexStepMode` from the + engine layer to the platform layer and into `wgpu`. + - Ensure that draw helpers forward instance ranges correctly and reject + invalid ranges when validation is enabled. + - Commands: + - `cargo test -p lambda-rs-platform -- --nocapture` + - `cargo test -p lambda-rs -- --nocapture` +- Integration Tests + - Add or extend runnable scenarios in `crates/lambda-rs/tests/runnables.rs` + to cover instanced rendering of simple primitives (for example, many + cubes or quads sharing geometry with per-instance transforms). + - Validate that renders behave consistently across supported platforms and + backends. + - Commands: + - `cargo test --workspace -- --nocapture` +- Manual Checks + - Run an example binary that uses instanced rendering, verify that many + instances of a mesh render with distinct transforms or colors, and confirm + that instance count and ranges behave as expected when tweaked. + - Observe logs with instancing validation enabled to confirm that invalid + ranges or missing bindings produce actionable diagnostics. + +## Compatibility and Migration + +- Public engine APIs + - Adding `VertexStepMode` and step mode-aware buffer builders is designed to + be backwards compatible; existing code that does not configure per-instance + buffers continues to function unchanged. + - The default step mode for existing `with_buffer` calls MUST remain + per-vertex to avoid altering current behavior. +- Internal platform APIs + - The updated draw helper signatures in `lambda-rs-platform` constitute an + internal change; engine call sites MUST be updated in the same change set. + - No user-facing migration is required unless external code depends directly + on `lambda-rs-platform` internals, which is discouraged. +- Feature interactions + - Instancing validation MUST compose with existing rendering validation + features; enabling multiple validation features MUST NOT alter the + semantics of valid rendering commands. + - No new environment variables are introduced by this specification. + +## Changelog + +- 2025-11-23 (v0.1.0) — Initial draft of instanced rendering specification. From b1f0509d245065823dff2721f97e16c0215acc4f Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 24 Nov 2025 16:33:23 -0800 Subject: [PATCH 02/10] [add] vertex step mode and layout mappings. --- .../lambda-rs-platform/src/wgpu/pipeline.rs | 78 +++++++++++++++++-- crates/lambda-rs-platform/src/wgpu/vertex.rs | 21 +++++ crates/lambda-rs/src/render/pipeline.rs | 76 +++++++++++++++++- crates/lambda-rs/src/render/vertex.rs | 22 ++++++ docs/specs/instanced-rendering.md | 76 +++++++++--------- 5 files changed, 228 insertions(+), 45 deletions(-) diff --git a/crates/lambda-rs-platform/src/wgpu/pipeline.rs b/crates/lambda-rs-platform/src/wgpu/pipeline.rs index c0a99f90..df934feb 100644 --- a/crates/lambda-rs-platform/src/wgpu/pipeline.rs +++ b/crates/lambda-rs-platform/src/wgpu/pipeline.rs @@ -4,6 +4,7 @@ use std::ops::Range; use wgpu; +pub use crate::wgpu::vertex::VertexStepMode; use crate::wgpu::{ bind, gpu::Gpu, @@ -81,6 +82,14 @@ pub struct VertexAttributeDesc { pub format: ColorFormat, } +/// Description of a single vertex buffer layout used by a pipeline. +#[derive(Clone, Debug)] +struct VertexBufferLayoutDesc { + array_stride: u64, + step_mode: VertexStepMode, + attributes: Vec, +} + /// Compare function used for depth and stencil tests. #[derive(Clone, Copy, Debug)] pub enum CompareFunction { @@ -301,7 +310,7 @@ impl RenderPipeline { pub struct RenderPipelineBuilder<'a> { label: Option, layout: Option<&'a wgpu::PipelineLayout>, - vertex_buffers: Vec<(u64, Vec)>, + vertex_buffers: Vec, cull_mode: CullingMode, color_target_format: Option, depth_stencil: Option, @@ -340,7 +349,26 @@ impl<'a> RenderPipelineBuilder<'a> { array_stride: u64, attributes: Vec, ) -> Self { - self.vertex_buffers.push((array_stride, attributes)); + self = self.with_vertex_buffer_step_mode( + array_stride, + VertexStepMode::Vertex, + attributes, + ); + return self; + } + + /// Add a vertex buffer layout with attributes and an explicit step mode. + pub fn with_vertex_buffer_step_mode( + mut self, + array_stride: u64, + step_mode: VertexStepMode, + attributes: Vec, + ) -> Self { + self.vertex_buffers.push(VertexBufferLayoutDesc { + array_stride, + step_mode, + attributes, + }); return self; } @@ -431,11 +459,12 @@ impl<'a> RenderPipelineBuilder<'a> { // storage stable for layout lifetimes. let mut attr_storage: Vec> = Vec::new(); let mut strides: Vec = Vec::new(); - for (stride, attrs) in &self.vertex_buffers { + let mut step_modes: Vec = Vec::new(); + for buffer_desc in &self.vertex_buffers { let mut raw_attrs: Vec = - Vec::with_capacity(attrs.len()); + Vec::with_capacity(buffer_desc.attributes.len()); - for attribute in attrs.iter() { + for attribute in buffer_desc.attributes.iter() { raw_attrs.push(wgpu::VertexAttribute { shader_location: attribute.shader_location, offset: attribute.offset, @@ -444,7 +473,8 @@ impl<'a> RenderPipelineBuilder<'a> { } let boxed: Box<[wgpu::VertexAttribute]> = raw_attrs.into_boxed_slice(); attr_storage.push(boxed); - strides.push(*stride); + strides.push(buffer_desc.array_stride); + step_modes.push(buffer_desc.step_mode); } // Now build layouts referencing the stable storage in `attr_storage`. let mut vbl: Vec> = Vec::new(); @@ -452,7 +482,7 @@ impl<'a> RenderPipelineBuilder<'a> { let slice = boxed.as_ref(); vbl.push(wgpu::VertexBufferLayout { array_stride: strides[i], - step_mode: wgpu::VertexStepMode::Vertex, + step_mode: step_modes[i].to_wgpu(), attributes: slice, }); } @@ -511,3 +541,37 @@ impl<'a> RenderPipelineBuilder<'a> { }; } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn vertex_step_mode_maps_to_wgpu() { + let vertex_mode = VertexStepMode::Vertex.to_wgpu(); + let instance_mode = VertexStepMode::Instance.to_wgpu(); + + assert_eq!(vertex_mode, wgpu::VertexStepMode::Vertex); + assert_eq!(instance_mode, wgpu::VertexStepMode::Instance); + } + + #[test] + fn with_vertex_buffer_defaults_to_per_vertex_step_mode() { + let builder = RenderPipelineBuilder::new().with_vertex_buffer( + 16, + vec![VertexAttributeDesc { + shader_location: 0, + offset: 0, + format: ColorFormat::Rgb32Sfloat, + }], + ); + + let vertex_buffers = &builder.vertex_buffers; + + assert_eq!(vertex_buffers.len(), 1); + assert!(matches!( + vertex_buffers[0].step_mode, + VertexStepMode::Vertex + )); + } +} diff --git a/crates/lambda-rs-platform/src/wgpu/vertex.rs b/crates/lambda-rs-platform/src/wgpu/vertex.rs index da8b2ded..19847b5c 100644 --- a/crates/lambda-rs-platform/src/wgpu/vertex.rs +++ b/crates/lambda-rs-platform/src/wgpu/vertex.rs @@ -26,3 +26,24 @@ impl ColorFormat { }; } } + +/// Step mode applied to a vertex buffer layout. +/// +/// `Vertex` advances attributes per vertex; `Instance` advances attributes per +/// instance. This mirrors `wgpu::VertexStepMode` without exposing the raw +/// dependency to higher layers. +#[derive(Clone, Copy, Debug)] +pub enum VertexStepMode { + Vertex, + Instance, +} + +impl VertexStepMode { + /// Map the engine step mode to the underlying graphics API. + pub(crate) fn to_wgpu(self) -> wgpu::VertexStepMode { + return match self { + VertexStepMode::Vertex => wgpu::VertexStepMode::Vertex, + VertexStepMode::Instance => wgpu::VertexStepMode::Instance, + }; + } +} diff --git a/crates/lambda-rs/src/render/pipeline.rs b/crates/lambda-rs/src/render/pipeline.rs index 0d032ae5..2f988ebb 100644 --- a/crates/lambda-rs/src/render/pipeline.rs +++ b/crates/lambda-rs/src/render/pipeline.rs @@ -45,7 +45,11 @@ use super::{ render_pass::RenderPass, shader::Shader, texture, - vertex::VertexAttribute, + vertex::{ + VertexAttribute, + VertexBufferLayout, + VertexStepMode, + }, RenderContext, }; use crate::render::validation; @@ -106,6 +110,7 @@ pub type PushConstantUpload = (PipelineStage, Range); struct BufferBinding { buffer: Rc, + layout: VertexBufferLayout, attributes: Vec, } @@ -159,6 +164,15 @@ impl CullingMode { } } +fn to_platform_step_mode( + step_mode: VertexStepMode, +) -> platform_pipeline::VertexStepMode { + return match step_mode { + VertexStepMode::PerVertex => platform_pipeline::VertexStepMode::Vertex, + VertexStepMode::PerInstance => platform_pipeline::VertexStepMode::Instance, + }; +} + /// Engine-level stencil operation. #[derive(Clone, Copy, Debug)] pub enum StencilOperation { @@ -265,9 +279,23 @@ impl RenderPipelineBuilder { /// Declare a vertex buffer and the vertex attributes consumed by the shader. pub fn with_buffer( + self, + buffer: Buffer, + attributes: Vec, + ) -> Self { + return self.with_buffer_step_mode( + buffer, + attributes, + VertexStepMode::PerVertex, + ); + } + + /// Declare a vertex buffer with an explicit step mode. + pub fn with_buffer_step_mode( mut self, buffer: Buffer, attributes: Vec, + step_mode: VertexStepMode, ) -> Self { #[cfg(any(debug_assertions, feature = "render-validation-encoder",))] { @@ -278,13 +306,32 @@ impl RenderPipelineBuilder { ); } } + + let layout = VertexBufferLayout { + stride: buffer.stride(), + step_mode, + }; self.bindings.push(BufferBinding { buffer: Rc::new(buffer), + layout, attributes, }); return self; } + /// Declare a per-instance vertex buffer. + pub fn with_instance_buffer( + self, + buffer: Buffer, + attributes: Vec, + ) -> Self { + return self.with_buffer_step_mode( + buffer, + attributes, + VertexStepMode::PerInstance, + ); + } + /// Declare a push constant range for a shader stage in bytes. pub fn with_push_constant( mut self, @@ -488,8 +535,11 @@ impl RenderPipelineBuilder { }) .collect(); - rp_builder = - rp_builder.with_vertex_buffer(binding.buffer.stride(), attributes); + rp_builder = rp_builder.with_vertex_buffer_step_mode( + binding.layout.stride, + to_platform_step_mode(binding.layout.step_mode), + attributes, + ); buffers.push(binding.buffer.clone()); } @@ -594,3 +644,23 @@ impl RenderPipelineBuilder { }; } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn engine_step_mode_maps_to_platform_step_mode() { + let per_vertex = to_platform_step_mode(VertexStepMode::PerVertex); + let per_instance = to_platform_step_mode(VertexStepMode::PerInstance); + + assert!(matches!( + per_vertex, + platform_pipeline::VertexStepMode::Vertex + )); + assert!(matches!( + per_instance, + platform_pipeline::VertexStepMode::Instance + )); + } +} diff --git a/crates/lambda-rs/src/render/vertex.rs b/crates/lambda-rs/src/render/vertex.rs index 1592a149..fea22693 100644 --- a/crates/lambda-rs/src/render/vertex.rs +++ b/crates/lambda-rs/src/render/vertex.rs @@ -26,6 +26,28 @@ impl ColorFormat { } } +/// Step mode applied to a vertex buffer layout. +/// +/// `PerVertex` advances attributes once per vertex; `PerInstance` advances +/// attributes once per instance. This mirrors the platform step mode without +/// exposing backend types. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum VertexStepMode { + PerVertex, + PerInstance, +} + +/// Layout for a single vertex buffer slot. +/// +/// `stride` describes the size in bytes of one element in the buffer. The +/// `step_mode` field determines whether attributes sourced from this buffer +/// advance per vertex or per instance. +#[derive(Clone, Copy, Debug)] +pub struct VertexBufferLayout { + pub stride: u64, + pub step_mode: VertexStepMode, +} + /// A single vertex element (format + byte offset). #[derive(Clone, Copy, Debug)] /// diff --git a/docs/specs/instanced-rendering.md b/docs/specs/instanced-rendering.md index 03ad017b..0e1740e3 100644 --- a/docs/specs/instanced-rendering.md +++ b/docs/specs/instanced-rendering.md @@ -3,13 +3,13 @@ title: "Instanced Rendering" document_id: "instanced-rendering-2025-11-23" status: "draft" created: "2025-11-23T00:00:00Z" -last_updated: "2025-11-23T00:00:00Z" -version: "0.1.0" +last_updated: "2025-11-25T00: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: "afabc0597de11b66124e937b4346923e25da3159" +repo_commit: "84c73fbac8ce660827189fda1de96e50b5c8a9d5" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["spec", "rendering", "instancing", "vertex-input"] @@ -51,12 +51,12 @@ tags: ["spec", "rendering", "instancing", "vertex-input"] ### Goals - Define instance-rate vertex buffer semantics in the high-level vertex input - model using engine-level types. + model using types defined in `lambda-rs`. - Allow per-instance data (for example, transforms and colors) to be supplied through buffers and consumed by vertex shaders using existing binding patterns. - Clarify the semantics of the `instances: Range` field on draw commands - and propagate instance ranges through the platform layer to `wgpu`. + and propagate instance ranges through `lambda-rs-platform` to `wgpu`. - Add feature-gated validation for instance ranges, buffer usage, and configuration ordering that integrates with existing rendering validation features. @@ -90,21 +90,21 @@ tags: ["spec", "rendering", "instancing", "vertex-input"] ## Architecture Overview -- High-level layer (`lambda-rs`) +- Crate `lambda-rs` - `RenderPipelineBuilder` declares vertex buffer layouts, including a step mode that describes whether a buffer is per-vertex or per-instance. - The render command stream uses existing `Draw` and `DrawIndexed` commands with an `instances: Range` field to control instance count and first instance. - - Public types represent step modes and instance-aware vertex buffer layouts; +- Public types represent step modes and instance-aware vertex buffer layouts; backend-specific details remain internal to `lambda-rs-platform`. -- Platform layer (`lambda-rs-platform`) - - Wraps `wgpu` vertex buffer layouts with an engine-level `VertexStepMode` +- Crate `lambda-rs-platform` + - Wraps `wgpu` vertex buffer layouts with a crate-local `VertexStepMode` and forwards instance ranges to `wgpu::RenderPass::draw` and `wgpu::RenderPass::draw_indexed`. - Exposes draw helpers that accept both vertex or index ranges and instance - ranges, ensuring that engine-level commands can express instance counts and - first instance indices. + ranges, ensuring that commands in `lambda-rs` can express instance counts + and first instance indices. - Data flow ``` @@ -165,8 +165,8 @@ App Code - `Draw` and `DrawIndexed` remain the single entry points for emitting primitives; instanced rendering is expressed entirely through the `instances` range. - - The engine MUST treat `instances = 0..1` as the default single-instance - behavior used by existing rendering paths. + - The `lambda-rs` crate MUST treat `instances = 0..1` as the default + single-instance behavior used by existing rendering paths. - Feature flags (`lambda-rs`) - `render-instancing-validation` - Owning crate: `lambda-rs`. @@ -192,8 +192,9 @@ App Code number of instances emitted and the first instance index: - `first_instance = instances.start`. - `instance_count = instances.end - instances.start`. - - When `instance_count == 0`, the engine SHOULD treat the draw as a no-op and - MAY log a debug-level diagnostic when instancing validation is enabled. + - When `instance_count == 0`, the `lambda-rs` crate SHOULD treat the draw as + a no-op and MAY log a debug-level diagnostic when instancing validation is + enabled. - When no buffers are configured with `PerInstance` step mode, instanced draws remain valid and expose the instance index only through the shader built-in. @@ -211,12 +212,12 @@ App Code those slots. - Validation behavior - When `render-instancing-validation` and `render-validation-encoder` are - enabled, the engine SHOULD: + enabled, the `lambda-rs` crate SHOULD: - Verify that all buffer slots used by per-instance attributes are bound before a draw that uses those attributes. - Emit a clear error when a draw is issued with an `instances` range whose - upper bound exceeds engine-configured expectations for the instance - buffer size, when this information is available. + upper bound exceeds expectations for the instance buffer size, when this + information is available. - Check that `instances.start <= instances.end` and treat negative-length ranges as configuration errors. @@ -232,7 +233,7 @@ App Code - `BindVertexBuffer` MUST reference a buffer created with `BufferType::Vertex` and a slot index that is less than the number of vertex buffer layouts declared on the pipeline. - - When `render-instancing-validation` is enabled, the engine SHOULD: + - When `render-instancing-validation` is enabled, the `lambda-rs` crate SHOULD: - Verify that the set of bound buffers covers all pipeline slots that declare per-instance attributes before a draw is issued. - Log an error if a draw is issued with a per-instance attribute whose slot @@ -249,8 +250,9 @@ App Code ## Constraints and Rules - Device support - - Instanced rendering is a core capability of the `wgpu` backend; the engine - MAY assume basic support for instancing on all supported devices. + - Instanced rendering is a core capability of the `wgpu` backend; the + implementation in `lambda-rs` MAY assume basic support for instancing on + all supported devices. - If a backend without instancing support is ever introduced, instanced draws MUST fail fast at pipeline creation or command encoding with a clear diagnostic. @@ -263,8 +265,9 @@ App Code - Instance buffer sizes SHOULD be chosen to accommodate the maximum expected instance count for the associated draw paths. - Limits - - The engine SHOULD respect and document any `wgpu` limits on maximum vertex - buffer stride, attribute count, and instance count. + - The implementation in `lambda-rs` SHOULD respect and document any `wgpu` + limits on maximum vertex buffer stride, attribute count, and instance + count. - Instanced draws that exceed device limits MUST be rejected and logged rather than silently truncated. @@ -290,15 +293,15 @@ App Code ## Requirements Checklist - Functionality - - [ ] Instance-aware vertex buffer layouts defined in `lambda-rs` and + - [x] Instance-aware vertex buffer layouts supported in `lambda-rs` and `lambda-rs-platform`. - - [ ] Draw helpers in `lambda-rs-platform` accept and forward instance + - [x] Draw helpers in `lambda-rs-platform` accept and forward instance ranges. - [ ] Existing draw paths continue to function with `instances = 0..1`. - API Surface - - [ ] `VertexStepMode` exposed at engine and platform layers. - - [ ] `RenderPipelineBuilder` supports explicit per-instance buffers via - `with_buffer_step_mode` and `with_instance_buffer`. + - [x] `VertexStepMode` exposed in `lambda-rs` and `lambda-rs-platform`. + - [x] `RenderPipelineBuilder` in `lambda-rs` supports explicit per-instance + buffers via `with_buffer_step_mode` and `with_instance_buffer`. - [ ] Instancing validation feature flag defined in `lambda-rs`. - Validation and Errors - [ ] Command ordering checks cover instanced draws. @@ -316,14 +319,14 @@ App Code - [ ] Any necessary migration notes captured in `docs/rendering.md` or related documentation. -For each checked item, include a reference to a commit, pull request, or file -path that demonstrates the implementation. +For each checked item, the implementing change set SHOULD reference the +relevant code and tests, for example in the pull request description. ## Verification and Testing - Unit Tests - - Verify that vertex buffer layouts correctly map `VertexStepMode` from the - engine layer to the platform layer and into `wgpu`. + - Verify that vertex buffer layouts correctly map `VertexStepMode` from + `lambda-rs` into `lambda-rs-platform` and then into `wgpu`. - Ensure that draw helpers forward instance ranges correctly and reject invalid ranges when validation is enabled. - Commands: @@ -346,15 +349,16 @@ path that demonstrates the implementation. ## Compatibility and Migration -- Public engine APIs +- Public `lambda-rs` APIs - Adding `VertexStepMode` and step mode-aware buffer builders is designed to be backwards compatible; existing code that does not configure per-instance buffers continues to function unchanged. - The default step mode for existing `with_buffer` calls MUST remain per-vertex to avoid altering current behavior. -- Internal platform APIs +- Internal `lambda-rs-platform` APIs - The updated draw helper signatures in `lambda-rs-platform` constitute an - internal change; engine call sites MUST be updated in the same change set. + internal change; call sites in `lambda-rs` MUST be updated in the same + change set. - No user-facing migration is required unless external code depends directly on `lambda-rs-platform` internals, which is discouraged. - Feature interactions @@ -365,4 +369,6 @@ path that demonstrates the implementation. ## Changelog +- 2025-11-25 (v0.1.2) — Update terminology to reference crates by name and remove per-file implementation locations from the Requirements Checklist. +- 2025-11-24 (v0.1.1) — Mark initial instancing layout and step mode support as implemented in the Requirements Checklist; metadata updated. - 2025-11-23 (v0.1.0) — Initial draft of instanced rendering specification. From 7f8375d73a0dbca5eb143dda38e8e1600f62683c Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 24 Nov 2025 16:59:29 -0800 Subject: [PATCH 03/10] [add] instanced rendering validation & update specification/features document. --- crates/lambda-rs/Cargo.toml | 1 + crates/lambda-rs/src/render/mod.rs | 109 +++++++++++++++++++++- crates/lambda-rs/src/render/pipeline.rs | 12 +++ crates/lambda-rs/src/render/validation.rs | 89 ++++++++++++++++++ docs/features.md | 11 ++- docs/specs/instanced-rendering.md | 10 +- 6 files changed, 220 insertions(+), 12 deletions(-) diff --git a/crates/lambda-rs/Cargo.toml b/crates/lambda-rs/Cargo.toml index 8801049b..1f9ba5dd 100644 --- a/crates/lambda-rs/Cargo.toml +++ b/crates/lambda-rs/Cargo.toml @@ -73,6 +73,7 @@ render-validation-stencil = [] render-validation-pass-compat = [] render-validation-device = [] render-validation-encoder = [] +render-instancing-validation = [] # ---------------------------- PLATFORM DEPENDENCIES --------------------------- diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index e499a2f8..2f304fa4 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -621,12 +621,19 @@ impl RenderContext { { Self::apply_viewport(pass, &initial_viewport); - #[cfg(any(debug_assertions, feature = "render-validation-encoder",))] + #[cfg(any( + debug_assertions, + feature = "render-validation-encoder", + feature = "render-instancing-validation", + ))] let mut current_pipeline: Option = None; #[cfg(any(debug_assertions, feature = "render-validation-encoder",))] let mut bound_index_buffer: Option<(usize, u32)> = None; + #[cfg(any(debug_assertions, feature = "render-instancing-validation",))] + let mut bound_vertex_slots: HashSet = HashSet::new(); + // De-duplicate advisories within this pass #[cfg(any( debug_assertions, @@ -689,8 +696,12 @@ 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",))] + // happen only after a pipeline is set when validation is enabled. + #[cfg(any( + debug_assertions, + feature = "render-validation-encoder", + feature = "render-instancing-validation", + ))] { current_pipeline = Some(pipeline); } @@ -792,6 +803,14 @@ impl RenderContext { )); })?; + #[cfg(any( + debug_assertions, + feature = "render-instancing-validation", + ))] + { + bound_vertex_slots.insert(buffer); + } + pass.set_vertex_buffer(buffer as u32, buffer_ref.raw()); } RenderCommand::BindIndexBuffer { buffer, format } => { @@ -863,7 +882,11 @@ impl RenderContext { vertices, instances, } => { - #[cfg(any(debug_assertions, feature = "render-validation-encoder",))] + #[cfg(any( + debug_assertions, + feature = "render-validation-encoder", + feature = "render-instancing-validation", + ))] { if current_pipeline.is_none() { return Err(RenderError::Configuration( @@ -871,6 +894,42 @@ impl RenderContext { .to_string(), )); } + + #[cfg(any( + debug_assertions, + feature = "render-instancing-validation", + ))] + { + let pipeline_index = current_pipeline.expect( + "current_pipeline must be set when validation is active", + ); + let pipeline_ref = &self.render_pipelines[pipeline_index]; + + validation::validate_instance_bindings( + pipeline_ref.pipeline().label().unwrap_or("unnamed"), + pipeline_ref.per_instance_slots(), + &bound_vertex_slots, + ) + .map_err(RenderError::Configuration)?; + } + + if let Err(msg) = + validation::validate_instance_range("Draw", &instances) + { + return Err(RenderError::Configuration(msg)); + } + if instances.start == instances.end { + #[cfg(any( + debug_assertions, + feature = "render-instancing-validation", + ))] + logging::debug!( + "Skipping Draw with empty instance range {}..{}", + instances.start, + instances.end + ); + continue; + } } pass.draw(vertices, instances); } @@ -914,6 +973,48 @@ impl RenderContext { ))); } } + #[cfg(any( + debug_assertions, + feature = "render-validation-encoder", + feature = "render-instancing-validation", + ))] + { + #[cfg(any( + debug_assertions, + feature = "render-instancing-validation", + ))] + { + let pipeline_index = current_pipeline.expect( + "current_pipeline must be set when validation is active", + ); + let pipeline_ref = &self.render_pipelines[pipeline_index]; + + validation::validate_instance_bindings( + pipeline_ref.pipeline().label().unwrap_or("unnamed"), + pipeline_ref.per_instance_slots(), + &bound_vertex_slots, + ) + .map_err(RenderError::Configuration)?; + } + + if let Err(msg) = + validation::validate_instance_range("DrawIndexed", &instances) + { + return Err(RenderError::Configuration(msg)); + } + if instances.start == instances.end { + #[cfg(any( + debug_assertions, + feature = "render-instancing-validation", + ))] + logging::debug!( + "Skipping DrawIndexed with empty instance range {}..{}", + instances.start, + instances.end + ); + continue; + } + } pass.draw_indexed(indices, base_vertex, instances); } RenderCommand::BeginRenderPass { .. } => { diff --git a/crates/lambda-rs/src/render/pipeline.rs b/crates/lambda-rs/src/render/pipeline.rs index 2f988ebb..aad3bfbd 100644 --- a/crates/lambda-rs/src/render/pipeline.rs +++ b/crates/lambda-rs/src/render/pipeline.rs @@ -65,6 +65,7 @@ pub struct RenderPipeline { color_target_count: u32, expects_depth_stencil: bool, uses_stencil: bool, + per_instance_slots: Vec, } impl RenderPipeline { @@ -100,6 +101,11 @@ impl RenderPipeline { pub(super) fn uses_stencil(&self) -> bool { return self.uses_stencil; } + + /// Per-vertex-buffer flags indicating which slots advance per instance. + pub(super) fn per_instance_slots(&self) -> &Vec { + return &self.per_instance_slots; + } } /// Public alias for platform shader stage flags used by push constants. @@ -519,6 +525,7 @@ impl RenderPipelineBuilder { // Vertex buffers and attributes let mut buffers = Vec::with_capacity(self.bindings.len()); + let mut per_instance_slots = Vec::with_capacity(self.bindings.len()); let mut rp_builder = platform_pipeline::RenderPipelineBuilder::new() .with_label(self.label.as_deref().unwrap_or("lambda-render-pipeline")) .with_layout(&pipeline_layout) @@ -541,6 +548,10 @@ impl RenderPipelineBuilder { attributes, ); buffers.push(binding.buffer.clone()); + per_instance_slots.push(matches!( + binding.layout.step_mode, + VertexStepMode::PerInstance + )); } if fragment_module.is_some() { @@ -641,6 +652,7 @@ impl RenderPipelineBuilder { // Depth/stencil is enabled when `with_depth*` was called on the builder. expects_depth_stencil: self.use_depth, uses_stencil: self.stencil.is_some(), + per_instance_slots, }; } } diff --git a/crates/lambda-rs/src/render/validation.rs b/crates/lambda-rs/src/render/validation.rs index 0d13e375..0a34f069 100644 --- a/crates/lambda-rs/src/render/validation.rs +++ b/crates/lambda-rs/src/render/validation.rs @@ -1,5 +1,10 @@ //! Small helpers for limits and alignment validation used by the renderer. +use std::{ + collections::HashSet, + ops::Range, +}; + /// Align `value` up to the nearest multiple of `align`. /// If `align` is zero, returns `value` unchanged. pub fn align_up(value: u64, align: u64) -> u64 { @@ -54,6 +59,47 @@ pub fn validate_sample_count(samples: u32) -> Result<(), String> { } } +/// Validate that an instance range is well-formed for a draw command. +/// +/// The `command_name` is included in any error message to make diagnostics +/// easier to interpret when multiple draw commands are present. +pub fn validate_instance_range( + command_name: &str, + instances: &Range, +) -> Result<(), String> { + if instances.start > instances.end { + return Err(format!( + "{} instance range start {} is greater than end {}", + command_name, instances.start, instances.end + )); + } + return Ok(()); +} + +/// Validate that all per-instance vertex buffer slots have been bound before +/// issuing a draw that consumes them. +/// +/// The `pipeline_label` identifies the pipeline in diagnostics. The +/// `per_instance_slots` slice marks which vertex buffer slots advance once +/// per instance, while `bound_slots` tracks the vertex buffer slots that +/// have been bound in the current render pass. +pub fn validate_instance_bindings( + pipeline_label: &str, + per_instance_slots: &[bool], + bound_slots: &HashSet, +) -> Result<(), String> { + for (slot, is_instance) in per_instance_slots.iter().enumerate() { + if *is_instance && !bound_slots.contains(&(slot as u32)) { + return Err(format!( + "Render pipeline '{}' requires a per-instance vertex buffer bound at slot {} but no BindVertexBuffer command bound that slot in this pass", + pipeline_label, + slot + )); + } + } + return Ok(()); +} + #[cfg(test)] mod tests { use super::*; @@ -89,4 +135,47 @@ mod tests { .unwrap(); assert!(err.contains("not 256-byte aligned")); } + + #[test] + fn validate_instance_range_accepts_valid_ranges() { + assert!(validate_instance_range("Draw", &(0..1)).is_ok()); + assert!(validate_instance_range("DrawIndexed", &(2..2)).is_ok()); + } + + #[test] + fn validate_instance_range_rejects_negative_length() { + let err = validate_instance_range("Draw", &(5..1)) + .err() + .expect("must error"); + assert!(err.contains("Draw instance range start 5 is greater than end 1")); + } + + #[test] + fn validate_instance_bindings_accepts_bound_slots() { + let per_instance_slots = vec![true, false, true]; + let mut bound = HashSet::new(); + bound.insert(0); + bound.insert(2); + + assert!(validate_instance_bindings( + "test-pipeline", + &per_instance_slots, + &bound + ) + .is_ok()); + } + + #[test] + fn validate_instance_bindings_rejects_missing_slot() { + let per_instance_slots = vec![true, false, true]; + let mut bound = HashSet::new(); + bound.insert(0); + + let err = + validate_instance_bindings("instanced", &per_instance_slots, &bound) + .err() + .expect("must error"); + assert!(err.contains("instanced")); + assert!(err.contains("slot 2")); + } } diff --git a/docs/features.md b/docs/features.md index 04785f79..8ac020be 100644 --- a/docs/features.md +++ b/docs/features.md @@ -3,13 +3,13 @@ title: "Cargo Features Overview" document_id: "features-2025-11-17" status: "living" created: "2025-11-17T23:59:00Z" -last_updated: "2025-11-17T23:59:00Z" -version: "0.1.0" +last_updated: "2025-11-25T00:00:00Z" +version: "0.1.1" engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "70670f8ad6bb7ac14a62e7d5847bf21cfe13f665" +repo_commit: "b1f0509d245065823dff2721f97e16c0215acc4f" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["guide", "features", "validation", "cargo"] @@ -57,6 +57,10 @@ Granular features (crate: `lambda-rs`) - `render-validation-pass-compat`: SetPipeline-time errors when color targets or depth/stencil expectations do not match the active pass. - `render-validation-device`: device/format probing advisories (if available via the platform layer). - `render-validation-encoder`: additional per-draw/encoder-time checks; highest runtime cost. +- `render-instancing-validation`: instance-range and per-instance buffer binding validation for `RenderCommand::Draw` and `RenderCommand::DrawIndexed`. Behavior: + - Validates that `instances.start <= instances.end` and treats `start == end` as a no-op (draw is skipped). + - Ensures that all vertex buffer slots marked as per-instance on the active pipeline have been bound in the current render pass. + - Adds per-draw checks proportional to the number of instanced draws and per-instance slots; SHOULD be enabled only when diagnosing instancing issues. Always-on safeguards (debug and release) - Clamp depth clear values to `[0.0, 1.0]`. @@ -76,4 +80,5 @@ Usage examples - `cargo test -p lambda-rs --features render-validation-msaa` ## Changelog +- 0.1.1 (2025-11-25): Document `render-instancing-validation` behavior and update metadata. - 0.1.0 (2025-11-17): Initial document introducing validation features and behavior by build type. diff --git a/docs/specs/instanced-rendering.md b/docs/specs/instanced-rendering.md index 0e1740e3..4c8017e0 100644 --- a/docs/specs/instanced-rendering.md +++ b/docs/specs/instanced-rendering.md @@ -9,7 +9,7 @@ engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "84c73fbac8ce660827189fda1de96e50b5c8a9d5" +repo_commit: "b1f0509d245065823dff2721f97e16c0215acc4f" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["spec", "rendering", "instancing", "vertex-input"] @@ -302,11 +302,11 @@ App Code - [x] `VertexStepMode` exposed in `lambda-rs` and `lambda-rs-platform`. - [x] `RenderPipelineBuilder` in `lambda-rs` supports explicit per-instance buffers via `with_buffer_step_mode` and `with_instance_buffer`. - - [ ] Instancing validation feature flag defined in `lambda-rs`. + - [x] Instancing validation feature flag defined in `lambda-rs`. - Validation and Errors - [ ] Command ordering checks cover instanced draws. - - [ ] Instance range validation implemented and feature-gated. - - [ ] Buffer binding diagnostics cover per-instance attributes. + - [x] Instance range validation implemented and feature-gated. + - [x] Buffer binding diagnostics cover per-instance attributes. - Performance - [ ] Critical instanced draw paths reasoned about or profiled. - [ ] Memory usage for instance buffers characterized for example scenes. @@ -369,6 +369,6 @@ relevant code and tests, for example in the pull request description. ## Changelog -- 2025-11-25 (v0.1.2) — Update terminology to reference crates by name and remove per-file implementation locations from the Requirements Checklist. +- 2025-11-25 (v0.1.2) — Update terminology to reference crates by name, remove per-file implementation locations from the Requirements Checklist, and mark instancing validation features as implemented in `lambda-rs`. - 2025-11-24 (v0.1.1) — Mark initial instancing layout and step mode support as implemented in the Requirements Checklist; metadata updated. - 2025-11-23 (v0.1.0) — Initial draft of instanced rendering specification. From 2ff0a581af014a754e982881193c36f47e685602 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 24 Nov 2025 17:24:28 -0800 Subject: [PATCH 04/10] [add] demo showcasing instanced rendering and updated specification. --- crates/lambda-rs/examples/instanced_quads.rs | 384 +++++++++++++++++++ docs/specs/instanced-rendering.md | 11 +- 2 files changed, 390 insertions(+), 5 deletions(-) create mode 100644 crates/lambda-rs/examples/instanced_quads.rs diff --git a/crates/lambda-rs/examples/instanced_quads.rs b/crates/lambda-rs/examples/instanced_quads.rs new file mode 100644 index 00000000..50b9455b --- /dev/null +++ b/crates/lambda-rs/examples/instanced_quads.rs @@ -0,0 +1,384 @@ +#![allow(clippy::needless_return)] + +//! Example: Instanced 2D quads with per-instance vertex data. +//! +//! This example renders a grid of quads that all share the same geometry +//! but use per-instance offsets and colors supplied from a second vertex +//! buffer. It exercises `RenderPipelineBuilder::with_instance_buffer` and +//! `RenderCommand::DrawIndexed` with a non-trivial instance range. + +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 instance_offset; +layout (location = 2) in vec3 instance_color; + +layout (location = 0) out vec3 frag_color; + +void main() { + vec3 position = vertex_position + instance_offset; + gl_Position = vec4(position, 1.0); + frag_color = instance_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 QuadVertex { + position: [f32; 3], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +struct InstanceData { + offset: [f32; 3], + color: [f32; 3], +} + +// --------------------------------- COMPONENT --------------------------------- + +/// Component that renders a grid of instanced quads. +pub struct InstancedQuadsExample { + vertex_shader: Shader, + fragment_shader: Shader, + render_pass_id: Option, + render_pipeline_id: Option, + index_buffer_id: Option, + index_count: u32, + instance_count: u32, + width: u32, + height: u32, +} + +impl Component for InstancedQuadsExample { + fn on_attach( + &mut self, + render_context: &mut RenderContext, + ) -> Result { + let render_pass = RenderPassBuilder::new().build(render_context); + + // Quad geometry in clip space centered at the origin. + let quad_vertices: Vec = vec![ + QuadVertex { + position: [-0.05, -0.05, 0.0], + }, + QuadVertex { + position: [0.05, -0.05, 0.0], + }, + QuadVertex { + position: [0.05, 0.05, 0.0], + }, + QuadVertex { + position: [-0.05, 0.05, 0.0], + }, + ]; + + // Two triangles forming a quad. + let indices: Vec = vec![0, 1, 2, 2, 3, 0]; + let index_count = indices.len() as u32; + + // Build a grid of instance offsets and colors. + let grid_size: u32 = 10; + let spacing: f32 = 0.2; + let start: f32 = -0.9; + + let mut instances: Vec = Vec::new(); + for y in 0..grid_size { + for x in 0..grid_size { + let offset_x = start + (x as f32) * spacing; + let offset_y = start + (y as f32) * spacing; + + // Simple color gradient across the grid. + let color_r = (x as f32) / ((grid_size - 1) as f32); + let color_g = (y as f32) / ((grid_size - 1) as f32); + let color_b = 0.5; + + instances.push(InstanceData { + offset: [offset_x, offset_y, 0.0], + color: [color_r, color_g, color_b], + }); + } + } + let instance_count = instances.len() as u32; + + // Build vertex, instance, and index buffers. + let vertex_buffer = BufferBuilder::new() + .with_usage(Usage::VERTEX) + .with_properties(Properties::DEVICE_LOCAL) + .with_buffer_type(BufferType::Vertex) + .with_label("instanced-quads-vertices") + .build(render_context, quad_vertices) + .map_err(|error| error.to_string())?; + + let instance_buffer = BufferBuilder::new() + .with_usage(Usage::VERTEX) + .with_properties(Properties::DEVICE_LOCAL) + .with_buffer_type(BufferType::Vertex) + .with_label("instanced-quads-instances") + .build(render_context, instances) + .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("instanced-quads-indices") + .build(render_context, indices) + .map_err(|error| error.to_string())?; + + // Vertex attributes for per-vertex positions in slot 0. + let vertex_attributes = vec![VertexAttribute { + location: 0, + offset: 0, + element: VertexElement { + format: ColorFormat::Rgb32Sfloat, + offset: 0, + }, + }]; + + // Instance attributes in slot 1: offset and color. + let instance_attributes = vec![ + VertexAttribute { + location: 1, + offset: 0, + element: VertexElement { + format: ColorFormat::Rgb32Sfloat, + offset: 0, + }, + }, + VertexAttribute { + location: 2, + offset: 0, + element: VertexElement { + format: ColorFormat::Rgb32Sfloat, + offset: 12, + }, + }, + ]; + + let pipeline = RenderPipelineBuilder::new() + .with_culling(CullingMode::Back) + .with_buffer(vertex_buffer, vertex_attributes) + .with_instance_buffer(instance_buffer, instance_attributes) + .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; + self.instance_count = instance_count; + + logging::info!( + "Instanced quads example attached with {} instances", + self.instance_count + ); + return Ok(ComponentResult::Success); + } + + fn on_detach( + &mut self, + _render_context: &mut RenderContext, + ) -> Result { + logging::info!("Instanced quads 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 { + // This example uses static instance data; no per-frame updates required. + 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..self.instance_count, + }, + RenderCommand::EndRenderPass, + ]; + } +} + +impl Default for InstancedQuadsExample { + fn default() -> Self { + let vertex_virtual_shader = VirtualShader::Source { + source: VERTEX_SHADER_SOURCE.to_string(), + kind: ShaderKind::Vertex, + entry_point: "main".to_string(), + name: "instanced_quads".to_string(), + }; + + let fragment_virtual_shader = VirtualShader::Source { + source: FRAGMENT_SHADER_SOURCE.to_string(), + kind: ShaderKind::Fragment, + entry_point: "main".to_string(), + name: "instanced_quads".to_string(), + }; + + let mut shader_builder = ShaderBuilder::new(); + let vertex_shader = shader_builder.build(vertex_virtual_shader); + let fragment_shader = 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, + instance_count: 0, + width: 800, + height: 600, + }; + } +} + +fn main() { + let runtime: ApplicationRuntime = + ApplicationRuntimeBuilder::new("Instanced Quads Example") + .with_window_configured_as(|window_builder| { + return window_builder + .with_dimensions(800, 600) + .with_name("Instanced Quads Example"); + }) + .with_renderer_configured_as(|render_builder| { + return render_builder.with_render_timeout(1_000_000_000); + }) + .with_component(|runtime, example: InstancedQuadsExample| { + return (runtime, example); + }) + .build(); + + start_runtime(runtime); +} diff --git a/docs/specs/instanced-rendering.md b/docs/specs/instanced-rendering.md index 4c8017e0..9c6a70e7 100644 --- a/docs/specs/instanced-rendering.md +++ b/docs/specs/instanced-rendering.md @@ -3,13 +3,13 @@ title: "Instanced Rendering" document_id: "instanced-rendering-2025-11-23" status: "draft" created: "2025-11-23T00:00:00Z" -last_updated: "2025-11-25T00:00:00Z" -version: "0.1.2" +last_updated: "2025-11-25T01:00:00Z" +version: "0.1.3" engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "b1f0509d245065823dff2721f97e16c0215acc4f" +repo_commit: "7f8375d73a0dbca5eb143dda38e8e1600f62683c" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["spec", "rendering", "instancing", "vertex-input"] @@ -297,7 +297,7 @@ App Code `lambda-rs-platform`. - [x] Draw helpers in `lambda-rs-platform` accept and forward instance ranges. - - [ ] Existing draw paths continue to function with `instances = 0..1`. + - [x] Existing draw paths continue to function with `instances = 0..1`. - API Surface - [x] `VertexStepMode` exposed in `lambda-rs` and `lambda-rs-platform`. - [x] `RenderPipelineBuilder` in `lambda-rs` supports explicit per-instance @@ -314,7 +314,7 @@ App Code - Documentation and Examples - [ ] User-facing rendering docs updated to describe instanced rendering and usage patterns. - - [ ] At least one example or runnable scenario added that demonstrates + - [x] At least one example or runnable scenario added that demonstrates instanced rendering. - [ ] Any necessary migration notes captured in `docs/rendering.md` or related documentation. @@ -369,6 +369,7 @@ relevant code and tests, for example in the pull request description. ## Changelog +- 2025-11-25 (v0.1.3) — Mark existing draw paths as compatible with `instances = 0..1`, record the addition of an instanced rendering example, and update metadata. - 2025-11-25 (v0.1.2) — Update terminology to reference crates by name, remove per-file implementation locations from the Requirements Checklist, and mark instancing validation features as implemented in `lambda-rs`. - 2025-11-24 (v0.1.1) — Mark initial instancing layout and step mode support as implemented in the Requirements Checklist; metadata updated. - 2025-11-23 (v0.1.0) — Initial draft of instanced rendering specification. From 6fa1a359816d9cbb01f927545c56181cb65da8a5 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 24 Nov 2025 17:43:49 -0800 Subject: [PATCH 05/10] [add] tutorial for instanced rendering demo. --- docs/tutorials/README.md | 10 +- docs/tutorials/instanced-quads.md | 500 ++++++++++++++++++++++++++++++ 2 files changed, 506 insertions(+), 4 deletions(-) create mode 100644 docs/tutorials/instanced-quads.md diff --git a/docs/tutorials/README.md b/docs/tutorials/README.md index a0b26228..08cc18fa 100644 --- a/docs/tutorials/README.md +++ b/docs/tutorials/README.md @@ -3,28 +3,30 @@ title: "Tutorials Index" document_id: "tutorials-index-2025-10-17" status: "living" created: "2025-10-17T00:20:00Z" -last_updated: "2025-11-17T00:00:00Z" -version: "0.3.0" +last_updated: "2025-11-25T00:00:00Z" +version: "0.4.0" engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "ceaf345777d871912b2f92ae629a34b8e6f8654a" +repo_commit: "2ff0a581af014a754e982881193c36f47e685602" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["index", "tutorials", "docs"] --- -This index lists tutorials that teach specific engine tasks through complete, incremental builds. +This index lists tutorials that teach specific `lambda-rs` tasks through complete, incremental builds. - Uniform Buffers: Build a Spinning Triangle — [uniform-buffers.md](uniform-buffers.md) - Textured Quad: Sample a 2D Texture — [textured-quad.md](textured-quad.md) - Textured Cube: 3D Push Constants + 2D Sampling — [textured-cube.md](textured-cube.md) - Reflective Room: Stencil Masked Reflections with MSAA — [reflective-room.md](reflective-room.md) +-, Instanced Rendering: Grid of Colored Quads — [instanced-quads.md](instanced-quads.md) Browse all tutorials in this directory. Changelog +- 0.4.0 (2025-11-25): Add Instanced Quads tutorial; update metadata and commit. - 0.3.0 (2025-11-17): Add Reflective Room tutorial; update metadata and commit. - 0.2.0 (2025-11-10): Add links for textured quad and textured cube; update metadata and commit. - 0.1.0 (2025-10-17): Initial index with uniform buffers tutorial. diff --git a/docs/tutorials/instanced-quads.md b/docs/tutorials/instanced-quads.md new file mode 100644 index 00000000..d9e7f2e0 --- /dev/null +++ b/docs/tutorials/instanced-quads.md @@ -0,0 +1,500 @@ +--- +title: "Instanced Rendering: Grid of Colored Quads" +document_id: "instanced-quads-tutorial-2025-11-25" +status: "draft" +created: "2025-11-25T00:00:00Z" +last_updated: "2025-11-25T00:00:00Z" +version: "0.1.0" +engine_workspace_version: "2023.1.30" +wgpu_version: "26.0.1" +shader_backend_default: "naga" +winit_version: "0.29.10" +repo_commit: "2ff0a581af014a754e982881193c36f47e685602" +owners: ["lambda-sh"] +reviewers: ["engine", "rendering"] +tags: ["tutorial", "graphics", "instancing", "vertex-buffers", "rust", "wgpu"] +--- + +## Overview +This tutorial builds an instanced rendering example using the `lambda-rs` crate. The final application renders a grid of 2D quads that all share the same geometry but read per-instance offsets and colors from a second vertex buffer. The example demonstrates how to configure per-vertex and per-instance buffers, construct an instanced render pipeline, and issue draw commands with a multi-instance range. + +Reference implementation: `crates/lambda-rs/examples/instanced_quads.rs`. + +## Goals + +- Build an instanced rendering example that draws a grid of quads using shared geometry and per-instance data. +- Understand how per-vertex and per-instance attributes are described on `RenderPipelineBuilder`. +- Learn how `RenderCommand::DrawIndexed` uses an instance range to control how many instances are rendered. +- Reinforce correct usage flags and buffer types for vertex and index data. + +## Prerequisites + +- The workspace builds successfully: `cargo build --workspace`. +- Familiarity with the `lambda-rs` runtime and component model, for example from the indexed draws and uniform buffer tutorials. +- Ability to run examples: + - `cargo run -p lambda-rs --example minimal` + - `cargo run -p lambda-rs --example instanced_quads` + +## Requirements and Constraints + +- Per-vertex and per-instance vertex attribute layouts on the pipeline MUST match shader `location` qualifiers and data formats. +- The instance buffer MUST be bound to the same slot that `with_instance_buffer` configures on the pipeline before issuing draw commands that rely on per-instance data. +- Draw calls that use instancing MUST provide an `instances` range where `start` is less than or equal to `end`. Rationale: `RenderContext` validation rejects inverted ranges. +- The instance buffer MAY be updated over time to animate positions or colors; this tutorial uses a static grid for clarity. + +## Data Flow + +- Quad geometry (positions) and instance data (offsets and colors) are constructed in Rust. +- Two vertex buffers (one per-vertex and one per-instance) and an index buffer are created using `BufferBuilder`. +- A `RenderPipeline` associates slot `0` with per-vertex positions and slot `1` with per-instance data. +- At render time, commands bind the pipeline, vertex buffers, and index buffer, then issue `DrawIndexed` with an instance range that covers the grid. + +ASCII diagram + +``` +quad vertices, instance offsets, colors + │ upload via BufferBuilder + ▼ +Vertex Buffer (slot 0, per-vertex) Vertex Buffer (slot 1, per-instance) + │ │ + └──────────────┬───────────────────┘ + ▼ +RenderPipeline (per-vertex + per-instance layouts) + │ +RenderCommand::{BindVertexBuffer, BindIndexBuffer, DrawIndexed} + │ +Render Pass +``` + +## Implementation Steps + +### Step 1 — Shaders and Attribute Layout +Step 1 defines the vertex and fragment shaders for instanced quads. The vertex shader consumes per-vertex positions and per-instance offsets and colors, and the fragment shader writes the interpolated color. + +```glsl +#version 450 + +layout (location = 0) in vec3 vertex_position; +layout (location = 1) in vec3 instance_offset; +layout (location = 2) in vec3 instance_color; + +layout (location = 0) out vec3 frag_color; + +void main() { + vec3 position = vertex_position + instance_offset; + gl_Position = vec4(position, 1.0); + frag_color = instance_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); +} +``` + +Attribute locations `0`, `1`, and `2` correspond to pipeline vertex attribute definitions for the per-vertex position and the per-instance offset and color. These locations will be matched by `VertexAttribute` entries when the render pipeline is constructed. + +### Step 2 — Vertex and Instance Types and Component State +Step 2 introduces the Rust vertex and instance structures and prepares the component state. The component stores compiled shaders and identifiers for the render pass, pipeline, and buffers. + +```rust +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, + }, +}; + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +struct QuadVertex { + position: [f32; 3], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +struct InstanceData { + offset: [f32; 3], + color: [f32; 3], +} + +pub struct InstancedQuadsExample { + vertex_shader: Shader, + fragment_shader: Shader, + render_pass_id: Option, + render_pipeline_id: Option, + index_buffer_id: Option, + index_count: u32, + instance_count: u32, + width: u32, + height: u32, +} + +impl Default for InstancedQuadsExample { + fn default() -> Self { + let vertex_virtual_shader = VirtualShader::Source { + source: VERTEX_SHADER_SOURCE.to_string(), + kind: ShaderKind::Vertex, + entry_point: "main".to_string(), + name: "instanced_quads".to_string(), + }; + + let fragment_virtual_shader = VirtualShader::Source { + source: FRAGMENT_SHADER_SOURCE.to_string(), + kind: ShaderKind::Fragment, + entry_point: "main".to_string(), + name: "instanced_quads".to_string(), + }; + + let mut shader_builder = ShaderBuilder::new(); + let vertex_shader = shader_builder.build(vertex_virtual_shader); + let fragment_shader = 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, + instance_count: 0, + width: 800, + height: 600, + }; + } +} +``` + +The `QuadVertex` and `InstanceData` structures mirror the GLSL inputs as arrays of `f32`, and the component tracks resource identifiers and counts that are populated during attachment. The `Default` implementation constructs shader objects from the GLSL source so that the component is ready to build a pipeline when it receives a `RenderContext`. + +### Step 3 — Render Pass, Geometry, Instances, and Buffers +Step 3 implements the `on_attach` method for the component. This method creates the render pass, quad geometry, instance data, GPU buffers, and the render pipeline. It also records the number of indices and instances for use during rendering. + +```rust +fn on_attach( + &mut self, + render_context: &mut RenderContext, +) -> Result { + let render_pass = RenderPassBuilder::new().build(render_context); + + // Quad geometry in clip space centered at the origin. + let quad_vertices: Vec = vec![ + QuadVertex { + position: [-0.05, -0.05, 0.0], + }, + QuadVertex { + position: [0.05, -0.05, 0.0], + }, + QuadVertex { + position: [0.05, 0.05, 0.0], + }, + QuadVertex { + position: [-0.05, 0.05, 0.0], + }, + ]; + + // Two triangles forming a quad. + let indices: Vec = vec![0, 1, 2, 2, 3, 0]; + let index_count = indices.len() as u32; + + // Build a grid of instance offsets and colors. + let grid_size: u32 = 10; + let spacing: f32 = 0.2; + let start: f32 = -0.9; + + let mut instances: Vec = Vec::new(); + for y in 0..grid_size { + for x in 0..grid_size { + let offset_x = start + (x as f32) * spacing; + let offset_y = start + (y as f32) * spacing; + + // Simple color gradient across the grid. + let color_r = (x as f32) / ((grid_size - 1) as f32); + let color_g = (y as f32) / ((grid_size - 1) as f32); + let color_b = 0.5; + + instances.push(InstanceData { + offset: [offset_x, offset_y, 0.0], + color: [color_r, color_g, color_b], + }); + } + } + let instance_count = instances.len() as u32; + + // Build vertex, instance, and index buffers. + let vertex_buffer = BufferBuilder::new() + .with_usage(Usage::VERTEX) + .with_properties(Properties::DEVICE_LOCAL) + .with_buffer_type(BufferType::Vertex) + .with_label("instanced-quads-vertices") + .build(render_context, quad_vertices) + .map_err(|error| error.to_string())?; + + let instance_buffer = BufferBuilder::new() + .with_usage(Usage::VERTEX) + .with_properties(Properties::DEVICE_LOCAL) + .with_buffer_type(BufferType::Vertex) + .with_label("instanced-quads-instances") + .build(render_context, instances) + .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("instanced-quads-indices") + .build(render_context, indices) + .map_err(|error| error.to_string())?; + + // Vertex attributes for per-vertex positions in slot 0. + let vertex_attributes = vec![VertexAttribute { + location: 0, + offset: 0, + element: VertexElement { + format: ColorFormat::Rgb32Sfloat, + offset: 0, + }, + }]; + + // Instance attributes in slot 1: offset and color. + let instance_attributes = vec![ + VertexAttribute { + location: 1, + offset: 0, + element: VertexElement { + format: ColorFormat::Rgb32Sfloat, + offset: 0, + }, + }, + VertexAttribute { + location: 2, + offset: 0, + element: VertexElement { + format: ColorFormat::Rgb32Sfloat, + offset: 12, + }, + }, + ]; + + let pipeline = RenderPipelineBuilder::new() + .with_culling(CullingMode::Back) + .with_buffer(vertex_buffer, vertex_attributes) + .with_instance_buffer(instance_buffer, instance_attributes) + .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; + self.instance_count = instance_count; + + logging::info!( + "Instanced quads example attached with {} instances", + self.instance_count + ); + return Ok(ComponentResult::Success); +} +``` + +The first buffer created by `with_buffer` is treated as a per-vertex buffer in slot `0`, while `with_instance_buffer` registers the instance buffer in slot `1` with per-instance step mode. The `vertex_attributes` and `instance_attributes` vectors connect shader locations `0`, `1`, and `2` to their corresponding buffer slots and formats, and the component records index and instance counts for later draws. + +### 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!("Instanced quads 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 { + // This example uses static instance data; no per-frame updates required. + return Ok(ComponentResult::Success); +} +``` + +The component does not modify instance data over time, so `on_update` is a no-op. The resize path is the only dynamic input and ensures that the viewport used during rendering matches the current window size. + +### Step 5 — Render Commands and Runtime Entry Point +Step 5 records the render commands that bind the pipeline, vertex buffers, and index buffer, then wires the component into the `lambda-rs` runtime as a windowed application. + +```rust +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..self.instance_count, + }, + RenderCommand::EndRenderPass, + ]; +} + +fn main() { + let runtime: ApplicationRuntime = + ApplicationRuntimeBuilder::new("Instanced Quads Example") + .with_window_configured_as(|window_builder| { + return window_builder + .with_dimensions(800, 600) + .with_name("Instanced Quads Example"); + }) + .with_renderer_configured_as(|render_builder| { + return render_builder.with_render_timeout(1_000_000_000); + }) + .with_component(|runtime, example: InstancedQuadsExample| { + return (runtime, example); + }) + .build(); + + start_runtime(runtime); +} +``` + +The commands bind both vertex buffers and the index buffer before issuing `DrawIndexed`. The `instances: 0..self.instance_count` range enables instanced rendering, and the runtime builder configures the window and renderer and installs the component so that `lambda-rs` drives `on_attach`, `on_event`, `on_update`, and `on_render` each frame. + +## Validation + +- Commands: + - `cargo run -p lambda-rs --example instanced_quads` + - `cargo test -p lambda-rs -- --nocapture` +- Expected behavior: + - A grid of small quads appears in the window, with colors varying smoothly across the grid based on instance indices. + - Changing the grid size or instance count SHOULD change the number of quads rendered without altering the per-vertex geometry. + +## Notes + +- Vertex attribute locations in the shaders MUST match the `VertexAttribute` configuration for both the per-vertex and per-instance buffers. +- The instance buffer MUST be bound on the same slot that `with_instance_buffer` uses; binding a different slot will lead to incorrect or undefined attribute data during rendering. +- Instance ranges for `DrawIndexed` MUST remain within the logical count of instances created for the instance buffer; validation features such as `render-instancing-validation` SHOULD be enabled when developing new instanced render paths. +- Per-instance data MAY be updated each frame to animate offsets or colors; static data is sufficient for verifying buffer layouts and instance ranges. + +## Conclusion + +This tutorial demonstrates how the `lambda-rs` crate uses per-vertex and per-instance vertex buffers to render a grid of quads with shared geometry and per-instance colors. The `instanced_quads` example serves as a concrete reference for applications that require instanced rendering of repeated geometry with varying per-instance attributes. + +## Exercises + +- Modify the grid dimensions and spacing to explore different layouts and densities of quads. +- Animate instance offsets over time in `on_update` to create a simple wave pattern across the grid. +- Introduce a uniform buffer that applies a global transform to all instances and combine it with per-instance offsets. +- Extend the shaders to include per-instance scale or rotation and add fields to `InstanceData` to drive those transforms. +- Add a second instanced draw call that uses the same geometry but a different instance buffer to render a second grid with an alternate color pattern. +- Experiment with validation features, such as `render-instancing-validation`, by intentionally omitting the instance buffer binding and observing how configuration errors are reported. + +## Changelog + +- 2025-11-25 (v0.1.0) — Initial instanced quads tutorial describing per-vertex and per-instance buffers and the `instanced_quads` example. From 75ff33866badd28de9310377a355872b0e5fe752 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 24 Nov 2025 17:53:55 -0800 Subject: [PATCH 06/10] [fix] instance validation assertions. --- crates/lambda-rs/src/render/mod.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 2f304fa4..0e59fa03 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -918,11 +918,13 @@ impl RenderContext { { return Err(RenderError::Configuration(msg)); } + } + #[cfg(any( + debug_assertions, + feature = "render-instancing-validation", + ))] + { if instances.start == instances.end { - #[cfg(any( - debug_assertions, - feature = "render-instancing-validation", - ))] logging::debug!( "Skipping Draw with empty instance range {}..{}", instances.start, @@ -1002,11 +1004,13 @@ impl RenderContext { { return Err(RenderError::Configuration(msg)); } + } + #[cfg(any( + debug_assertions, + feature = "render-instancing-validation", + ))] + { if instances.start == instances.end { - #[cfg(any( - debug_assertions, - feature = "render-instancing-validation", - ))] logging::debug!( "Skipping DrawIndexed with empty instance range {}..{}", instances.start, From c8f727f3774029135ed1f7a7224288faf7b9e442 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 24 Nov 2025 17:56:43 -0800 Subject: [PATCH 07/10] [fix] instance validation blocks --- crates/lambda-rs/src/render/mod.rs | 71 ++++++++++++++++-------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 0e59fa03..7301c497 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -894,24 +894,23 @@ impl RenderContext { .to_string(), )); } + } - #[cfg(any( - debug_assertions, - feature = "render-instancing-validation", - ))] - { - let pipeline_index = current_pipeline.expect( - "current_pipeline must be set when validation is active", - ); - let pipeline_ref = &self.render_pipelines[pipeline_index]; - - validation::validate_instance_bindings( - pipeline_ref.pipeline().label().unwrap_or("unnamed"), - pipeline_ref.per_instance_slots(), - &bound_vertex_slots, - ) - .map_err(RenderError::Configuration)?; - } + #[cfg(any( + debug_assertions, + feature = "render-instancing-validation", + ))] + { + let pipeline_index = current_pipeline + .expect("current_pipeline must be set when validation is active"); + let pipeline_ref = &self.render_pipelines[pipeline_index]; + + validation::validate_instance_bindings( + pipeline_ref.pipeline().label().unwrap_or("unnamed"), + pipeline_ref.per_instance_slots(), + &bound_vertex_slots, + ) + .map_err(RenderError::Configuration)?; if let Err(msg) = validation::validate_instance_range("Draw", &instances) @@ -981,23 +980,29 @@ impl RenderContext { feature = "render-instancing-validation", ))] { - #[cfg(any( - debug_assertions, - feature = "render-instancing-validation", - ))] - { - let pipeline_index = current_pipeline.expect( - "current_pipeline must be set when validation is active", - ); - let pipeline_ref = &self.render_pipelines[pipeline_index]; - - validation::validate_instance_bindings( - pipeline_ref.pipeline().label().unwrap_or("unnamed"), - pipeline_ref.per_instance_slots(), - &bound_vertex_slots, - ) - .map_err(RenderError::Configuration)?; + if current_pipeline.is_none() { + return Err(RenderError::Configuration( + "DrawIndexed command encountered before any pipeline was set in this render pass" + .to_string(), + )); } + } + + #[cfg(any( + debug_assertions, + feature = "render-instancing-validation", + ))] + { + let pipeline_index = current_pipeline + .expect("current_pipeline must be set when validation is active"); + let pipeline_ref = &self.render_pipelines[pipeline_index]; + + validation::validate_instance_bindings( + pipeline_ref.pipeline().label().unwrap_or("unnamed"), + pipeline_ref.per_instance_slots(), + &bound_vertex_slots, + ) + .map_err(RenderError::Configuration)?; if let Err(msg) = validation::validate_instance_range("DrawIndexed", &instances) From 3fa70c2d4d6c2e7ced2a88bbd9e6f387d26a4c7f Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 24 Nov 2025 18:16:10 -0800 Subject: [PATCH 08/10] [fix] instancing validation flag name & add it to render-validation-all. --- crates/lambda-rs/Cargo.toml | 3 ++- crates/lambda-rs/src/render/mod.rs | 36 ++++++------------------------ docs/features.md | 18 +++++++++------ docs/specs/instanced-rendering.md | 20 +++++++++++------ docs/tutorials/instanced-quads.md | 11 ++++----- 5 files changed, 39 insertions(+), 49 deletions(-) diff --git a/crates/lambda-rs/Cargo.toml b/crates/lambda-rs/Cargo.toml index 1f9ba5dd..ad4332df 100644 --- a/crates/lambda-rs/Cargo.toml +++ b/crates/lambda-rs/Cargo.toml @@ -58,6 +58,7 @@ render-validation-strict = [ render-validation-all = [ "render-validation-strict", "render-validation-device", + "render-validation-instancing", ] # Granular feature flags @@ -73,7 +74,7 @@ render-validation-stencil = [] render-validation-pass-compat = [] render-validation-device = [] render-validation-encoder = [] -render-instancing-validation = [] +render-validation-instancing = [] # ---------------------------- PLATFORM DEPENDENCIES --------------------------- diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 7301c497..1f1f66e3 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -621,17 +621,13 @@ impl RenderContext { { Self::apply_viewport(pass, &initial_viewport); - #[cfg(any( - debug_assertions, - feature = "render-validation-encoder", - feature = "render-instancing-validation", - ))] + #[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; - #[cfg(any(debug_assertions, feature = "render-instancing-validation",))] + #[cfg(any(debug_assertions, feature = "render-validation-instancing",))] let mut bound_vertex_slots: HashSet = HashSet::new(); // De-duplicate advisories within this pass @@ -882,11 +878,7 @@ impl RenderContext { vertices, instances, } => { - #[cfg(any( - debug_assertions, - feature = "render-validation-encoder", - feature = "render-instancing-validation", - ))] + #[cfg(any(debug_assertions, feature = "render-validation-encoder",))] { if current_pipeline.is_none() { return Err(RenderError::Configuration( @@ -898,7 +890,7 @@ impl RenderContext { #[cfg(any( debug_assertions, - feature = "render-instancing-validation", + feature = "render-validation-instancing", ))] { let pipeline_index = current_pipeline @@ -920,7 +912,7 @@ impl RenderContext { } #[cfg(any( debug_assertions, - feature = "render-instancing-validation", + feature = "render-validation-instancing", ))] { if instances.start == instances.end { @@ -976,21 +968,7 @@ impl RenderContext { } #[cfg(any( debug_assertions, - feature = "render-validation-encoder", - feature = "render-instancing-validation", - ))] - { - if current_pipeline.is_none() { - return Err(RenderError::Configuration( - "DrawIndexed command encountered before any pipeline was set in this render pass" - .to_string(), - )); - } - } - - #[cfg(any( - debug_assertions, - feature = "render-instancing-validation", + feature = "render-validation-instancing", ))] { let pipeline_index = current_pipeline @@ -1012,7 +990,7 @@ impl RenderContext { } #[cfg(any( debug_assertions, - feature = "render-instancing-validation", + feature = "render-validation-instancing", ))] { if instances.start == instances.end { diff --git a/docs/features.md b/docs/features.md index 8ac020be..5da596aa 100644 --- a/docs/features.md +++ b/docs/features.md @@ -3,13 +3,13 @@ title: "Cargo Features Overview" document_id: "features-2025-11-17" status: "living" created: "2025-11-17T23:59:00Z" -last_updated: "2025-11-25T00:00:00Z" -version: "0.1.1" +last_updated: "2025-11-25T02:20:00Z" +version: "0.1.3" engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "b1f0509d245065823dff2721f97e16c0215acc4f" +repo_commit: "c8f727f3774029135ed1f7a7224288faf7b9e442" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["guide", "features", "validation", "cargo"] @@ -44,9 +44,9 @@ This document enumerates the primary Cargo features exposed by the workspace rel ## Render Validation Umbrella features (crate: `lambda-rs`) -- `render-validation`: enables common builder/pipeline validation logs (MSAA counts, depth clear advisories, stencil format upgrades). -- `render-validation-strict`: includes `render-validation` and enables per-draw SetPipeline-time compatibility checks. -- `render-validation-all`: superset of `render-validation-strict` and enables device-probing advisories. +- `render-validation`: enables common builder/pipeline validation logs (MSAA counts, depth clear advisories, stencil format upgrades) by composing granular validation features. +- `render-validation-strict`: includes `render-validation` and enables per-draw SetPipeline-time compatibility checks by composing additional granular encoder features. +- `render-validation-all`: superset of `render-validation-strict` and enables device-probing advisories and instancing validation. This umbrella includes all granular render-validation flags, including `render-validation-instancing`. Granular features (crate: `lambda-rs`) - `render-validation-msaa`: validates/logs MSAA sample counts; logs pass/pipeline sample mismatches. Behavior: @@ -57,7 +57,7 @@ Granular features (crate: `lambda-rs`) - `render-validation-pass-compat`: SetPipeline-time errors when color targets or depth/stencil expectations do not match the active pass. - `render-validation-device`: device/format probing advisories (if available via the platform layer). - `render-validation-encoder`: additional per-draw/encoder-time checks; highest runtime cost. -- `render-instancing-validation`: instance-range and per-instance buffer binding validation for `RenderCommand::Draw` and `RenderCommand::DrawIndexed`. Behavior: +- `render-validation-instancing`: instance-range and per-instance buffer binding validation for `RenderCommand::Draw` and `RenderCommand::DrawIndexed`. Behavior: - Validates that `instances.start <= instances.end` and treats `start == end` as a no-op (draw is skipped). - Ensures that all vertex buffer slots marked as per-instance on the active pipeline have been bound in the current render pass. - Adds per-draw checks proportional to the number of instanced draws and per-instance slots; SHOULD be enabled only when diagnosing instancing issues. @@ -76,9 +76,13 @@ Usage examples - `cargo build -p lambda-rs --features render-validation` - Enable strict compatibility checks in release: - `cargo run -p lambda-rs --features render-validation-strict` +- Enable all validations, including device advisories and instancing validation, in release: + - `cargo test -p lambda-rs --features render-validation-all` - Enable only MSAA validation in release: - `cargo test -p lambda-rs --features render-validation-msaa` ## Changelog +- 0.1.3 (2025-11-25): Rename the instancing validation feature to `render-validation-instancing`, clarify umbrella composition, and update metadata. +- 0.1.2 (2025-11-25): Clarify umbrella versus granular validation features, record that `render-validation-all` includes `render-instancing-validation`, and update metadata. - 0.1.1 (2025-11-25): Document `render-instancing-validation` behavior and update metadata. - 0.1.0 (2025-11-17): Initial document introducing validation features and behavior by build type. diff --git a/docs/specs/instanced-rendering.md b/docs/specs/instanced-rendering.md index 9c6a70e7..b62d9745 100644 --- a/docs/specs/instanced-rendering.md +++ b/docs/specs/instanced-rendering.md @@ -3,13 +3,13 @@ title: "Instanced Rendering" document_id: "instanced-rendering-2025-11-23" status: "draft" created: "2025-11-23T00:00:00Z" -last_updated: "2025-11-25T01:00:00Z" -version: "0.1.3" +last_updated: "2025-11-25T02:20:00Z" +version: "0.1.5" engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "7f8375d73a0dbca5eb143dda38e8e1600f62683c" +repo_commit: "c8f727f3774029135ed1f7a7224288faf7b9e442" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["spec", "rendering", "instancing", "vertex-input"] @@ -168,7 +168,7 @@ App Code - The `lambda-rs` crate MUST treat `instances = 0..1` as the default single-instance behavior used by existing rendering paths. - Feature flags (`lambda-rs`) - - `render-instancing-validation` + - `render-validation-instancing` - Owning crate: `lambda-rs`. - Default: disabled in release builds; MAY be enabled in debug builds or by opt-in. @@ -211,8 +211,8 @@ App Code and instance) before issuing `Draw` or `DrawIndexed` commands that rely on those slots. - Validation behavior - - When `render-instancing-validation` and `render-validation-encoder` are - enabled, the `lambda-rs` crate SHOULD: + - When `render-validation-instancing` is enabled (or when `debug_assertions` + are active), the `lambda-rs` crate SHOULD: - Verify that all buffer slots used by per-instance attributes are bound before a draw that uses those attributes. - Emit a clear error when a draw is issued with an `instances` range whose @@ -220,6 +220,10 @@ App Code information is available. - Check that `instances.start <= instances.end` and treat negative-length ranges as configuration errors. + - Umbrella features such as `render-validation-all` MAY include + `render-validation-instancing` for convenience, but code and tests MUST + gate instancing behavior on the granular `render-validation-instancing` + feature (plus `debug_assertions`), not on umbrella feature names. ### Validation and Errors @@ -233,7 +237,7 @@ App Code - `BindVertexBuffer` MUST reference a buffer created with `BufferType::Vertex` and a slot index that is less than the number of vertex buffer layouts declared on the pipeline. - - When `render-instancing-validation` is enabled, the `lambda-rs` crate SHOULD: + - When `render-validation-instancing` is enabled, the `lambda-rs` crate SHOULD: - Verify that the set of bound buffers covers all pipeline slots that declare per-instance attributes before a draw is issued. - Log an error if a draw is issued with a per-instance attribute whose slot @@ -369,6 +373,8 @@ relevant code and tests, for example in the pull request description. ## Changelog +- 2025-11-25 (v0.1.5) — Rename the granular instancing validation feature to `render-validation-instancing`, clarify naming in feature documentation, and update metadata. +- 2025-11-25 (v0.1.4) — Clarify that instancing validation is gated by the granular `render-instancing-validation` feature (and `debug_assertions`) and may be included in umbrella features such as `render-validation-all`; update metadata. - 2025-11-25 (v0.1.3) — Mark existing draw paths as compatible with `instances = 0..1`, record the addition of an instanced rendering example, and update metadata. - 2025-11-25 (v0.1.2) — Update terminology to reference crates by name, remove per-file implementation locations from the Requirements Checklist, and mark instancing validation features as implemented in `lambda-rs`. - 2025-11-24 (v0.1.1) — Mark initial instancing layout and step mode support as implemented in the Requirements Checklist; metadata updated. diff --git a/docs/tutorials/instanced-quads.md b/docs/tutorials/instanced-quads.md index d9e7f2e0..6357a7b1 100644 --- a/docs/tutorials/instanced-quads.md +++ b/docs/tutorials/instanced-quads.md @@ -3,13 +3,13 @@ title: "Instanced Rendering: Grid of Colored Quads" document_id: "instanced-quads-tutorial-2025-11-25" status: "draft" created: "2025-11-25T00:00:00Z" -last_updated: "2025-11-25T00:00:00Z" -version: "0.1.0" +last_updated: "2025-11-25T02:20:00Z" +version: "0.1.1" engine_workspace_version: "2023.1.30" wgpu_version: "26.0.1" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "2ff0a581af014a754e982881193c36f47e685602" +repo_commit: "c8f727f3774029135ed1f7a7224288faf7b9e442" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] tags: ["tutorial", "graphics", "instancing", "vertex-buffers", "rust", "wgpu"] @@ -479,7 +479,7 @@ The commands bind both vertex buffers and the index buffer before issuing `DrawI - Vertex attribute locations in the shaders MUST match the `VertexAttribute` configuration for both the per-vertex and per-instance buffers. - The instance buffer MUST be bound on the same slot that `with_instance_buffer` uses; binding a different slot will lead to incorrect or undefined attribute data during rendering. -- Instance ranges for `DrawIndexed` MUST remain within the logical count of instances created for the instance buffer; validation features such as `render-instancing-validation` SHOULD be enabled when developing new instanced render paths. +- Instance ranges for `DrawIndexed` MUST remain within the logical count of instances created for the instance buffer; validation features such as `render-validation-instancing` SHOULD be enabled when developing new instanced render paths. - Per-instance data MAY be updated each frame to animate offsets or colors; static data is sufficient for verifying buffer layouts and instance ranges. ## Conclusion @@ -493,8 +493,9 @@ This tutorial demonstrates how the `lambda-rs` crate uses per-vertex and per-ins - Introduce a uniform buffer that applies a global transform to all instances and combine it with per-instance offsets. - Extend the shaders to include per-instance scale or rotation and add fields to `InstanceData` to drive those transforms. - Add a second instanced draw call that uses the same geometry but a different instance buffer to render a second grid with an alternate color pattern. -- Experiment with validation features, such as `render-instancing-validation`, by intentionally omitting the instance buffer binding and observing how configuration errors are reported. +- Experiment with validation features, such as `render-validation-instancing`, by intentionally omitting the instance buffer binding and observing how configuration errors are reported. ## Changelog +- 2025-11-25 (v0.1.1) — Align feature naming with `render-validation-instancing` and update metadata. - 2025-11-25 (v0.1.0) — Initial instanced quads tutorial describing per-vertex and per-instance buffers and the `instanced_quads` example. From 1d131c076e41dad022de60dd91d1d0c849b4ef5f Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 25 Nov 2025 13:18:03 -0800 Subject: [PATCH 09/10] [update] documentation to clarify offsets. --- docs/specs/indexed-draws-and-multiple-vertex-buffers.md | 2 +- docs/tutorials/instanced-quads.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/specs/indexed-draws-and-multiple-vertex-buffers.md b/docs/specs/indexed-draws-and-multiple-vertex-buffers.md index 701a7b70..0f14c382 100644 --- a/docs/specs/indexed-draws-and-multiple-vertex-buffers.md +++ b/docs/specs/indexed-draws-and-multiple-vertex-buffers.md @@ -108,7 +108,7 @@ App Code - `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 }`. + - `struct VertexAttribute { location: u32, offset: u32, element: VertexElement }`. The effective byte offset of a vertex attribute is computed as `offset + element.offset`, where `offset` is a base offset within the buffer element and `element.offset` is the offset of the field within the logical vertex or instance struct. - `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. diff --git a/docs/tutorials/instanced-quads.md b/docs/tutorials/instanced-quads.md index 6357a7b1..8aa339d5 100644 --- a/docs/tutorials/instanced-quads.md +++ b/docs/tutorials/instanced-quads.md @@ -343,7 +343,7 @@ fn on_attach( } ``` -The first buffer created by `with_buffer` is treated as a per-vertex buffer in slot `0`, while `with_instance_buffer` registers the instance buffer in slot `1` with per-instance step mode. The `vertex_attributes` and `instance_attributes` vectors connect shader locations `0`, `1`, and `2` to their corresponding buffer slots and formats, and the component records index and instance counts for later draws. +The first buffer created by `with_buffer` is treated as a per-vertex buffer in slot `0`, while `with_instance_buffer` registers the instance buffer in slot `1` with per-instance step mode. The `vertex_attributes` and `instance_attributes` vectors connect shader locations `0`, `1`, and `2` to their corresponding buffer slots and formats, and the component records index and instance counts for later draws. The effective byte offset of each attribute is computed as `attribute.offset + attribute.element.offset`. In this example `attribute.offset` is kept at `0` for all attributes, and the struct layout is expressed entirely through `VertexElement::offset` (for example, the `color` field in `InstanceData` starts 12 bytes after the `offset` field). More complex layouts MAY use a non-zero `attribute.offset` to reuse the same attribute description at different base positions within a vertex or instance element. ### 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. From d457b83168920a9f851109baec5d511519a2d451 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 25 Nov 2025 13:19:05 -0800 Subject: [PATCH 10/10] [update] tutorial link. --- docs/tutorials/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/README.md b/docs/tutorials/README.md index 08cc18fa..c7ba793f 100644 --- a/docs/tutorials/README.md +++ b/docs/tutorials/README.md @@ -21,7 +21,7 @@ This index lists tutorials that teach specific `lambda-rs` tasks through complet - Textured Quad: Sample a 2D Texture — [textured-quad.md](textured-quad.md) - Textured Cube: 3D Push Constants + 2D Sampling — [textured-cube.md](textured-cube.md) - Reflective Room: Stencil Masked Reflections with MSAA — [reflective-room.md](reflective-room.md) --, Instanced Rendering: Grid of Colored Quads — [instanced-quads.md](instanced-quads.md) +- Instanced Rendering: Grid of Colored Quads — [instanced-quads.md](instanced-quads.md) Browse all tutorials in this directory.