Skip to content

Commit 00aabab

Browse files
committed
[add] ubo validation, add tests, and update render context impl.
1 parent 3e63f82 commit 00aabab

File tree

6 files changed

+210
-47
lines changed

6 files changed

+210
-47
lines changed

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,24 +70,38 @@ mod tests {
7070
/// Bind group layout used when creating pipelines and bind groups.
7171
pub struct BindGroupLayout {
7272
layout: Rc<lambda_platform::wgpu::bind::BindGroupLayout>,
73+
/// Total number of dynamic bindings declared in this layout.
74+
dynamic_binding_count: u32,
7375
}
7476

7577
impl BindGroupLayout {
7678
pub(crate) fn raw(&self) -> &wgpu::BindGroupLayout {
7779
self.layout.raw()
7880
}
81+
82+
/// Number of dynamic bindings declared in this layout.
83+
pub fn dynamic_binding_count(&self) -> u32 {
84+
self.dynamic_binding_count
85+
}
7986
}
8087

8188
#[derive(Debug, Clone)]
8289
/// Bind group that binds one or more resources to a pipeline set index.
8390
pub struct BindGroup {
8491
group: Rc<lambda_platform::wgpu::bind::BindGroup>,
92+
/// Cached number of dynamic bindings expected when binding this group.
93+
dynamic_binding_count: u32,
8594
}
8695

8796
impl BindGroup {
8897
pub(crate) fn raw(&self) -> &wgpu::BindGroup {
8998
self.group.raw()
9099
}
100+
101+
/// Number of dynamic bindings expected when calling set_bind_group.
102+
pub fn dynamic_binding_count(&self) -> u32 {
103+
self.dynamic_binding_count
104+
}
91105
}
92106

93107
/// Builder for creating a bind group layout with uniform buffer bindings.
@@ -135,6 +149,8 @@ impl BindGroupLayoutBuilder {
135149
pub fn build(self, render_context: &RenderContext) -> BindGroupLayout {
136150
let mut platform =
137151
lambda_platform::wgpu::bind::BindGroupLayoutBuilder::new();
152+
let dynamic_binding_count =
153+
self.entries.iter().filter(|(_, _, d)| *d).count() as u32;
138154
if let Some(label) = &self.label {
139155
platform = platform.with_label(label);
140156
}
@@ -148,6 +164,7 @@ impl BindGroupLayoutBuilder {
148164
let layout = platform.build(render_context.device());
149165
BindGroupLayout {
150166
layout: Rc::new(layout),
167+
dynamic_binding_count,
151168
}
152169
}
153170
}
@@ -203,12 +220,23 @@ impl<'a> BindGroupBuilder<'a> {
203220
if let Some(label) = &self.label {
204221
platform = platform.with_label(label);
205222
}
223+
let max_binding = render_context.limit_max_uniform_buffer_binding_size();
206224
for (binding, buffer, offset, size) in self.entries.into_iter() {
225+
if let Some(sz) = size {
226+
assert!(
227+
sz.get() <= max_binding,
228+
"Uniform binding at binding={} requests size={} > device limit {}",
229+
binding,
230+
sz.get(),
231+
max_binding
232+
);
233+
}
207234
platform = platform.with_uniform(binding, buffer.raw(), offset, size);
208235
}
209236
let group = platform.build(render_context.device());
210237
BindGroup {
211238
group: Rc::new(group),
239+
dynamic_binding_count: layout.dynamic_binding_count(),
212240
}
213241
}
214242
}

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,48 @@ impl Buffer {
138138
}
139139
}
140140

141+
/// Strongly‑typed uniform buffer wrapper for ergonomics and safety.
142+
///
143+
/// Stores a single value of type `T` and provides a convenience method to
144+
/// upload updates to the GPU. The underlying buffer has `UNIFORM` usage and
145+
/// is CPU‑visible by default for easy updates via `Queue::write_buffer`.
146+
pub struct UniformBuffer<T> {
147+
inner: Buffer,
148+
_phantom: core::marker::PhantomData<T>,
149+
}
150+
151+
impl<T: Copy> UniformBuffer<T> {
152+
/// Create a new uniform buffer initialized with `initial`.
153+
pub fn new(
154+
render_context: &mut RenderContext,
155+
initial: &T,
156+
label: Option<&str>,
157+
) -> Result<Self, &'static str> {
158+
let mut builder = BufferBuilder::new();
159+
builder.with_length(core::mem::size_of::<T>());
160+
builder.with_usage(Usage::UNIFORM);
161+
builder.with_properties(Properties::CPU_VISIBLE);
162+
if let Some(l) = label {
163+
builder.with_label(l);
164+
}
165+
let inner = builder.build(render_context, vec![*initial])?;
166+
Ok(Self {
167+
inner,
168+
_phantom: core::marker::PhantomData,
169+
})
170+
}
171+
172+
/// Borrow the underlying generic `Buffer` for binding.
173+
pub fn raw(&self) -> &Buffer {
174+
&self.inner
175+
}
176+
177+
/// Write a new value to the GPU buffer at offset 0.
178+
pub fn write(&self, render_context: &RenderContext, value: &T) {
179+
self.inner.write_value(render_context, 0, value);
180+
}
181+
}
182+
141183
/// Builder for creating `Buffer` objects with explicit usage and properties.
142184
///
143185
/// A buffer is a block of memory the GPU can access. You supply a total byte

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub mod pipeline;
1010
pub mod render_pass;
1111
pub mod scene_math;
1212
pub mod shader;
13+
pub mod validation;
1314
pub mod vertex;
1415
pub mod viewport;
1516
pub mod window;
@@ -213,6 +214,21 @@ impl RenderContext {
213214
self.config.format
214215
}
215216

