pebble/wgpu/buffers.rs
1//! Buffer and bind-group construction — three builders, one per thing being
2//! built:
3//! - [`BufferBuilder`] — a plain, uniform, or storage [`Buffer`], empty or
4//! pre-populated with data.
5//! - [`DynamicBufferBuilder`] — a [`DynamicBuffer`] sized to hold many
6//! dynamically-offset elements; bundles the per-element stride so it
7//! can't drift out of sync with what the buffer was actually built with.
8//! - [`BindGroupBuilder`] — assembles a `wgpu::BindGroup` from already-built
9//! [`Buffer`]s/textures/samplers, one binding at a time.
10//!
11//! Prefer these over hand-writing `wgpu::BufferDescriptor`/`BindGroupDescriptor`
12//! against `backend.device` directly: correct usage flags are one method
13//! call away instead of memorized flag combinations, and the dynamic-offset
14//! path gets alignment right in a way that's easy to miss by hand. Re-exported,
15//! along with [`binding`](super::binding), from [`wgpu::prelude`](super::prelude).
16
17use crate::wgpu::backend::WGPUBackend;
18use crate::wgpu::binding::BindGroupLayout;
19use crate::wgpu::buffer::{Buffer, DynamicBuffer};
20use crate::wgpu::cubemap::GPUCubemap;
21use crate::wgpu::flags::BufferUsages;
22use crate::wgpu::gpu_context::GpuContext;
23use crate::wgpu::samplers::Sampler;
24use crate::wgpu::texture_array::GPUTextureArray;
25use crate::wgpu::texture_view::TextureView;
26use crate::wgpu::textures::GPUTexture;
27
28/// A `wgpu::BindGroup`, opaque — built only via [`BindGroupBuilder::build`].
29/// Bind it against a [`RenderPass`](super::render_pass::RenderPass)/
30/// [`ComputePass`](super::compute_pass::ComputePass) via their
31/// `set_bind_group`; there's no way to reach the underlying `wgpu::BindGroup`
32/// from outside this crate.
33pub struct BindGroup(wgpu::BindGroup);
34
35impl BindGroup {
36 pub(crate) fn raw(&self) -> &wgpu::BindGroup {
37 &self.0
38 }
39}
40
41// ---------------------------------------------------------------------
42// Plain buffers
43// ---------------------------------------------------------------------
44
45enum BufferContents<'a> {
46 Empty(u64),
47 Data(&'a [u8]),
48}
49
50impl<'a> BufferContents<'a> {
51 fn size(&self) -> u64 {
52 match self {
53 BufferContents::Empty(size) => *size,
54 BufferContents::Data(data) => data.len() as u64,
55 }
56 }
57}
58
59/// Builds a [`Buffer`] — empty (via [`empty`](Self::empty)) or pre-populated
60/// (via [`with_data`](Self::with_data)). Two constructors rather than one
61/// `new()` plus a `.data()`/`.size()` setter pair, so there's no way to call
62/// both and have the second one silently win — the same shape as
63/// [`Texture`](super::textures::Texture)'s `from_file`/`from_data`/`empty`.
64///
65/// ```ignore
66/// let camera_buffer = BufferBuilder::empty(64)
67/// .label("camera")
68/// .uniform()
69/// .build(&backend);
70///
71/// let vertex_buffer = BufferBuilder::with_data(bytemuck::cast_slice(&vertices))
72/// .label("mesh vertices")
73/// .usage(BufferUsages::VERTEX)
74/// .build(&backend);
75/// ```
76///
77/// For a dynamically-offset buffer (many elements, selected via
78/// `set_bind_group`'s dynamic offset), use [`DynamicBufferBuilder`] instead
79/// — it returns the per-element stride alongside the buffer, which plain
80/// `BufferBuilder` has no way to compute.
81pub struct BufferBuilder<'a> {
82 label: Option<&'a str>,
83 usage: BufferUsages,
84 contents: BufferContents<'a>,
85}
86
87impl<'a> BufferBuilder<'a> {
88 /// Allocates an empty buffer of `size` bytes, to be written into later
89 /// via [`Buffer::write`].
90 pub fn empty(size: u64) -> Self {
91 Self { label: None, usage: BufferUsages::empty(), contents: BufferContents::Empty(size) }
92 }
93
94 /// Pre-populates the buffer with `data` (its size is taken from `data`'s length).
95 pub fn with_data(data: &'a [u8]) -> Self {
96 Self { label: None, usage: BufferUsages::empty(), contents: BufferContents::Data(data) }
97 }
98
99 pub fn label(mut self, label: impl Into<Option<&'a str>>) -> Self {
100 self.label = label.into();
101 self
102 }
103
104 /// Sets the buffer's usage flags outright — use this for anything not
105 /// covered by [`uniform`](Self::uniform)/[`storage`](Self::storage)
106 /// (a vertex/index buffer, a `MAP_READ` staging buffer, ...).
107 pub fn usage(mut self, usage: BufferUsages) -> Self {
108 self.usage = usage;
109 self
110 }
111
112 /// Shorthand for `.usage(BufferUsages::UNIFORM | BufferUsages::COPY_DST)`.
113 pub fn uniform(self) -> Self {
114 self.usage(BufferUsages::UNIFORM | BufferUsages::COPY_DST)
115 }
116
117 /// Shorthand for `.usage(BufferUsages::STORAGE | BufferUsages::COPY_DST)`.
118 pub fn storage(self) -> Self {
119 self.usage(BufferUsages::STORAGE | BufferUsages::COPY_DST)
120 }
121
122 pub fn build(self, backend: &WGPUBackend) -> Buffer {
123 let raw = self.build_raw(&backend.device);
124 Buffer::new(raw, GpuContext::from_backend(backend))
125 }
126
127 /// Internal primitive behind [`build`](Self::build) — used directly only
128 /// where a [`WGPUBackend`] isn't available yet (bootstrapping a staging
129 /// buffer for [`Buffer::read`](crate::wgpu::buffer::Buffer::read), which
130 /// only has `device`/`queue` separately).
131 pub(crate) fn build_raw(self, device: &wgpu::Device) -> wgpu::Buffer {
132 check_buffer_size(device, self.label, self.usage, self.contents.size());
133 match self.contents {
134 BufferContents::Data(data) => {
135 use wgpu::util::DeviceExt;
136 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
137 label: self.label,
138 contents: data,
139 usage: self.usage.into(),
140 })
141 }
142 BufferContents::Empty(size) => device.create_buffer(&wgpu::BufferDescriptor {
143 label: self.label,
144 size,
145 usage: self.usage.into(),
146 mapped_at_creation: false,
147 }),
148 }
149 }
150}
151
152/// Panics if `size` exceeds this device's `max_buffer_size`, or — when `usage` includes
153/// `UNIFORM`/`STORAGE` — the tighter `max_uniform_buffer_binding_size`/
154/// `max_storage_buffer_binding_size` those binding types are further capped to. The
155/// difference between a clear message here (the actual size and the device's real limit) and
156/// an opaque wgpu validation panic deep inside `create_buffer`/`create_buffer_init`.
157fn check_buffer_size(device: &wgpu::Device, label: Option<&str>, usage: BufferUsages, size: u64) {
158 let limits = device.limits();
159 let labeled = || label.map(|l| format!(" '{l}'")).unwrap_or_default();
160 if size > limits.max_buffer_size {
161 panic!("buffer{}: {size} bytes exceeds this device's max_buffer_size ({})", labeled(), limits.max_buffer_size);
162 }
163 if usage.contains(BufferUsages::UNIFORM) && size > limits.max_uniform_buffer_binding_size {
164 panic!(
165 "buffer{}: {size} bytes exceeds this device's max_uniform_buffer_binding_size ({})",
166 labeled(),
167 limits.max_uniform_buffer_binding_size
168 );
169 }
170 if usage.contains(BufferUsages::STORAGE) && size > limits.max_storage_buffer_binding_size {
171 panic!(
172 "buffer{}: {size} bytes exceeds this device's max_storage_buffer_binding_size ({})",
173 labeled(),
174 limits.max_storage_buffer_binding_size
175 );
176 }
177}
178
179// ---------------------------------------------------------------------
180// Dynamically-offset buffers
181// ---------------------------------------------------------------------
182
183enum DynamicKind {
184 Uniform,
185 Storage,
186}
187
188/// Builds a [`DynamicBuffer`] — empty, sized and aligned to hold `count`
189/// dynamically-offset elements of `element_size` bytes each — for one large
190/// buffer holding many objects'/elements' data, rebound at a different
191/// offset via `set_bind_group`'s dynamic offsets slice instead of a bind
192/// group per object/dispatch. Pair with a layout entry from
193/// [`BindingKind::dynamic_uniform_buffer`](super::binding::BindingKind::dynamic_uniform_buffer)/
194/// [`dynamic_storage_buffer`](super::binding::BindingKind::dynamic_storage_buffer).
195///
196/// ```ignore
197/// let dynamic = DynamicBufferBuilder::uniform(element_size, count).build(&backend);
198/// // ... later, per element:
199/// dynamic.write_element(index, &element_bytes);
200/// // ... at draw time:
201/// pass.set_bind_group(0, Some(&bind_group), &[index as u32 * dynamic.stride() as u32]);
202/// ```
203pub struct DynamicBufferBuilder<'a> {
204 label: Option<&'a str>,
205 kind: DynamicKind,
206 element_size: u64,
207 count: u64,
208}
209
210impl<'a> DynamicBufferBuilder<'a> {
211 pub fn uniform(element_size: u64, count: u64) -> Self {
212 Self { label: None, kind: DynamicKind::Uniform, element_size, count }
213 }
214
215 pub fn storage(element_size: u64, count: u64) -> Self {
216 Self { label: None, kind: DynamicKind::Storage, element_size, count }
217 }
218
219 pub fn label(mut self, label: impl Into<Option<&'a str>>) -> Self {
220 self.label = label.into();
221 self
222 }
223
224 pub fn build(self, backend: &WGPUBackend) -> DynamicBuffer {
225 let (usage, stride) = match self.kind {
226 DynamicKind::Uniform => (
227 BufferUsages::UNIFORM | BufferUsages::COPY_DST,
228 dynamic_uniform_offset_stride(backend, self.element_size),
229 ),
230 DynamicKind::Storage => (
231 BufferUsages::STORAGE | BufferUsages::COPY_DST,
232 dynamic_storage_offset_stride(backend, self.element_size),
233 ),
234 };
235 let buffer = BufferBuilder::empty(stride * self.count).label(self.label).usage(usage).build(backend);
236 DynamicBuffer::new(buffer, stride, self.element_size)
237 }
238}
239
240/// Rounds `element_size` up to the device's required alignment for dynamic offsets on
241/// uniform buffers, giving the stride to use when packing multiple elements into one
242/// buffer for use with [`BindingKind::dynamic_uniform_buffer`](super::binding::BindingKind::dynamic_uniform_buffer).
243/// [`DynamicBufferBuilder`] calls this for you — use it directly only if you're sizing
244/// a dynamic buffer some other way.
245pub fn dynamic_uniform_offset_stride(backend: &WGPUBackend, element_size: u64) -> u64 {
246 dynamic_uniform_offset_stride_raw(&backend.device, element_size)
247}
248
249/// Internal primitive behind [`dynamic_uniform_offset_stride`] — used
250/// directly only by tests, which have a raw `wgpu::Device` but no full
251/// [`WGPUBackend`].
252pub(crate) fn dynamic_uniform_offset_stride_raw(device: &wgpu::Device, element_size: u64) -> u64 {
253 align_to(element_size, device.limits().min_uniform_buffer_offset_alignment as u64)
254}
255
256/// Same as [`dynamic_uniform_offset_stride`] but for storage buffers.
257pub fn dynamic_storage_offset_stride(backend: &WGPUBackend, element_size: u64) -> u64 {
258 dynamic_storage_offset_stride_raw(&backend.device, element_size)
259}
260
261/// Internal primitive behind [`dynamic_storage_offset_stride`] — used
262/// directly only by tests, which have a raw `wgpu::Device` but no full
263/// [`WGPUBackend`].
264pub(crate) fn dynamic_storage_offset_stride_raw(device: &wgpu::Device, element_size: u64) -> u64 {
265 align_to(element_size, device.limits().min_storage_buffer_offset_alignment as u64)
266}
267
268fn align_to(size: u64, alignment: u64) -> u64 {
269 size.div_ceil(alignment) * alignment
270}
271
272/// Builds the bind group entry resource for a dynamically-offset binding. Unlike
273/// `buffer.as_entire_binding()`, this scopes the entry to a single `element_size`-sized
274/// element starting at offset 0 in the buffer — required because the dynamic offset passed
275/// to `set_bind_group` at draw/dispatch time is added on top of this base range, and wgpu
276/// validates `offset + size <= buffer size`. Binding the whole buffer here would make any
277/// nonzero dynamic offset fail validation. [`BindGroupBuilder::dynamic_buffer`] calls this
278/// for you.
279fn dynamic_buffer_binding(buffer: &wgpu::Buffer, element_size: u64) -> wgpu::BindingResource<'_> {
280 wgpu::BindingResource::Buffer(wgpu::BufferBinding {
281 buffer,
282 offset: 0,
283 size: wgpu::BufferSize::new(element_size),
284 })
285}
286
287// ---------------------------------------------------------------------
288// Bind groups
289// ---------------------------------------------------------------------
290
291/// Builds a `wgpu::BindGroup` against `layout` one binding at a time.
292///
293/// The plain methods ([`buffer`](Self::buffer), [`texture_2d`](Self::texture_2d),
294/// [`sampler`](Self::sampler), [`dynamic_buffer`](Self::dynamic_buffer), ...)
295/// assign `@binding(N)` in call order, starting at 0 — the common case,
296/// matching a layout whose entries are numbered the same way. If your
297/// target's bindings aren't contiguous from 0 (e.g. looked up by name
298/// against a [`BindGroupTarget`](super::binding::BindGroupTarget), as
299/// [`GPUBindingInstance`](super::instance::GPUBindingInstance) does), use
300/// the `_at` variants to assign an explicit `@binding(N)` instead.
301///
302/// ```ignore
303/// let bind_group = BindGroupBuilder::new(&layout)
304/// .label("camera_bind_group")
305/// .buffer(&camera_buffer)
306/// .build(&backend);
307/// ```
308pub struct BindGroupBuilder<'a> {
309 label: Option<&'a str>,
310 layout: &'a wgpu::BindGroupLayout,
311 entries: Vec<wgpu::BindGroupEntry<'a>>,
312 next_binding: u32,
313}
314
315impl<'a> BindGroupBuilder<'a> {
316 pub fn new(layout: &'a BindGroupLayout) -> Self {
317 Self::new_raw(layout.raw())
318 }
319
320 /// Internal primitive behind [`new`](Self::new) — used directly only by
321 /// code with its own raw `wgpu::BindGroupLayout` that never goes through
322 /// [`BindGroupLayoutBuilder`](super::binding::BindGroupLayoutBuilder)
323 /// (mipmap generation's fixed-shape blit layout).
324 pub(crate) fn new_raw(layout: &'a wgpu::BindGroupLayout) -> Self {
325 Self { label: None, layout, entries: Vec::new(), next_binding: 0 }
326 }
327
328 pub fn label(mut self, label: impl Into<Option<&'a str>>) -> Self {
329 self.label = label.into();
330 self
331 }
332
333 /// Binds `buffer` in its entirety at the next `@binding(N)` (call order,
334 /// starting at 0).
335 pub fn buffer(self, buffer: &'a Buffer) -> Self {
336 let binding = self.next_binding;
337 self.buffer_at(binding, buffer)
338 }
339
340 /// Same as [`buffer`](Self::buffer) but at an explicit `@binding(N)`.
341 pub fn buffer_at(mut self, binding: u32, buffer: &'a Buffer) -> Self {
342 self.entries.push(wgpu::BindGroupEntry { binding, resource: buffer.raw().as_entire_binding() });
343 self.next_binding = self.next_binding.max(binding + 1);
344 self
345 }
346
347 /// Binds `buffer` scoped to one element (see [`DynamicBuffer::element_size`])
348 /// at the next `@binding(N)`.
349 pub fn dynamic_buffer(self, buffer: &'a DynamicBuffer) -> Self {
350 let binding = self.next_binding;
351 self.dynamic_buffer_at(binding, buffer)
352 }
353
354 /// Same as [`dynamic_buffer`](Self::dynamic_buffer) but at an explicit `@binding(N)`.
355 pub fn dynamic_buffer_at(mut self, binding: u32, buffer: &'a DynamicBuffer) -> Self {
356 let resource = dynamic_buffer_binding(buffer.buffer.raw(), buffer.element_size);
357 self.entries.push(wgpu::BindGroupEntry { binding, resource });
358 self.next_binding = self.next_binding.max(binding + 1);
359 self
360 }
361
362 /// Binds a 2D texture's view at the next `@binding(N)`.
363 pub fn texture_2d(self, texture: &'a GPUTexture) -> Self {
364 let binding = self.next_binding;
365 self.texture_2d_at(binding, texture)
366 }
367
368 /// Same as [`texture_2d`](Self::texture_2d) but at an explicit `@binding(N)`.
369 pub fn texture_2d_at(self, binding: u32, texture: &'a GPUTexture) -> Self {
370 self.texture_view_raw_at(binding, texture.view())
371 }
372
373 /// Binds a texture array's view at the next `@binding(N)`.
374 pub fn texture_array(self, texture: &'a GPUTextureArray) -> Self {
375 let binding = self.next_binding;
376 self.texture_array_at(binding, texture)
377 }
378
379 /// Same as [`texture_array`](Self::texture_array) but at an explicit `@binding(N)`.
380 pub fn texture_array_at(self, binding: u32, texture: &'a GPUTextureArray) -> Self {
381 self.texture_view_raw_at(binding, texture.view())
382 }
383
384 /// Binds a cubemap's view at the next `@binding(N)`.
385 pub fn texture_cubemap(self, texture: &'a GPUCubemap) -> Self {
386 let binding = self.next_binding;
387 self.texture_cubemap_at(binding, texture)
388 }
389
390 /// Same as [`texture_cubemap`](Self::texture_cubemap) but at an explicit `@binding(N)`.
391 pub fn texture_cubemap_at(self, binding: u32, texture: &'a GPUCubemap) -> Self {
392 self.texture_view_raw_at(binding, texture.view())
393 }
394
395 /// Binds an opaque [`TextureView`] — a render target built via
396 /// [`TextureBuilder`](super::texture_view::TextureBuilder)/
397 /// [`GPUCubemap::face_attachment`](GPUCubemap::face_attachment) — at the
398 /// next `@binding(N)`, for sampling it back in a later pass (a shadow
399 /// map, a post-process input, ...).
400 pub fn texture_view(self, view: &'a TextureView) -> Self {
401 let binding = self.next_binding;
402 self.texture_view_at(binding, view)
403 }
404
405 /// Same as [`texture_view`](Self::texture_view) but at an explicit `@binding(N)`.
406 pub fn texture_view_at(self, binding: u32, view: &'a TextureView) -> Self {
407 self.texture_view_raw_at(binding, view.raw())
408 }
409
410 /// Low-level primitive behind every `texture_*` method above — kept
411 /// `pub(crate)` for internal code (mipmap generation's blit pass) that
412 /// binds an ad-hoc single-mip-level view rather than a whole
413 /// [`GPUTexture`]/[`GPUTextureArray`]/[`GPUCubemap`]/[`TextureView`].
414 pub(crate) fn texture_view_raw_at(mut self, binding: u32, view: &'a wgpu::TextureView) -> Self {
415 self.entries.push(wgpu::BindGroupEntry { binding, resource: wgpu::BindingResource::TextureView(view) });
416 self.next_binding = self.next_binding.max(binding + 1);
417 self
418 }
419
420 /// Binds `sampler` at the next `@binding(N)`.
421 pub fn sampler(self, sampler: &'a Sampler) -> Self {
422 let binding = self.next_binding;
423 self.sampler_at(binding, sampler)
424 }
425
426 /// Same as [`sampler`](Self::sampler) but at an explicit `@binding(N)`.
427 pub fn sampler_at(self, binding: u32, sampler: &'a Sampler) -> Self {
428 self.sampler_raw_at(binding, sampler.raw())
429 }
430
431 /// Low-level primitive behind [`sampler`](Self::sampler) — kept
432 /// `pub(crate)` for the same internal reason as
433 /// [`texture_view_raw_at`](Self::texture_view_raw_at).
434 pub(crate) fn sampler_raw_at(mut self, binding: u32, sampler: &'a wgpu::Sampler) -> Self {
435 self.entries.push(wgpu::BindGroupEntry { binding, resource: wgpu::BindingResource::Sampler(sampler) });
436 self.next_binding = self.next_binding.max(binding + 1);
437 self
438 }
439
440 pub fn build(self, backend: &WGPUBackend) -> BindGroup {
441 BindGroup(self.build_raw(&backend.device))
442 }
443
444 /// Internal primitive behind [`build`](Self::build) — used directly only
445 /// by code that needs a raw `wgpu::BindGroup` to feed into a raw
446 /// `wgpu::RenderPass` it built itself (mipmap generation's blit pass).
447 pub(crate) fn build_raw(self, device: &wgpu::Device) -> wgpu::BindGroup {
448 device.create_bind_group(&wgpu::BindGroupDescriptor {
449 label: self.label,
450 layout: self.layout,
451 entries: &self.entries,
452 })
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459 use crate::wgpu::test_util::with_device;
460
461 #[test]
462 fn a_size_within_every_limit_does_not_panic() {
463 with_device!(device, _queue, {
464 BufferBuilder::empty(64).uniform().build_raw(&device);
465 });
466 }
467
468 #[test]
469 fn exceeding_max_buffer_size_panics() {
470 with_device!(device, _queue, {
471 let too_big = device.limits().max_buffer_size + 1;
472 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
473 BufferBuilder::empty(too_big).usage(BufferUsages::COPY_DST).build_raw(&device);
474 }));
475 assert!(result.is_err(), "expected a panic for a size exceeding max_buffer_size");
476 });
477 }
478
479 #[test]
480 fn exceeding_max_uniform_buffer_binding_size_panics() {
481 with_device!(device, _queue, {
482 let too_big = device.limits().max_uniform_buffer_binding_size + 1;
483 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
484 BufferBuilder::empty(too_big).uniform().build_raw(&device);
485 }));
486 assert!(
487 result.is_err(),
488 "expected a panic for a size exceeding max_uniform_buffer_binding_size"
489 );
490 });
491 }
492
493 #[test]
494 fn exceeding_max_uniform_buffer_binding_size_is_fine_for_a_non_uniform_buffer() {
495 // Same size that panics as a uniform buffer above must NOT panic here — the tighter
496 // check only applies when `usage` actually includes UNIFORM (not STORAGE either, so
497 // this can't accidentally trip the sibling max_storage_buffer_binding_size check).
498 with_device!(device, _queue, {
499 let big = (device.limits().max_uniform_buffer_binding_size + 1).min(device.limits().max_buffer_size);
500 BufferBuilder::empty(big).usage(BufferUsages::COPY_DST).build_raw(&device);
501 });
502 }
503}