@@ -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+
233317impl < ' 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 ) ]
295475pub struct RenderPipeline {
0 commit comments