217+
/// Device limit: maximum bytes that can be bound for a single uniform buffer binding.
218+
pub fn limit_max_uniform_buffer_binding_size(&self) -> u64 {
219+
self.gpu.limits().max_uniform_buffer_binding_size.into()
220+
}
221+
222+
/// Device limit: number of bind groups that can be used by a pipeline layout.
223+
pub fn limit_max_bind_groups(&self) -> u32 {
224+
self.gpu.limits().max_bind_groups
225+
}
226+
227+
/// Device limit: required alignment in bytes for dynamic uniform buffer offsets.
228+
pub fn limit_min_uniform_buffer_offset_alignment(&self) -> u32 {
229+
self.gpu.limits().min_uniform_buffer_offset_alignment
230+
}
231+
216232
/// Encode and submit GPU work for a single frame.
217233
fn render_internal(
218234
&mut self,
@@ -328,6 +344,14 @@ impl RenderContext {
328344
let group_ref = self.bind_groups.get(group).ok_or_else(|| {
329345
RenderError::Configuration(format!("Unknown bind group {group}"))
330346
})?;
347+
// Validate dynamic offsets count and alignment before binding.
348+
validation::validate_dynamic_offsets(
349+
group_ref.dynamic_binding_count(),
350+
&dynamic_offsets,
351+
self.limit_min_uniform_buffer_offset_alignment(),
352+
set,
353+
)
354+
.map_err(RenderError::Configuration)?;
331355
pass.set_bind_group(set, group_ref.raw(), &dynamic_offsets);
332356
}
333357
RenderCommand::BindVertexBuffer { pipeline, buffer } => {

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,14 @@ impl RenderPipelineBuilder {
200200
})
201201
.collect();
202202

203+
let max_bind_groups = render_context.limit_max_bind_groups() as usize;
204+
assert!(
205+
self.bind_group_layouts.len() <= max_bind_groups,
206+
"Pipeline declares {} bind group layouts, exceeds device max {}",
207+
self.bind_group_layouts.len(),
208+
max_bind_groups
209+
);
210+
203211
let bind_group_layout_refs: Vec<&wgpu::BindGroupLayout> = self
204212
.bind_group_layouts
205213
.iter()
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//! Small helpers for limits and alignment validation used by the renderer.
2+
3+
/// Align `value` up to the nearest multiple of `align`.
4+
/// If `align` is zero, returns `value` unchanged.
5+
pub fn align_up(value: u64, align: u64) -> u64 {
6+
if align == 0 {
7+
return value;
8+
}
9+
let mask = align - 1;
10+
(value + mask) & !mask
11+
}
12+
13+
/// Validate a set of dynamic offsets against the required count and alignment.
14+
/// Returns `Ok(())` when valid; otherwise a human‑readable error message.
15+
pub fn validate_dynamic_offsets(
16+
required_count: u32,
17+
offsets: &[u32],
18+
alignment: u32,
19+
set_index: u32,
20+
) -> Result<(), String> {
21+
if offsets.len() as u32 != required_count {
22+
return Err(format!(
23+
"Bind group at set {} expects {} dynamic offsets, got {}",
24+
set_index,
25+
required_count,
26+
offsets.len()
27+
));
28+
}
29+
let align = alignment.max(1);
30+
for (i, off) in offsets.iter().enumerate() {
31+
if (*off as u32) % align != 0 {
32+
return Err(format!(
33+
"Dynamic offset[{}]={} is not {}-byte aligned",
34+
i, off, align
35+
));
36+
}
37+
}
38+
Ok(())
39+
}
40+
41+
#[cfg(test)]
42+
mod tests {
43+
use super::*;
44+
45+
#[test]
46+
fn align_up_noop_on_zero_align() {
47+
assert_eq!(align_up(13, 0), 13);
48+
}
49+
50+
#[test]
51+
fn align_up_rounds_to_multiple() {
52+
assert_eq!(align_up(0, 256), 0);
53+
assert_eq!(align_up(1, 256), 256);
54+
assert_eq!(align_up(255, 256), 256);
55+
assert_eq!(align_up(256, 256), 256);
56+
assert_eq!(align_up(257, 256), 512);
57+
}
58+
59+
#[test]
60+
fn validate_dynamic_offsets_count_and_alignment() {
61+
// Correct count and alignment
62+
assert!(validate_dynamic_offsets(2, &[0, 256], 256, 0).is_ok());
63+
64+
// Wrong count
65+
let err = validate_dynamic_offsets(3, &[0, 256], 256, 1)
66+
.err()
67+
.unwrap();
68+
assert!(err.contains("expects 3 dynamic offsets"));
69+
70+
// Misaligned
71+
let err = validate_dynamic_offsets(2, &[0, 128], 256, 0)
72+
.err()
73+
.unwrap();
74+
assert!(err.contains("not 256-byte aligned"));
75+
}
76+
}

0 commit comments

Comments
 (0)