Skip to content

Commit 229960f

Browse files
authored
feat(lambda-rs): Add write_slice and write_bytes to Buffer
2 parents 5b5e501 + c8cd426 commit 229960f

File tree

10 files changed

+202
-30
lines changed

10 files changed

+202
-30
lines changed

crates/lambda-rs/examples/indexed_multi_vertex_buffers.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,16 @@ struct PositionVertex {
8989
position: [f32; 3],
9090
}
9191

92+
unsafe impl lambda::pod::PlainOldData for PositionVertex {}
93+
9294
#[repr(C)]
9395
#[derive(Clone, Copy, Debug)]
9496
struct ColorVertex {
9597
color: [f32; 3],
9698
}
9799

100+
unsafe impl lambda::pod::PlainOldData for ColorVertex {}
101+
98102
// --------------------------------- COMPONENT ---------------------------------
99103

100104
pub struct IndexedMultiBufferExample {

crates/lambda-rs/examples/instanced_quads.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,17 @@ struct QuadVertex {
9292
position: [f32; 3],
9393
}
9494

95+
unsafe impl lambda::pod::PlainOldData for QuadVertex {}
96+
9597
#[repr(C)]
9698
#[derive(Clone, Copy, Debug)]
9799
struct InstanceData {
98100
offset: [f32; 3],
99101
color: [f32; 3],
100102
}
101103

104+
unsafe impl lambda::pod::PlainOldData for InstanceData {}
105+
102106
// --------------------------------- COMPONENT ---------------------------------
103107

104108
/// Component that renders a grid of instanced quads.

crates/lambda-rs/examples/uniform_buffer_triangle.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ pub struct GlobalsUniform {
101101
pub render_matrix: [[f32; 4]; 4],
102102
}
103103

104+
unsafe impl lambda::pod::PlainOldData for GlobalsUniform {}
105+
104106
// --------------------------------- COMPONENT ---------------------------------
105107

106108
pub struct UniformBufferExample {

crates/lambda-rs/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
pub mod component;
1515
pub mod events;
1616
pub mod math;
17+
pub mod pod;
1718
pub mod render;
1819
pub mod runtime;
1920
pub mod runtimes;

crates/lambda-rs/src/pod.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//! Plain-old-data marker trait for safe byte uploads.
2+
//!
3+
//! The engine frequently uploads CPU data into GPU buffers by reinterpreting
4+
//! a value or slice as raw bytes. This is only sound for types that are safe
5+
//! to view as bytes.
6+
7+
/// Marker trait for types that are safe to reinterpret as raw bytes.
8+
///
9+
/// This trait is required by typed buffer upload APIs (for example
10+
/// `render::buffer::Buffer::write_value` and `render::buffer::Buffer::write_slice`)
11+
/// and typed buffer creation APIs (for example
12+
/// `render::buffer::BufferBuilder::build`) because those operations upload the
13+
/// in-memory representation of a value to the GPU.
14+
///
15+
/// # Safety
16+
/// Types implementing `PlainOldData` MUST satisfy all of the following:
17+
/// - Every byte of the value is initialized (including any padding bytes).
18+
/// - The type has no pointers or references that would be invalidated by a
19+
/// raw byte copy.
20+
/// - The type's byte representation is stable for GPU consumption. Prefer
21+
/// `#[repr(C)]` or `#[repr(transparent)]`.
22+
///
23+
/// Implementing this trait incorrectly can cause undefined behavior.
24+
pub unsafe trait PlainOldData: Copy {}
25+
26+
unsafe impl PlainOldData for u8 {}
27+
unsafe impl PlainOldData for i8 {}
28+
unsafe impl PlainOldData for u16 {}
29+
unsafe impl PlainOldData for i16 {}
30+
unsafe impl PlainOldData for u32 {}
31+
unsafe impl PlainOldData for i32 {}
32+
unsafe impl PlainOldData for u64 {}
33+
unsafe impl PlainOldData for i64 {}
34+
unsafe impl PlainOldData for u128 {}
35+
unsafe impl PlainOldData for i128 {}
36+
unsafe impl PlainOldData for usize {}
37+
unsafe impl PlainOldData for isize {}
38+
unsafe impl PlainOldData for f32 {}
39+
unsafe impl PlainOldData for f64 {}
40+
unsafe impl PlainOldData for bool {}
41+
unsafe impl PlainOldData for char {}
42+
unsafe impl<T: PlainOldData, const N: usize> PlainOldData for [T; N] {}

crates/lambda-rs/src/render/buffer.rs

Lines changed: 119 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use super::{
2727
mesh::Mesh,
2828
RenderContext,
2929
};
30+
pub use crate::pod::PlainOldData;
3031

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

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

152-
self.buffer.write_bytes(gpu.platform(), offset, bytes);
150+
/// Write raw bytes into this buffer at the specified byte offset.
151+
///
152+
/// This is useful when data is already available as a byte slice (for
153+
/// example, asset blobs or staging buffers).
154+
///
155+
/// Example
156+
/// ```rust,ignore
157+
/// let raw_data: &[u8] = load_binary_data();
158+
/// buffer.write_bytes(render_context.gpu(), 0, raw_data);
159+
/// ```
160+
pub fn write_bytes(&self, gpu: &Gpu, offset: u64, data: &[u8]) {
161+
self.buffer.write_bytes(gpu.platform(), offset, data);
153162
}
163+
164+
/// Write a slice of plain-old-data values into this buffer at the
165+
/// specified byte offset.
166+
///
167+
/// This is intended for uploading arrays of vertices, indices, instance
168+
/// data, or uniform blocks. The `T` type MUST be plain-old-data (POD) and
169+
/// safely representable as bytes. This is enforced by requiring `T` to
170+
/// implement `PlainOldData`.
171+
///
172+
/// Example
173+
/// ```rust,ignore
174+
/// let transforms: Vec<InstanceTransform> = compute_transforms();
175+
/// instance_buffer
176+
/// .write_slice(render_context.gpu(), 0, &transforms)
177+
/// .unwrap();
178+
/// ```
179+
pub fn write_slice<T: PlainOldData>(
180+
&self,
181+
gpu: &Gpu,
182+
offset: u64,
183+
data: &[T],
184+
) -> Result<(), &'static str> {
185+
let bytes = slice_as_bytes(data)?;
186+
self.write_bytes(gpu, offset, bytes);
187+
return Ok(());
188+
}
189+
}
190+
191+
fn value_as_bytes<T: PlainOldData>(data: &T) -> &[u8] {
192+
let bytes = unsafe {
193+
std::slice::from_raw_parts(
194+
(data as *const T) as *const u8,
195+
std::mem::size_of::<T>(),
196+
)
197+
};
198+
return bytes;
199+
}
200+
201+
fn checked_byte_len(
202+
element_size: usize,
203+
element_count: usize,
204+
) -> Result<usize, &'static str> {
205+
let Some(byte_len) = element_size.checked_mul(element_count) else {
206+
return Err("Buffer byte length overflow.");
207+
};
208+
return Ok(byte_len);
209+
}
210+
211+
fn slice_as_bytes<T: PlainOldData>(data: &[T]) -> Result<&[u8], &'static str> {
212+
let element_size = std::mem::size_of::<T>();
213+
let byte_len = checked_byte_len(element_size, data.len())?;
214+
215+
let bytes =
216+
unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len) };
217+
return Ok(bytes);
154218
}
155219

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

