Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/lambda-rs/examples/indexed_multi_vertex_buffers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,16 @@ struct PositionVertex {
position: [f32; 3],
}

unsafe impl lambda::pod::PlainOldData for PositionVertex {}

#[repr(C)]
#[derive(Clone, Copy, Debug)]
struct ColorVertex {
color: [f32; 3],
}

unsafe impl lambda::pod::PlainOldData for ColorVertex {}

// --------------------------------- COMPONENT ---------------------------------

pub struct IndexedMultiBufferExample {
Expand Down
4 changes: 4 additions & 0 deletions crates/lambda-rs/examples/instanced_quads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,17 @@ struct QuadVertex {
position: [f32; 3],
}

unsafe impl lambda::pod::PlainOldData for QuadVertex {}

#[repr(C)]
#[derive(Clone, Copy, Debug)]
struct InstanceData {
offset: [f32; 3],
color: [f32; 3],
}

unsafe impl lambda::pod::PlainOldData for InstanceData {}

// --------------------------------- COMPONENT ---------------------------------

/// Component that renders a grid of instanced quads.
Expand Down
2 changes: 2 additions & 0 deletions crates/lambda-rs/examples/uniform_buffer_triangle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ pub struct GlobalsUniform {
pub render_matrix: [[f32; 4]; 4],
}

unsafe impl lambda::pod::PlainOldData for GlobalsUniform {}

// --------------------------------- COMPONENT ---------------------------------

pub struct UniformBufferExample {
Expand Down
1 change: 1 addition & 0 deletions crates/lambda-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
pub mod component;
pub mod events;
pub mod math;
pub mod pod;
pub mod render;
pub mod runtime;
pub mod runtimes;
Expand Down
42 changes: 42 additions & 0 deletions crates/lambda-rs/src/pod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! Plain-old-data marker trait for safe byte uploads.
//!
//! The engine frequently uploads CPU data into GPU buffers by reinterpreting
//! a value or slice as raw bytes. This is only sound for types that are safe
//! to view as bytes.

/// Marker trait for types that are safe to reinterpret as raw bytes.
///
/// This trait is required by typed buffer upload APIs (for example
/// `render::buffer::Buffer::write_value` and `render::buffer::Buffer::write_slice`)
/// and typed buffer creation APIs (for example
/// `render::buffer::BufferBuilder::build`) because those operations upload the
/// in-memory representation of a value to the GPU.
///
/// # Safety
/// Types implementing `PlainOldData` MUST satisfy all of the following:
/// - Every byte of the value is initialized (including any padding bytes).
/// - The type has no pointers or references that would be invalidated by a
/// raw byte copy.
/// - The type's byte representation is stable for GPU consumption. Prefer
/// `#[repr(C)]` or `#[repr(transparent)]`.
///
/// Implementing this trait incorrectly can cause undefined behavior.
pub unsafe trait PlainOldData: Copy {}

unsafe impl PlainOldData for u8 {}
unsafe impl PlainOldData for i8 {}
unsafe impl PlainOldData for u16 {}
unsafe impl PlainOldData for i16 {}
unsafe impl PlainOldData for u32 {}
unsafe impl PlainOldData for i32 {}
unsafe impl PlainOldData for u64 {}
unsafe impl PlainOldData for i64 {}
unsafe impl PlainOldData for u128 {}
unsafe impl PlainOldData for i128 {}
unsafe impl PlainOldData for usize {}
unsafe impl PlainOldData for isize {}
unsafe impl PlainOldData for f32 {}
unsafe impl PlainOldData for f64 {}
unsafe impl PlainOldData for bool {}
unsafe impl PlainOldData for char {}
unsafe impl<T: PlainOldData, const N: usize> PlainOldData for [T; N] {}
137 changes: 119 additions & 18 deletions crates/lambda-rs/src/render/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use super::{
mesh::Mesh,
RenderContext,
};
pub use crate::pod::PlainOldData;

/// High‑level classification for buffers created by the engine.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -140,17 +141,80 @@ impl Buffer {

/// Write a single plain-old-data value into this buffer at the specified
/// byte offset. This is intended for updating uniform buffer contents from
/// the CPU. The `data` type must be trivially copyable.
pub fn write_value<T: Copy>(&self, gpu: &Gpu, offset: u64, data: &T) {
let bytes = unsafe {
std::slice::from_raw_parts(
(data as *const T) as *const u8,
std::mem::size_of::<T>(),
)
};
/// the CPU. The `data` type must implement `PlainOldData`.
pub fn write_value<T: PlainOldData>(&self, gpu: &Gpu, offset: u64, data: &T) {
let bytes = value_as_bytes(data);
self.write_bytes(gpu, offset, bytes);
}

self.buffer.write_bytes(gpu.platform(), offset, bytes);
/// Write raw bytes into this buffer at the specified byte offset.
///
/// This is useful when data is already available as a byte slice (for
/// example, asset blobs or staging buffers).
///
/// Example
/// ```rust,ignore
/// let raw_data: &[u8] = load_binary_data();
/// buffer.write_bytes(render_context.gpu(), 0, raw_data);
/// ```
pub fn write_bytes(&self, gpu: &Gpu, offset: u64, data: &[u8]) {
self.buffer.write_bytes(gpu.platform(), offset, data);
}

/// Write a slice of plain-old-data values into this buffer at the
/// specified byte offset.
///
/// This is intended for uploading arrays of vertices, indices, instance
/// data, or uniform blocks. The `T` type MUST be plain-old-data (POD) and
/// safely representable as bytes. This is enforced by requiring `T` to
/// implement `PlainOldData`.
///
/// Example
/// ```rust,ignore
/// let transforms: Vec<InstanceTransform> = compute_transforms();
/// instance_buffer
/// .write_slice(render_context.gpu(), 0, &transforms)
/// .unwrap();
/// ```
pub fn write_slice<T: PlainOldData>(
&self,
gpu: &Gpu,
offset: u64,
data: &[T],
) -> Result<(), &'static str> {
let bytes = slice_as_bytes(data)?;
self.write_bytes(gpu, offset, bytes);
return Ok(());
}
}

fn value_as_bytes<T: PlainOldData>(data: &T) -> &[u8] {
let bytes = unsafe {
std::slice::from_raw_parts(
(data as *const T) as *const u8,
std::mem::size_of::<T>(),
)
};
return bytes;
}

fn checked_byte_len(
element_size: usize,
element_count: usize,
) -> Result<usize, &'static str> {
let Some(byte_len) = element_size.checked_mul(element_count) else {
return Err("Buffer byte length overflow.");
};
return Ok(byte_len);
}

