Skip to content

Commit 9e4bd83

Browse files
committed
[add] validation for imediate ranges.
1 parent d0ef986 commit 9e4bd83

File tree

1 file changed

+188
-8
lines changed

1 file changed

+188
-8
lines changed

crates/lambda-rs-platform/src/wgpu/pipeline.rs

Lines changed: 188 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,90 @@ pub struct PipelineLayoutBuilder<'a> {
230230
immediate_data_ranges: Vec<ImmediateDataRange>,
231231
}
232232

233+
/// Align a `u32` value up to the provided power-of-two alignment.
234+
fn align_up_u32(value: u32, alignment: u32) -> u32 {
235+
if alignment == 0 {
236+
return value;
237+
}
238+
let remainder = value % alignment;
239+
if remainder == 0 {
240+
return value;
241+
}
242+
return value + (alignment - remainder);
243+
}
244+
245+
/// Validate immediate ranges and calculate the minimum allocation size.
246+
///
247+
/// wgpu v28 uses a single byte region of size `immediate_size`, addressed by
248+
/// `set_immediates(offset, data)`. This function enforces that the provided
249+
/// ranges:
250+
/// - Start at byte offset 0 (as a union)
251+
/// - Cover a contiguous span with no gaps (as a union)
252+
/// - Are aligned to `wgpu::IMMEDIATE_DATA_ALIGNMENT`
253+
fn validate_and_calculate_immediate_size(
254+
immediate_data_ranges: &[ImmediateDataRange],
255+
) -> Result<u32, String> {
256+
if immediate_data_ranges.is_empty() {
257+
return Ok(0);
258+
}
259+
260+
let alignment = wgpu::IMMEDIATE_DATA_ALIGNMENT;
261+
262+
let mut sorted_ranges: Vec<Range<u32>> = immediate_data_ranges
263+
.iter()
264+
.map(|r| r.range.clone())
265+
.collect();
266+
sorted_ranges.sort_by_key(|range| (range.start, range.end));
267+
268+
for range in &sorted_ranges {
269+
if range.start > range.end {
270+
return Err(format!(
271+
"Immediate data range start {} exceeds end {}.",
272+
range.start, range.end
273+
));
274+
}
275+
if range.start == range.end {
276+
return Err(format!(
277+
"Immediate data range {}..{} is empty.",
278+
range.start, range.end
279+
));
280+
}
281+
if range.start % alignment != 0 {
282+
return Err(format!(
283+
"Immediate data range start {} is not aligned to {} bytes.",
284+
range.start, alignment
285+
));
286+
}
287+
if range.end % alignment != 0 {
288+
return Err(format!(
289+
"Immediate data range end {} is not aligned to {} bytes.",
290+
range.end, alignment
291+
));
292+
}
293+
}
294+
295+
let mut current_end = 0;
296+
for range in &sorted_ranges {
297+
if range.start > current_end {
298+
return Err(format!(
299+
"Immediate data ranges must be contiguous starting at 0; found gap \
300+
{}..{}.",
301+
current_end, range.start
302+
));
303+
}
304+
current_end = current_end.max(range.end);
305+
}
306+
307+
if current_end % alignment != 0 {
308+
return Err(format!(
309+
"Immediate data size {} is not aligned to {} bytes.",
310+
current_end, alignment
311+
));
312+
}
313+
314+
return Ok(current_end);
315+
}
316+
233317
impl<'a> PipelineLayoutBuilder<'a> {
234318
/// New builder with no layouts or immediate data.
235319
pub fn new() -> Self {
@@ -266,14 +350,31 @@ impl<'a> PipelineLayoutBuilder<'a> {
266350
let layouts_raw: Vec<&wgpu::BindGroupLayout> =
267351
self.layouts.iter().map(|l| l.raw()).collect();
268352

269-
// Calculate the total immediate size from immediate data ranges.
270-
// The immediate_size is the maximum end offset across all ranges.
271-
let immediate_size = self
272-
.immediate_data_ranges
273-
.iter()
274-
.map(|r| r.range.end)
275-
.max()
276-
.unwrap_or(0);
353+
// wgpu v28 allocates a single immediate byte region sized by
354+
// `PipelineLayoutDescriptor::immediate_size`. If callers provide multiple
355+
// ranges, they are treated as sub-ranges of the same contiguous allocation.
356+
//
357+
// Validate that the union of ranges starts at 0 and has no gaps so that the
358+
// required allocation size is well-defined.
359+
let (immediate_size, fallback_used) =
360+
match validate_and_calculate_immediate_size(&self.immediate_data_ranges) {
361+
Ok(size) => (size, false),
362+
Err(message) => {
363+
logging::error!(
364+
"Invalid immediate data ranges for pipeline layout: {}",
365+
message
366+
);
367+
debug_assert!(false, "{}", message);
368+
369+
let max_end = self
370+
.immediate_data_ranges
371+
.iter()
372+
.map(|r| r.range.end)
373+
.max()
374+
.unwrap_or(0);
375+
(align_up_u32(max_end, wgpu::IMMEDIATE_DATA_ALIGNMENT), true)
376+
}
377+
};
277378

278379
let raw =
279380
gpu
@@ -283,13 +384,92 @@ impl<'a> PipelineLayoutBuilder<'a> {
283384
bind_group_layouts: &layouts_raw,
284385
immediate_size,
285386
});
387+
if fallback_used {
388+
logging::warn!(
389+
"Pipeline layout immediate size computed using fallback; consider \
390+
declaring immediate ranges as a single contiguous span starting at 0."
391+
);
392+
}
286393
return PipelineLayout {
287394
raw,
288395
label: self.label,
289396
};
290397
}
291398
}
292399

