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