fn slice_as_bytes<T: PlainOldData>(data: &[T]) -> Result<&[u8], &'static str> {
let element_size = std::mem::size_of::<T>();
let byte_len = checked_byte_len(element_size, data.len())?;

let bytes =
unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len) };
return Ok(bytes);
}

/// Strongly‑typed uniform buffer wrapper for ergonomics and safety.
Expand All @@ -176,7 +240,7 @@ pub struct UniformBuffer<T> {
_phantom: core::marker::PhantomData<T>,
}

impl<T: Copy> UniformBuffer<T> {
impl<T: PlainOldData> UniformBuffer<T> {
/// Create a new uniform buffer initialized with `initial`.
pub fn new(
gpu: &Gpu,
Expand Down Expand Up @@ -287,22 +351,23 @@ impl BufferBuilder {
/// Create a buffer initialized with the provided `data`.
///
/// Returns an error if the resolved length would be zero.
pub fn build<Data: Copy>(
///
/// The element type MUST implement `PlainOldData` because the engine uploads
/// the in-memory representation to the GPU.
pub fn build<Data: PlainOldData>(
&self,
gpu: &Gpu,
data: Vec<Data>,
) -> Result<Buffer, &'static str> {
let element_size = std::mem::size_of::<Data>();
let buffer_length = self.resolve_length(element_size, data.len())?;
let byte_len = checked_byte_len(element_size, data.len())?;

// SAFETY: Converting data to bytes is safe because its underlying
// type, Data, is constrained to Copy and the lifetime of the slice does
// not outlive data.
// type, Data, is constrained to PlainOldData and the lifetime of the slice
// does not outlive data.
let bytes = unsafe {
std::slice::from_raw_parts(
data.as_ptr() as *const u8,
element_size * data.len(),
)
std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len)
};

let mut builder = platform_buffer::BufferBuilder::new()
Expand Down Expand Up @@ -346,7 +411,7 @@ impl BufferBuilder {
data_len: usize,
) -> Result<usize, &'static str> {
let buffer_length = if self.buffer_length == 0 {
element_size * data_len
checked_byte_len(element_size, data_len)?
} else {
self.buffer_length
};
Expand Down Expand Up @@ -375,4 +440,40 @@ mod tests {
// Test module is a child of this module and can access private fields.
assert_eq!(builder.label.as_deref(), Some("buffer-test"));
}

#[test]
fn resolve_length_rejects_overflow() {
let builder = BufferBuilder::new();
let result = builder.resolve_length(usize::MAX, 2);
assert!(result.is_err());
}

#[test]
fn value_as_bytes_matches_native_bytes() {
let value: u32 = 0x1122_3344;
let expected = value.to_ne_bytes();
assert_eq!(value_as_bytes(&value), expected.as_slice());
}

#[test]
fn slice_as_bytes_matches_native_bytes() {
let values: [u16; 3] = [0x1122, 0x3344, 0x5566];
let mut expected: Vec<u8> = Vec::new();
for value in values {
expected.extend_from_slice(&value.to_ne_bytes());
}
assert_eq!(slice_as_bytes(&values).unwrap(), expected.as_slice());
}

#[test]
fn slice_as_bytes_empty_is_empty() {
let values: [u32; 0] = [];
assert_eq!(slice_as_bytes(&values).unwrap(), &[]);
}

#[test]
fn checked_byte_len_rejects_overflow() {
let result = checked_byte_len(usize::MAX, 2);
assert!(result.is_err());
}
}
2 changes: 2 additions & 0 deletions crates/lambda-rs/src/render/vertex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ pub struct Vertex {
pub color: [f32; 3],
}

unsafe impl crate::pod::PlainOldData for Vertex {}

/// Builder for constructing a `Vertex` instance incrementally.
#[derive(Clone, Copy, Debug)]
pub struct VertexBuilder {
Expand Down
14 changes: 10 additions & 4 deletions docs/tutorials/indexed-draws-and-multiple-vertex-buffers.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ 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: "2026-01-16T00:00:00Z"
version: "0.3.1"
last_updated: "2026-01-24T00:00:00Z"
version: "0.3.3"
engine_workspace_version: "2023.1.30"
wgpu_version: "26.0.1"
shader_backend_default: "naga"
winit_version: "0.29.10"
repo_commit: "9435ad1491b5930054117406abe08dd1c37f2102"
repo_commit: "df476b77e1f2a17818869c3218cf223ab935c456"
owners: ["lambda-sh"]
reviewers: ["engine", "rendering"]
tags: ["tutorial", "graphics", "indexed-draws", "vertex-buffers", "rust", "wgpu"]
Expand Down Expand Up @@ -123,14 +123,18 @@ struct PositionVertex {
position: [f32; 3],
}

unsafe impl lambda::pod::PlainOldData for PositionVertex {}

#[repr(C)]
#[derive(Clone, Copy, Debug)]
struct ColorVertex {
color: [f32; 3],
}

unsafe impl lambda::pod::PlainOldData for ColorVertex {}
```

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.
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. The `PlainOldData` implementations mark the types as safe for `BufferBuilder` uploads.

### Step 2 — Component State and Shader Construction <a name="step-2"></a>

Expand Down Expand Up @@ -498,6 +502,8 @@ This tutorial demonstrates how indexed draws and multiple vertex buffers combine

## Changelog <a name="changelog"></a>

- 2026-01-24 (v0.3.3) — Move `PlainOldData` to `lambda::pod::PlainOldData`.
- 2026-01-24 (v0.3.2) — Add `PlainOldData` requirements for typed buffer data.
- 2026-01-16 (v0.3.1) — Update resize handling examples to use `event_mask()` and `on_window_event`.
- 2025-12-15 (v0.3.0) — Update builder API calls to use `render_context.gpu()` and add `surface_format`/`depth_format` parameters to `RenderPassBuilder` and `RenderPipelineBuilder`.
- 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.
Expand Down
14 changes: 10 additions & 4 deletions docs/tutorials/instanced-quads.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ title: "Instanced Rendering: Grid of Colored Quads"
document_id: "instanced-quads-tutorial-2025-11-25"
status: "draft"
created: "2025-11-25T00:00:00Z"
last_updated: "2026-01-16T00:00:00Z"
version: "0.2.1"
last_updated: "2026-01-24T00:00:00Z"
version: "0.2.3"
engine_workspace_version: "2023.1.30"
wgpu_version: "26.0.1"
shader_backend_default: "naga"
winit_version: "0.29.10"
repo_commit: "9435ad1491b5930054117406abe08dd1c37f2102"
repo_commit: "df476b77e1f2a17818869c3218cf223ab935c456"
owners: ["lambda-sh"]
reviewers: ["engine", "rendering"]
tags: ["tutorial", "graphics", "instancing", "vertex-buffers", "rust", "wgpu"]
Expand Down Expand Up @@ -156,13 +156,17 @@ struct QuadVertex {
position: [f32; 3],
}

unsafe impl lambda::pod::PlainOldData for QuadVertex {}

#[repr(C)]
#[derive(Clone, Copy, Debug)]
struct InstanceData {
offset: [f32; 3],
color: [f32; 3],
}

unsafe impl lambda::pod::PlainOldData for InstanceData {}

pub struct InstancedQuadsExample {
vertex_shader: Shader,
fragment_shader: Shader,
Expand Down Expand Up @@ -210,7 +214,7 @@ impl Default for InstancedQuadsExample {
}
```

The `QuadVertex` and `InstanceData` structures mirror the GLSL inputs as arrays of `f32`, and the component tracks resource identifiers and counts that are populated during attachment. The `Default` implementation constructs shader objects from the GLSL source so that the component is ready to build a pipeline when it receives a `RenderContext`.
The `QuadVertex` and `InstanceData` structures mirror the GLSL inputs as arrays of `f32`, and the component tracks resource identifiers and counts that are populated during attachment. The `PlainOldData` implementations mark the types as safe for `BufferBuilder` uploads, which reinterpret values as raw bytes when initializing GPU buffers. The `Default` implementation constructs shader objects from the GLSL source so that the component is ready to build a pipeline when it receives a `RenderContext`.

### Step 3 — Render Pass, Geometry, Instances, and Buffers <a name="step-3"></a>

Expand Down Expand Up @@ -510,6 +514,8 @@ This tutorial demonstrates how the `lambda-rs` crate uses per-vertex and per-ins

## Changelog <a name="changelog"></a>

- 2026-01-24 (v0.2.3) — Move `PlainOldData` to `lambda::pod::PlainOldData`.
- 2026-01-24 (v0.2.2) — Add `PlainOldData` requirements for typed buffer data.
- 2026-01-16 (v0.2.1) — Update resize handling examples to use `event_mask()` and `on_window_event`.
- 2025-12-15 (v0.2.0) — Update builder API calls to use `render_context.gpu()` and add `surface_format`/`depth_format` parameters to `RenderPassBuilder` and `RenderPipelineBuilder`.
- 2025-11-25 (v0.1.1) — Align feature naming with `render-validation-instancing` and update metadata.
Expand Down
Loading
Loading