179-
impl<T: Copy> UniformBuffer<T> {
243+
impl<T: PlainOldData> UniformBuffer<T> {
180244
/// Create a new uniform buffer initialized with `initial`.
181245
pub fn new(
182246
gpu: &Gpu,
@@ -287,22 +351,23 @@ impl BufferBuilder {
287351
/// Create a buffer initialized with the provided `data`.
288352
///
289353
/// Returns an error if the resolved length would be zero.
290-
pub fn build<Data: Copy>(
354+
///
355+
/// The element type MUST implement `PlainOldData` because the engine uploads
356+
/// the in-memory representation to the GPU.
357+
pub fn build<Data: PlainOldData>(
291358
&self,
292359
gpu: &Gpu,
293360
data: Vec<Data>,
294361
) -> Result<Buffer, &'static str> {
295362
let element_size = std::mem::size_of::<Data>();
296363
let buffer_length = self.resolve_length(element_size, data.len())?;
364+
let byte_len = checked_byte_len(element_size, data.len())?;
297365

298366
// SAFETY: Converting data to bytes is safe because its underlying
299-
// type, Data, is constrained to Copy and the lifetime of the slice does
300-
// not outlive data.
367+
// type, Data, is constrained to PlainOldData and the lifetime of the slice
368+
// does not outlive data.
301369
let bytes = unsafe {
302-
std::slice::from_raw_parts(
303-
data.as_ptr() as *const u8,
304-
element_size * data.len(),
305-
)
370+
std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len)
306371
};
307372

308373
let mut builder = platform_buffer::BufferBuilder::new()
@@ -346,7 +411,7 @@ impl BufferBuilder {
346411
data_len: usize,
347412
) -> Result<usize, &'static str> {
348413
let buffer_length = if self.buffer_length == 0 {
349-
element_size * data_len
414+
checked_byte_len(element_size, data_len)?
350415
} else {
351416
self.buffer_length
352417
};
@@ -375,4 +440,40 @@ mod tests {
375440
// Test module is a child of this module and can access private fields.
376441
assert_eq!(builder.label.as_deref(), Some("buffer-test"));
377442
}
443+
444+
#[test]
445+
fn resolve_length_rejects_overflow() {
446+
let builder = BufferBuilder::new();
447+
let result = builder.resolve_length(usize::MAX, 2);
448+
assert!(result.is_err());
449+
}
450+
451+
#[test]
452+
fn value_as_bytes_matches_native_bytes() {
453+
let value: u32 = 0x1122_3344;
454+
let expected = value.to_ne_bytes();
455+
assert_eq!(value_as_bytes(&value), expected.as_slice());
456+
}
457+
458+
#[test]
459+
fn slice_as_bytes_matches_native_bytes() {
460+
let values: [u16; 3] = [0x1122, 0x3344, 0x5566];
461+
let mut expected: Vec<u8> = Vec::new();
462+
for value in values {
463+
expected.extend_from_slice(&value.to_ne_bytes());
464+
}
465+
assert_eq!(slice_as_bytes(&values).unwrap(), expected.as_slice());
466+
}
467+
468+
#[test]
469+
fn slice_as_bytes_empty_is_empty() {
470+
let values: [u32; 0] = [];
471+
assert_eq!(slice_as_bytes(&values).unwrap(), &[]);
472+
}
473+
474+
#[test]
475+
fn checked_byte_len_rejects_overflow() {
476+
let result = checked_byte_len(usize::MAX, 2);
477+
assert!(result.is_err());
478+
}
378479
}

crates/lambda-rs/src/render/vertex.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ pub struct Vertex {
7878
pub color: [f32; 3],
7979
}
8080

81+
unsafe impl crate::pod::PlainOldData for Vertex {}
82+
8183
/// Builder for constructing a `Vertex` instance incrementally.
8284
#[derive(Clone, Copy, Debug)]
8385
pub struct VertexBuilder {

docs/tutorials/indexed-draws-and-multiple-vertex-buffers.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ title: "Indexed Draws and Multiple Vertex Buffers"
33
document_id: "indexed-draws-multiple-vertex-buffers-tutorial-2025-11-22"
44
status: "draft"
55
created: "2025-11-22T00:00:00Z"
6-
last_updated: "2026-01-16T00:00:00Z"
7-
version: "0.3.1"
6+
last_updated: "2026-01-24T00:00:00Z"
7+
version: "0.3.3"
88
engine_workspace_version: "2023.1.30"
99
wgpu_version: "26.0.1"
1010
shader_backend_default: "naga"
1111
winit_version: "0.29.10"
12-
repo_commit: "9435ad1491b5930054117406abe08dd1c37f2102"
12+
repo_commit: "df476b77e1f2a17818869c3218cf223ab935c456"
1313
owners: ["lambda-sh"]
1414
reviewers: ["engine", "rendering"]
1515
tags: ["tutorial", "graphics", "indexed-draws", "vertex-buffers", "rust", "wgpu"]
@@ -123,14 +123,18 @@ struct PositionVertex {
123123
position: [f32; 3],
124124
}
125125

126+
unsafe impl lambda::pod::PlainOldData for PositionVertex {}
127+
126128
#[repr(C)]
127129
#[derive(Clone, Copy, Debug)]
128130
struct ColorVertex {
129131
color: [f32; 3],
130132
}
133+
134+
unsafe impl lambda::pod::PlainOldData for ColorVertex {}
131135
```
132136

133-
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.
137+
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.
134138

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

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

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

505+
- 2026-01-24 (v0.3.3) — Move `PlainOldData` to `lambda::pod::PlainOldData`.
506+
- 2026-01-24 (v0.3.2) — Add `PlainOldData` requirements for typed buffer data.
501507
- 2026-01-16 (v0.3.1) — Update resize handling examples to use `event_mask()` and `on_window_event`.
502508
- 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`.
503509
- 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.

docs/tutorials/instanced-quads.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ title: "Instanced Rendering: Grid of Colored Quads"
33
document_id: "instanced-quads-tutorial-2025-11-25"
44
status: "draft"
55
created: "2025-11-25T00:00:00Z"
6-
last_updated: "2026-01-16T00:00:00Z"
7-
version: "0.2.1"
6+
last_updated: "2026-01-24T00:00:00Z"
7+
version: "0.2.3"
88
engine_workspace_version: "2023.1.30"
99
wgpu_version: "26.0.1"
1010
shader_backend_default: "naga"
1111
winit_version: "0.29.10"
12-
repo_commit: "9435ad1491b5930054117406abe08dd1c37f2102"
12+
repo_commit: "df476b77e1f2a17818869c3218cf223ab935c456"
1313
owners: ["lambda-sh"]
1414
reviewers: ["engine", "rendering"]
1515
tags: ["tutorial", "graphics", "instancing", "vertex-buffers", "rust", "wgpu"]
@@ -156,13 +156,17 @@ struct QuadVertex {
156156
position: [f32; 3],
157157
}
158158

159+
unsafe impl lambda::pod::PlainOldData for QuadVertex {}
160+
159161
#[repr(C)]
160162
#[derive(Clone, Copy, Debug)]
161163
struct InstanceData {
162164
offset: [f32; 3],
163165
color: [f32; 3],
164166
}
165167

168+
unsafe impl lambda::pod::PlainOldData for InstanceData {}
169+
166170
pub struct InstancedQuadsExample {
167171
vertex_shader: Shader,
168172
fragment_shader: Shader,
@@ -210,7 +214,7 @@ impl Default for InstancedQuadsExample {
210214
}
211215
```
212216

213-
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`.
217+
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`.
214218

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

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

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

517+
- 2026-01-24 (v0.2.3) — Move `PlainOldData` to `lambda::pod::PlainOldData`.
518+
- 2026-01-24 (v0.2.2) — Add `PlainOldData` requirements for typed buffer data.
513519
- 2026-01-16 (v0.2.1) — Update resize handling examples to use `event_mask()` and `on_window_event`.
514520
- 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`.
515521
- 2025-11-25 (v0.1.1) — Align feature naming with `render-validation-instancing` and update metadata.

0 commit comments

Comments
 (0)