400+
#[cfg(test)]
401+
mod immediate_size_tests {
402+
use super::{
403+
validate_and_calculate_immediate_size,
404+
ImmediateDataRange,
405+
PipelineStage,
406+
};
407+
408+
#[test]
409+
fn immediate_size_empty_ok() {
410+
let size = validate_and_calculate_immediate_size(&[]).unwrap();
411+
assert_eq!(size, 0);
412+
}
413+
414+
#[test]
415+
fn immediate_size_overlapping_ranges_ok() {
416+
let ranges = vec![
417+
ImmediateDataRange {
418+
stages: PipelineStage::VERTEX,
419+
range: 0..64,
420+
},
421+
ImmediateDataRange {
422+
stages: PipelineStage::FRAGMENT,
423+
range: 0..32,
424+
},
425+
];
426+
let size = validate_and_calculate_immediate_size(&ranges).unwrap();
427+
assert_eq!(size, 64);
428+
}
429+
430+
#[test]
431+
fn immediate_size_contiguous_ranges_ok() {
432+
let ranges = vec![
433+
ImmediateDataRange {
434+
stages: PipelineStage::VERTEX,
435+
range: 0..16,
436+
},
437+
ImmediateDataRange {
438+
stages: PipelineStage::FRAGMENT,
439+
range: 16..32,
440+
},
441+
];
442+
let size = validate_and_calculate_immediate_size(&ranges).unwrap();
443+
assert_eq!(size, 32);
444+
}
445+
446+
#[test]
447+
fn immediate_size_gap_is_error() {
448+
let ranges = vec![
449+
ImmediateDataRange {
450+
stages: PipelineStage::VERTEX,
451+
range: 0..16,
452+
},
453+
ImmediateDataRange {
454+
stages: PipelineStage::FRAGMENT,
455+
range: 32..48,
456+
},
457+
];
458+
let err = validate_and_calculate_immediate_size(&ranges).unwrap_err();
459+
assert!(err.contains("gap"));
460+
}
461+
462+
#[test]
463+
fn immediate_size_non_zero_start_is_error() {
464+
let ranges = vec![ImmediateDataRange {
465+
stages: PipelineStage::VERTEX,
466+
range: 16..32,
467+
}];
468+
let err = validate_and_calculate_immediate_size(&ranges).unwrap_err();
469+
assert!(err.contains("gap"));
470+
}
471+
}
472+
293473
/// Wrapper around `wgpu::RenderPipeline`.
294474
#[derive(Debug)]
295475
pub struct RenderPipeline {

0 commit comments

Comments
 (0)