Skip to main content

unlit_wgpu/
texel_array.rs

1//! Texture-backed arrays, for devices whose shaders cannot read storage
2//! buffers.
3//!
4//! WebGL2 has no storage buffers at all: a device created with WebGL2's limits
5//! reports `max_storage_buffers_per_shader_stage == 0`, and a bind-group layout
6//! naming one is rejected outright. An array that would otherwise live in a
7//! `var<storage, read>` still has to be readable per vertex, and the one
8//! facility WebGL2 does have for that is a texture read — `textureLoad` is
9//! available in every stage, and a float texture's texels can be read as the
10//! `f32` values the array held.
11//!
12//! A [`TexelArray`] is that translation: a flat array of fixed-size elements
13//! stored in a 2D texture, addressed by element index, with the element's bytes
14//! laid out exactly as the storage buffer laid them out. Because the byte
15//! layout is the same, the CPU packing is shared: the same element bytes are
16//! uploaded whether the shader reads them as a buffer or as texels.
17//!
18//! # Layout
19//!
20//! The texture holds `f32` lanes in texel order. An element occupies a whole
21//! number of texels, and a row holds a whole number of elements, so an element
22//! never straddles a row boundary. The element index therefore maps to a texel
23//! index by a single multiplication, and the flat index maps to a texel
24//! coordinate by dividing by the row width — which is the texture's own width,
25//! so the shader reads it back with `textureDimensions` rather than being told
26//! it as a constant:
27//!
28//! ```wgsl
29//! let width = textureDimensions(tex).x;
30//! let texel = element * TEXELS_PER_ELEMENT;
31//! let value = textureLoad(tex, vec2<i32>(i32(texel % width), i32(texel / width)), 0);
32//! ```
33//!
34//! # Texel format
35//!
36//! An element whose size is a whole number of 16-byte rows is stored in
37//! `Rgba32Float`, four `f32` lanes per texel — a `mat4x4<f32>`, for instance,
38//! is four texels, one per column. Any other element size is stored one `f32`
39//! per texel in `R32Float`. Both are readable in every stage without the
40//! `FLOAT32_FILTERABLE` feature: filtering is what that feature gates, and
41//! `textureLoad` does not filter.
42//!
43//! Neither format needs a bitcast of the values. They are read as the `f32`s
44//! they are, which is why the formats are float rather than the `u32` a storage
45//! buffer would have held: a shader reading a `u32` texel back would have to
46//! bitcast every lane, and the lanes are the same bits either way. Only a
47//! `u32` *member* of an element needs one, and that is the shader's concern.
48//!
49//! # Rows and padding
50//!
51//! A row is always a whole number of [`wgpu::COPY_BYTES_PER_ROW_ALIGNMENT`]
52//! bytes, which is what a buffer-to-texture copy demands, so an upload can be
53//! staged and recorded into the frame's own encoder rather than costing a
54//! submission of its own. The row width is in *elements*, not bytes: the row is
55//! exactly `elements_per_row * element_size` bytes, so the only padding is the
56//! unused tail of the last populated row. An element never straddles a row, so
57//! growing the row width never splits one.
58
59use crate::resources::{TextureExt, TextureView};
60use crate::staging::StagingBuffer;
61
62/// A flat array of fixed-size elements, as a bindable resource.
63///
64/// A device with storage buffers holds an array in one; a device without them —
65/// WebGL2 — holds the same bytes in a [`TexelArray`] the shader reads with
66/// `textureLoad`. A caller that has to work on both builds its bind group from
67/// this handle rather than from a buffer, so the two paths differ in how the
68/// array is uploaded and in nothing else.
69///
70/// See the [module docs](self) for how the texel path lays the bytes out.
71#[derive(Clone, Debug, PartialEq, Eq)]
72pub enum ArrayHandle {
73    /// A storage buffer holding the whole array.
74    Buffer(wgpu::Buffer),
75    /// A texture holding the whole array.
76    Texel(TextureView),
77}
78
79impl ArrayHandle {
80    /// The bind-group resource that binds this array.
81    pub fn binding_resource(&self) -> wgpu::BindingResource<'_> {
82        match self {
83            Self::Buffer(buffer) => buffer.as_entire_binding(),
84            Self::Texel(view) => wgpu::BindingResource::TextureView(view.view()),
85        }
86    }
87
88    /// The layout entry binding an array of `element_size`-byte elements held
89    /// this way.
90    ///
91    /// This is the Rust half of the shader's interface: the composed variant
92    /// declares a storage buffer on the buffer path and a non-filterable float
93    /// texture on the texel path, and the two have to agree.
94    pub fn layout_entry(
95        &self,
96        binding: u32,
97        visibility: wgpu::ShaderStages,
98        element_size: u64,
99    ) -> wgpu::BindGroupLayoutEntry {
100        match self {
101            Self::Buffer(_) => wgpu::BindGroupLayoutEntry {
102                binding,
103                visibility,
104                ty: wgpu::BindingType::Buffer {
105                    ty: wgpu::BufferBindingType::Storage { read_only: true },
106                    has_dynamic_offset: false,
107                    min_binding_size: Some(core::num::NonZeroU64::new(element_size).expect(
108                        "an element has a non-zero size, which is a valid binding minimum",
109                    )),
110                },
111                count: None,
112            },
113            Self::Texel(_) => TexelArrayLayout::binding(binding, visibility),
114        }
115    }
116}
117
118/// Where an [`Array`]'s elements live.
119enum ArrayStorage {
120    /// A storage buffer, with the staging buffer uploads reach it through.
121    Buffer {
122        /// The buffer holding every element.
123        buffer: wgpu::Buffer,
124        /// The staging buffer the bytes are written into before the copy.
125        staging: StagingBuffer,
126    },
127    /// A texture, which carries its own staging and row padding.
128    ///
129    /// Boxed because a [`TexelArray`] holds a texture, its view and a scratch
130    /// row buffer, and an [`Array`] is stored inline by its owner: the pointer
131    /// keeps the buffer path from paying for the texel path's footprint.
132    Texel(Box<TexelArray>),
133}
134
135/// A growable flat array of fixed-size elements, held in whichever resource the
136/// device can read.
137///
138/// This is the device-independent front end to the [module](self): a caller
139/// sizes an array by its element count, uploads the same packed bytes on either
140/// path, and binds it through [`Self::handle`] without knowing which resource
141/// it landed in. Pass `Some(max_texture_dimension_2d)` to put it in a texture
142/// and `None` to put it in a storage buffer.
143pub struct Array {
144    /// The resource the elements live in.
145    storage: ArrayStorage,
146    /// Bytes per element.
147    element_size: u64,
148    /// The texture limit on the texel path, `None` on the buffer path.
149    max_dimension: Option<u32>,
150}
151
152impl Array {
153    /// Allocate an array with room for `capacity` elements of `element_size`
154    /// bytes.
155    ///
156    /// `max_dimension` is `Some` of the device's
157    /// [`wgpu::Limits::max_texture_dimension_2d`] to hold the elements in a
158    /// texture, or `None` to hold them in a storage buffer.
159    ///
160    /// # Panics
161    ///
162    /// Under the conditions [`TexelArray::new`] panics, on the texel path.
163    pub fn new(
164        device: &wgpu::Device,
165        label: Option<&str>,
166        element_size: u64,
167        capacity: u64,
168        max_dimension: Option<u32>,
169    ) -> Self {
170        let capacity = capacity.max(1);
171        let storage = match max_dimension {
172            Some(max_dimension) => ArrayStorage::Texel(Box::new(TexelArray::new(
173                device,
174                label,
175                element_size,
176                capacity,
177                max_dimension,
178            ))),
179            None => ArrayStorage::Buffer {
180                buffer: create_array_buffer(device, label, element_size, capacity),
181                staging: StagingBuffer::new(),
182            },
183        };
184        Self {
185            storage,
186            element_size,
187            max_dimension,
188        }
189    }
190
191    /// Bytes per element.
192    pub fn element_size(&self) -> u64 {
193        self.element_size
194    }
195
196    /// How many elements fit without growing.
197    pub fn capacity(&self) -> u64 {
198        match &self.storage {
199            ArrayStorage::Buffer { buffer, .. } => buffer.size() / self.element_size,
200            ArrayStorage::Texel(texel) => texel.layout().capacity(),
201        }
202    }
203
204    /// Whether the elements are held in a texture rather than a buffer.
205    pub fn is_texel(&self) -> bool {
206        matches!(self.storage, ArrayStorage::Texel(_))
207    }
208
209    /// The bindable handle to the current resource.
210    ///
211    /// Cloned rather than borrowed so a caller can move it into the resource
212    /// graph, which is what makes a replaced array mark the bind groups built
213    /// from it dirty.
214    pub fn handle(&self) -> ArrayHandle {
215        match &self.storage {
216            ArrayStorage::Buffer { buffer, .. } => ArrayHandle::Buffer(buffer.clone()),
217            ArrayStorage::Texel(texel) => ArrayHandle::Texel(texel.view().clone()),
218        }
219    }
220
221    /// Grow to hold at least `capacity` elements, returning whether the
222    /// resource was replaced.
223    ///
224    /// A replacement is what a caller has to publish: every bind group built
225    /// from the old handle is stale and has to be rebuilt. A no-op when the
226    /// array already has the room.
227    ///
228    /// # Panics
229    ///
230    /// Under the conditions [`TexelArray::new`] panics, on the texel path.
231    pub fn grow_to(&mut self, device: &wgpu::Device, label: Option<&str>, capacity: u64) -> bool {
232        if capacity <= self.capacity() {
233            return false;
234        }
235        self.storage = match self.max_dimension {
236            Some(max_dimension) => ArrayStorage::Texel(Box::new(TexelArray::new(
237                device,
238                label,
239                self.element_size,
240                capacity,
241                max_dimension,
242            ))),
243            None => ArrayStorage::Buffer {
244                buffer: create_array_buffer(device, label, self.element_size, capacity),
245                staging: StagingBuffer::new(),
246            },
247        };
248        true
249    }
250
251    /// Upload `elements` — whole elements, laid out as the storage buffer would
252    /// have held them — recording the copy into `encoder`.
253    ///
254    /// # Panics
255    ///
256    /// If `elements` is not a whole number of elements, or holds more than
257    /// [`Self::capacity`] of them.
258    pub fn upload(
259        &mut self,
260        device: &wgpu::Device,
261        encoder: &mut wgpu::CommandEncoder,
262        elements: &[u8],
263    ) {
264        match &mut self.storage {
265            ArrayStorage::Buffer { buffer, staging } => {
266                staging.write(device, encoder, buffer, 0, elements);
267            }
268            ArrayStorage::Texel(texel) => texel.upload(device, encoder, elements),
269        }
270    }
271
272    /// Upload `elements` immediately, for an array written once rather than
273    /// every frame.
274    ///
275    /// This mirrors [`wgpu::Queue::write_buffer`] and
276    /// [`wgpu::Queue::write_texture`]: the bytes reach the resource through a
277    /// temporary buffer wgpu allocates and submits for the caller, so there is
278    /// no encoder to batch into. Prefer [`Self::upload`] for data that changes
279    /// every frame, where a reused staging buffer costs nothing per frame.
280    ///
281    /// # Panics
282    ///
283    /// If `elements` is not a whole number of elements, or holds more than
284    /// [`Self::capacity`] of them.
285    pub fn write(&mut self, queue: &wgpu::Queue, elements: &[u8]) {
286        match &mut self.storage {
287            ArrayStorage::Buffer { buffer, .. } => queue.write_buffer(buffer, 0, elements),
288            ArrayStorage::Texel(texel) => texel.write(queue, elements),
289        }
290    }
291}
292
293/// Create the storage buffer an array's elements live in.
294fn create_array_buffer(
295    device: &wgpu::Device,
296    label: Option<&str>,
297    element_size: u64,
298    capacity: u64,
299) -> wgpu::Buffer {
300    device.create_buffer(&wgpu::BufferDescriptor {
301        label,
302        size: capacity.max(1) * element_size,
303        usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
304        mapped_at_creation: false,
305    })
306}
307
308/// A texel array's shape and texel format.
309///
310/// Derived from an element size and a capacity; see the [module
311/// docs](self) for how the row width follows from the two.
312#[derive(Clone, Copy, Debug, PartialEq, Eq)]
313pub struct TexelArrayLayout {
314    /// The format the texture is created with.
315    pub texel_format: wgpu::TextureFormat,
316    /// `f32` lanes per texel: 1 for `R32Float`, 4 for `Rgba32Float`.
317    pub lanes_per_texel: u32,
318    /// Bytes per element.
319    pub element_size: u64,
320    /// How many elements one row holds.
321    pub elements_per_row: u32,
322    /// The texture's width in texels.
323    pub width: u32,
324    /// The texture's height in texels.
325    pub height: u32,
326}
327
328impl TexelArrayLayout {
329    /// Lay out `capacity` elements of `element_size` bytes in a texture no
330    /// larger than `max_dimension` on either axis.
331    ///
332    /// `max_dimension` is the device's
333    /// [`wgpu::Limits::max_texture_dimension_2d`].
334    ///
335    /// # Panics
336    ///
337    /// If `element_size` is not a whole number of `f32` lanes, or if `capacity`
338    /// elements of that size cannot be stored in a texture within
339    /// `max_dimension`.
340    pub fn new(element_size: u64, capacity: u64, max_dimension: u32) -> Self {
341        assert!(
342            element_size > 0 && element_size.is_multiple_of(size_of::<f32>() as u64),
343            "a texel array element of {element_size} bytes is not a whole number of `f32` lanes"
344        );
345        // Four lanes per texel whenever the element is a whole number of
346        // 16-byte rows, so a `mat4x4<f32>` or a 48-byte struct reads as a few
347        // whole texels instead of one texel per scalar.
348        let lanes_per_texel = if element_size.is_multiple_of(16) {
349            4
350        } else {
351            1
352        };
353        let texel_format = match lanes_per_texel {
354            4 => wgpu::TextureFormat::Rgba32Float,
355            _ => wgpu::TextureFormat::R32Float,
356        };
357        let texel_bytes = u64::from(lanes_per_texel) * size_of::<f32>() as u64;
358        let texels_per_element = element_size / texel_bytes;
359        // One element has to fit in one row, so an element wider than the
360        // texture limit cannot be stored at all: the row-width search below
361        // could only clamp to one element per row and overshoot the limit.
362        assert!(
363            texels_per_element <= u64::from(max_dimension),
364            "a {element_size}-byte element spans {texels_per_element} texels, \
365             wider than the {max_dimension}-texel texture limit"
366        );
367
368        let capacity = capacity.max(1);
369        let elements_per_row =
370            row_elements(element_size, texels_per_element, capacity, max_dimension);
371        let width = u32::try_from(u64::from(elements_per_row) * texels_per_element)
372            .expect("a row width within `max_dimension` fits a `u32`");
373        let height = u32::try_from(capacity.div_ceil(u64::from(elements_per_row)))
374            .expect("a height within `max_dimension` fits a `u32`")
375            .max(1);
376
377        Self {
378            texel_format,
379            lanes_per_texel,
380            element_size,
381            elements_per_row,
382            width,
383            height,
384        }
385    }
386
387    /// Bytes one row spans, padding included.
388    ///
389    /// Always a whole number of [`wgpu::COPY_BYTES_PER_ROW_ALIGNMENT`] bytes.
390    pub fn row_bytes(&self) -> u64 {
391        u64::from(self.elements_per_row) * self.element_size
392    }
393
394    /// How many elements fit without growing the texture.
395    pub fn capacity(&self) -> u64 {
396        u64::from(self.elements_per_row) * u64::from(self.height)
397    }
398
399    /// The texture size to create.
400    pub fn texture_size(&self) -> wgpu::Extent3d {
401        wgpu::Extent3d {
402            width: self.width,
403            height: self.height,
404            depth_or_array_layers: 1,
405        }
406    }
407
408    /// How many rows `element_count` elements occupy.
409    fn rows_for(&self, element_count: u64) -> u32 {
410        u32::try_from(element_count.div_ceil(u64::from(self.elements_per_row)))
411            .expect("no more rows than the texture has")
412            .max(1)
413    }
414
415    /// Write `elements` into `out` as the row-padded image the texture holds.
416    ///
417    /// `elements` is the packed element bytes — the same bytes a storage buffer
418    /// would have been uploaded, and the same ones [`Self::element_size`]
419    /// describes one of. `out` is reused across calls, so a steady frame
420    /// reallocates nothing. Only the rows the elements reach are written; the
421    /// rest of the texture is left as it was, which is sound because the shader
422    /// addresses nothing past the element count the caller uploaded.
423    ///
424    /// # Panics
425    ///
426    /// If `elements` is not a whole number of elements, or holds more than
427    /// [`Self::capacity`] of them.
428    pub fn pack(&self, elements: &[u8], out: &mut Vec<u8>) {
429        assert!(
430            (elements.len() as u64).is_multiple_of(self.element_size),
431            "{} bytes are not a whole number of {}-byte elements",
432            elements.len(),
433            self.element_size
434        );
435        assert!(
436            elements.len() as u64 <= self.capacity() * self.element_size,
437            "{} bytes hold more than the {} elements the array has room for",
438            elements.len(),
439            self.capacity()
440        );
441        let element_count = elements.len() as u64 / self.element_size;
442        let used = u64::from(self.rows_for(element_count)) * self.row_bytes();
443        out.clear();
444        out.extend_from_slice(elements);
445        out.resize(used as usize, 0);
446    }
447
448    /// The bind-group layout entry that binds one of these as a texture.
449    pub fn binding(binding: u32, visibility: wgpu::ShaderStages) -> wgpu::BindGroupLayoutEntry {
450        wgpu::BindGroupLayoutEntry {
451            binding,
452            visibility,
453            ty: wgpu::BindingType::Texture {
454                // Neither `R32Float` nor `Rgba32Float` is filterable without
455                // `FLOAT32_FILTERABLE`, and `textureLoad` never filters.
456                sample_type: wgpu::TextureSampleType::Float { filterable: false },
457                view_dimension: wgpu::TextureViewDimension::D2,
458                multisampled: false,
459            },
460            count: None,
461        }
462    }
463}
464
465/// The number of elements a row holds: enough for a row to reach the copy
466/// alignment, then doubled until the elements fit within `max_dimension` rows.
467fn row_elements(
468    element_size: u64,
469    texels_per_element: u64,
470    capacity: u64,
471    max_dimension: u32,
472) -> u32 {
473    let alignment = u64::from(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
474    // The fewest elements whose bytes make up whole alignment units, so the row
475    // needs no padding between it and the next and a buffer-to-texture copy
476    // accepts it as it stands.
477    let base = alignment / gcd(element_size, alignment);
478    // The width is `elements_per_row * texels_per_element`, so the row cannot
479    // hold more elements than that many texels allow.
480    let max_elements_per_row = (u64::from(max_dimension) / texels_per_element).max(1);
481    assert!(
482        base <= max_elements_per_row,
483        "a {element_size}-byte element needs a {}-texel row, wider than the \
484         {max_dimension}-texel texture limit",
485        base * texels_per_element
486    );
487
488    let mut elements_per_row = base;
489    while capacity.div_ceil(elements_per_row) > u64::from(max_dimension) {
490        assert!(
491            elements_per_row < max_elements_per_row,
492            "{capacity} elements of {element_size} bytes do not fit in a \
493             {max_dimension}x{max_dimension} texture"
494        );
495        // Doubling keeps the row a whole number of alignment units.
496        elements_per_row = (elements_per_row * 2).min(max_elements_per_row);
497    }
498    u32::try_from(elements_per_row).expect("a row within `max_dimension` fits a `u32`")
499}
500
501/// The greatest common divisor of `a` and `b`.
502fn gcd(mut a: u64, mut b: u64) -> u64 {
503    while b != 0 {
504        (a, b) = (b, a % b);
505    }
506    a
507}
508
509/// Create the texture a layout describes.
510fn create_texture(
511    device: &wgpu::Device,
512    label: Option<&str>,
513    layout: &TexelArrayLayout,
514) -> wgpu::Texture {
515    device.create_texture(&wgpu::TextureDescriptor {
516        label,
517        size: layout.texture_size(),
518        mip_level_count: 1,
519        sample_count: 1,
520        dimension: wgpu::TextureDimension::D2,
521        format: layout.texel_format,
522        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
523        view_formats: &[],
524    })
525}
526
527/// A flat array of fixed-size elements stored in a texture.
528///
529/// See the [module docs](self) for what it is for and how it is laid out.
530pub struct TexelArray {
531    /// The texture the elements live in.
532    texture: wgpu::Texture,
533    /// The whole texture as one view, which is what a bind group binds.
534    view: TextureView,
535    /// The array's shape and format.
536    layout: TexelArrayLayout,
537    /// The staging buffer the row-padded image reaches the texture through,
538    /// reused across frames like every other upload.
539    staging: StagingBuffer,
540    /// The row-padded image the last upload was assembled in, kept so a steady
541    /// frame reallocates nothing.
542    scratch: Vec<u8>,
543}
544
545impl TexelArray {
546    /// Allocate an array with room for `capacity` elements of `element_size`
547    /// bytes.
548    ///
549    /// `max_dimension` is the device's
550    /// [`wgpu::Limits::max_texture_dimension_2d`]; the texture never exceeds it
551    /// on either axis.
552    ///
553    /// # Panics
554    ///
555    /// Under the conditions [`TexelArrayLayout::new`] panics.
556    pub fn new(
557        device: &wgpu::Device,
558        label: Option<&str>,
559        element_size: u64,
560        capacity: u64,
561        max_dimension: u32,
562    ) -> Self {
563        let layout = TexelArrayLayout::new(element_size, capacity, max_dimension);
564        let texture = create_texture(device, label, &layout);
565        let view = TextureExt::create_view(&texture, &wgpu::TextureViewDescriptor::default());
566        Self {
567            texture,
568            view,
569            layout,
570            staging: StagingBuffer::new(),
571            scratch: Vec::new(),
572        }
573    }
574
575    /// The texture the elements live in.
576    pub fn texture(&self) -> &wgpu::Texture {
577        &self.texture
578    }
579
580    /// The view a bind group binds.
581    pub fn view(&self) -> &TextureView {
582        &self.view
583    }
584
585    /// The array's shape and format.
586    pub fn layout(&self) -> &TexelArrayLayout {
587        &self.layout
588    }
589
590    /// The bind-group resource that binds this array.
591    pub fn binding_resource(&self) -> wgpu::BindingResource<'_> {
592        wgpu::BindingResource::TextureView(self.view.view())
593    }
594
595    /// Upload `elements` — whole elements, laid out as the storage buffer would
596    /// have held them — into the texture, recording the copy into `encoder`.
597    ///
598    /// The bytes go through a reused staging buffer, so a steady frame
599    /// allocates nothing and the copy is batched with the rest of the frame's
600    /// work rather than costing a submission of its own.
601    ///
602    /// # Panics
603    ///
604    /// Under the conditions [`TexelArrayLayout::pack`] panics.
605    pub fn upload(
606        &mut self,
607        device: &wgpu::Device,
608        encoder: &mut wgpu::CommandEncoder,
609        elements: &[u8],
610    ) {
611        self.layout.pack(elements, &mut self.scratch);
612        let layout = wgpu::TexelCopyBufferLayout {
613            offset: 0,
614            bytes_per_row: Some(
615                u32::try_from(self.layout.row_bytes())
616                    .expect("a row no wider than the texture limit fits a `u32`"),
617            ),
618            rows_per_image: Some(self.layout.height),
619        };
620        // Upload only the populated rows. The capacity-sized texture would
621        // read past the deliberately shorter packed image on every partial
622        // upload, especially after a many-row array grows.
623        let size = wgpu::Extent3d {
624            height: self
625                .layout
626                .rows_for(elements.len() as u64 / self.layout.element_size),
627            ..self.layout.texture_size()
628        };
629        self.staging
630            .write_texture(device, encoder, &self.texture, layout, size, &self.scratch);
631    }
632
633    /// Upload `elements` through the queue, for data written once.
634    ///
635    /// The rows are re-aligned by [`wgpu::Queue::write_texture`], so unlike
636    /// [`Self::upload`] this needs no copy alignment — but it also costs a
637    /// submission of its own.
638    ///
639    /// # Panics
640    ///
641    /// Under the conditions [`TexelArrayLayout::pack`] panics.
642    pub fn write(&mut self, queue: &wgpu::Queue, elements: &[u8]) {
643        self.layout.pack(elements, &mut self.scratch);
644        // Only the rows `elements` fill: the packed image is shorter than the
645        // capacity-sized texture, so writing the whole texture reads past it.
646        let size = wgpu::Extent3d {
647            height: self
648                .layout
649                .rows_for(elements.len() as u64 / self.layout.element_size),
650            ..self.layout.texture_size()
651        };
652        queue.write_texture(
653            wgpu::TexelCopyTextureInfo {
654                texture: &self.texture,
655                mip_level: 0,
656                origin: wgpu::Origin3d::ZERO,
657                aspect: wgpu::TextureAspect::All,
658            },
659            &self.scratch,
660            wgpu::TexelCopyBufferLayout {
661                offset: 0,
662                bytes_per_row: Some(
663                    u32::try_from(self.layout.row_bytes())
664                        .expect("a row no wider than the texture limit fits a `u32`"),
665                ),
666                rows_per_image: Some(self.layout.height),
667            },
668            size,
669        );
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676    use crate::util::test::noop_device;
677
678    /// The element size of a `mat4x4<f32>`: four texels of `Rgba32Float`.
679    const MATRIX_ELEMENT: u64 = 64;
680    /// The element size of a struct-shaped element: three `Rgba32Float` texels,
681    /// a size that is neither the matrix element above nor a scalar.
682    const STRUCT_ELEMENT: u64 = 48;
683    /// The element size of a bare `f32` array.
684    const SCALAR_ELEMENT: u64 = 4;
685    /// A texture limit no test array reaches.
686    const ROOMY: u32 = 2048;
687
688    #[test]
689    fn a_matrix_element_reads_as_four_texels_of_four_lanes() {
690        let layout = TexelArrayLayout::new(MATRIX_ELEMENT, 8, ROOMY);
691
692        assert_eq!(layout.texel_format, wgpu::TextureFormat::Rgba32Float);
693        assert_eq!(layout.lanes_per_texel, 4);
694        assert_eq!(layout.elements_per_row, 4, "a row is 256 bytes");
695        assert_eq!(layout.row_bytes(), 256);
696        assert_eq!(layout.width, 16, "four elements of four texels each");
697        assert_eq!(layout.height, 2);
698    }
699
700    #[test]
701    fn a_scalar_element_reads_as_one_lane_per_texel() {
702        let layout = TexelArrayLayout::new(SCALAR_ELEMENT, 128, ROOMY);
703
704        assert_eq!(layout.texel_format, wgpu::TextureFormat::R32Float);
705        assert_eq!(layout.lanes_per_texel, 1);
706        assert_eq!(layout.elements_per_row, 64, "a row is 256 bytes");
707        assert_eq!(layout.width, 64);
708        assert_eq!(layout.height, 2);
709    }
710
711    #[test]
712    fn a_48_byte_element_still_reaches_the_row_alignment() {
713        // 48 bytes does not divide 256, so the row takes 16 elements — 768
714        // bytes, three alignment units — instead of the 6 that would fit in one.
715        let layout = TexelArrayLayout::new(STRUCT_ELEMENT, 32, ROOMY);
716
717        assert_eq!(layout.texel_format, wgpu::TextureFormat::Rgba32Float);
718        assert_eq!(layout.elements_per_row, 16);
719        assert_eq!(layout.row_bytes(), 768);
720        assert!(
721            layout
722                .row_bytes()
723                .is_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as u64)
724        );
725        assert_eq!(layout.width, 48, "sixteen elements of three texels each");
726        assert_eq!(layout.height, 2);
727    }
728
729    #[test]
730    fn every_element_size_produces_an_aligned_row() {
731        for element_size in [4u64, 8, 12, 16, 20, 32, 36, 48, 64, 96, 128, 256] {
732            let layout = TexelArrayLayout::new(element_size, 4, ROOMY);
733            assert!(
734                layout
735                    .row_bytes()
736                    .is_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as u64),
737                "a {element_size}-byte element gave a {}-byte row",
738                layout.row_bytes()
739            );
740            assert_eq!(
741                layout.width % layout.lanes_per_texel,
742                0,
743                "a {element_size}-byte element's row holds whole texels"
744            );
745        }
746    }
747
748    #[test]
749    fn an_element_that_is_not_a_whole_number_of_lanes_panics() {
750        let result = std::panic::catch_unwind(|| TexelArrayLayout::new(6, 1, ROOMY));
751
752        assert!(
753            result.is_err(),
754            "6 bytes is not a whole number of `f32` lanes"
755        );
756    }
757
758    #[test]
759    fn an_element_wider_than_the_texture_limit_panics() {
760        // A `Rgba32Float` texel is 16 bytes, so a 2048-texel row holds 32768
761        // bytes; one 16-byte lane more than that has no row wide enough.
762        let widest = u64::from(ROOMY) * 16;
763        assert!(std::panic::catch_unwind(|| TexelArrayLayout::new(widest, 1, ROOMY)).is_ok());
764
765        let result = std::panic::catch_unwind(|| TexelArrayLayout::new(widest + 16, 1, ROOMY));
766
767        assert!(result.is_err());
768    }
769
770    #[test]
771    fn an_empty_array_still_has_one_row() {
772        let layout = TexelArrayLayout::new(SCALAR_ELEMENT, 0, ROOMY);
773
774        assert_eq!(layout.height, 1, "a texture cannot have a zero-height axis");
775        assert_eq!(layout.capacity(), 64);
776    }
777
778    #[test]
779    fn a_capacity_beyond_one_column_grows_the_height() {
780        let layout = TexelArrayLayout::new(SCALAR_ELEMENT, 64 * 8, ROOMY);
781
782        assert_eq!(
783            layout.elements_per_row, 64,
784            "the row stays at one alignment"
785        );
786        assert_eq!(layout.width, 64);
787        assert_eq!(layout.height, 8, "the rows carry the extra elements");
788    }
789
790    #[test]
791    fn a_capacity_beyond_the_texture_limit_widens_the_row() {
792        // 2048 rows of 64 elements is the limit; one more element forces the
793        // row to widen instead of the height to grow past the limit.
794        let layout = TexelArrayLayout::new(SCALAR_ELEMENT, 64 * u64::from(ROOMY) + 1, ROOMY);
795
796        assert_eq!(layout.elements_per_row, 128, "the row doubled");
797        assert_eq!(layout.width, 128);
798        assert_eq!(layout.height, 1025, "and the elements now fit");
799        assert!(layout.height <= ROOMY);
800    }
801
802    #[test]
803    fn a_capacity_that_cannot_fit_at_all_panics() {
804        // One scalar per texel caps the array at 2048 rows of 2048 elements,
805        // so one element more than that has nowhere to go.
806        let fit = u64::from(ROOMY) * u64::from(ROOMY);
807        assert!(
808            std::panic::catch_unwind(|| TexelArrayLayout::new(SCALAR_ELEMENT, fit, ROOMY)).is_ok()
809        );
810
811        let result =
812            std::panic::catch_unwind(|| TexelArrayLayout::new(SCALAR_ELEMENT, fit + 1, ROOMY));
813
814        assert!(result.is_err());
815    }
816
817    #[test]
818    fn packing_pads_only_to_the_last_populated_row() {
819        let layout = TexelArrayLayout::new(STRUCT_ELEMENT, 32, ROOMY);
820        let elements: Vec<u8> = (0..6 * STRUCT_ELEMENT).map(|i| i as u8).collect();
821        let mut out = Vec::new();
822
823        layout.pack(&elements, &mut out);
824
825        assert_eq!(
826            out.len() as u64,
827            layout.row_bytes(),
828            "six of the sixteen elements in one row need one row's bytes"
829        );
830        assert_eq!(
831            &out[..elements.len()],
832            &elements[..],
833            "the elements are contiguous from the start"
834        );
835        assert!(
836            out[elements.len()..].iter().all(|byte| *byte == 0),
837            "the unused tail of the last row is zeroed"
838        );
839    }
840
841    #[test]
842    fn a_partial_upload_copies_only_its_populated_rows() {
843        let layout = TexelArrayLayout::new(SCALAR_ELEMENT, 256, ROOMY);
844        let elements = [7u8; 65 * SCALAR_ELEMENT as usize];
845        let mut scratch = Vec::new();
846        layout.pack(&elements, &mut scratch);
847        let rows = layout.rows_for(elements.len() as u64 / SCALAR_ELEMENT);
848
849        assert_eq!(rows, 2);
850        assert_eq!(scratch.len() as u64, u64::from(rows) * layout.row_bytes());
851        assert!(u64::from(rows) < u64::from(layout.height));
852    }
853
854    #[test]
855    fn packing_a_full_row_writes_exactly_one_row() {
856        let layout = TexelArrayLayout::new(SCALAR_ELEMENT, 128, ROOMY);
857
858        let mut out = Vec::new();
859        layout.pack(&vec![7; 64 * SCALAR_ELEMENT as usize], &mut out);
860
861        assert_eq!(out.len() as u64, layout.row_bytes());
862        assert_eq!(out.len(), 256);
863    }
864
865    #[test]
866    fn packing_more_than_one_row_spans_the_rows_it_needs() {
867        let layout = TexelArrayLayout::new(SCALAR_ELEMENT, 128, ROOMY);
868
869        let mut out = Vec::new();
870        layout.pack(&vec![7; 65 * SCALAR_ELEMENT as usize], &mut out);
871
872        assert_eq!(out.len(), 512, "the 65th element starts a second row");
873    }
874
875    #[test]
876    fn packing_again_shorter_clears_the_previous_tail() {
877        let layout = TexelArrayLayout::new(SCALAR_ELEMENT, 128, ROOMY);
878        let mut out = Vec::new();
879        layout.pack(&[0xff; 64 * SCALAR_ELEMENT as usize], &mut out);
880
881        layout.pack(&[1; 2 * SCALAR_ELEMENT as usize], &mut out);
882
883        assert!(
884            out[2 * SCALAR_ELEMENT as usize..]
885                .iter()
886                .all(|byte| *byte == 0),
887            "an element the shorter upload does not reach is cleared"
888        );
889    }
890
891    #[test]
892    fn packing_reuses_the_same_allocation() {
893        let layout = TexelArrayLayout::new(SCALAR_ELEMENT, 16, ROOMY);
894        let mut out = Vec::new();
895        layout.pack(&[1; 16], &mut out);
896        let capacity = out.capacity();
897
898        layout.pack(&[2; 16], &mut out);
899
900        assert_eq!(out.capacity(), capacity, "the scratch buffer is reused");
901    }
902
903    #[test]
904    #[should_panic(expected = "not a whole number")]
905    fn packing_a_partial_element_panics() {
906        let layout = TexelArrayLayout::new(SCALAR_ELEMENT, 16, ROOMY);
907        layout.pack(&[0; 6], &mut Vec::new());
908    }
909
910    #[test]
911    #[should_panic(expected = "room for")]
912    fn packing_more_than_capacity_panics() {
913        let layout = TexelArrayLayout::new(SCALAR_ELEMENT, 4, ROOMY);
914        let over = (layout.capacity() + 1) * SCALAR_ELEMENT;
915        layout.pack(&vec![0; over as usize], &mut Vec::new());
916    }
917
918    #[test]
919    fn an_array_creates_a_texture_a_bind_group_can_bind() {
920        let (device, _queue) = noop_device();
921        let array = TexelArray::new(&device, Some("test::texel_array"), MATRIX_ELEMENT, 4, ROOMY);
922
923        let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
924            label: Some("test::texel_array"),
925            entries: &[TexelArrayLayout::binding(0, wgpu::ShaderStages::VERTEX)],
926        });
927        let _bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
928            label: Some("test::texel_array"),
929            layout: &layout,
930            entries: &[wgpu::BindGroupEntry {
931                binding: 0,
932                resource: array.binding_resource(),
933            }],
934        });
935    }
936
937    #[test]
938    fn an_upload_records_a_copy_the_device_accepts() {
939        let (device, _queue) = noop_device();
940        let mut array =
941            TexelArray::new(&device, Some("test::texel_array"), MATRIX_ELEMENT, 4, ROOMY);
942        let mut encoder = device.create_command_encoder(&Default::default());
943
944        array.upload(&device, &mut encoder, &[0; 2 * MATRIX_ELEMENT as usize]);
945
946        let _ = encoder.finish();
947    }
948
949    #[test]
950    fn an_upload_of_an_empty_array_records_a_copy_too() {
951        let (device, _queue) = noop_device();
952        let mut array =
953            TexelArray::new(&device, Some("test::texel_array"), MATRIX_ELEMENT, 4, ROOMY);
954        let mut encoder = device.create_command_encoder(&Default::default());
955
956        array.upload(&device, &mut encoder, &[]);
957
958        let _ = encoder.finish();
959    }
960
961    #[test]
962    fn an_array_holds_a_buffer_or_a_texture_by_its_limit() {
963        let (device, _queue) = noop_device();
964
965        let buffered = Array::new(&device, Some("test"), SCALAR_ELEMENT, 4, None);
966        assert!(!buffered.is_texel());
967        assert_eq!(buffered.capacity(), 4);
968        assert!(matches!(buffered.handle(), ArrayHandle::Buffer(_)));
969
970        let texel = Array::new(&device, Some("test"), SCALAR_ELEMENT, 4, Some(ROOMY));
971        assert!(texel.is_texel());
972        assert_eq!(texel.capacity(), 64, "a row of 64 holds the four elements");
973        assert!(matches!(texel.handle(), ArrayHandle::Texel(_)));
974    }
975
976    #[test]
977    fn an_array_uploads_the_same_bytes_on_either_path() {
978        let (device, _queue) = noop_device();
979        let elements = [1u8; 2 * SCALAR_ELEMENT as usize];
980
981        let mut buffered = Array::new(&device, Some("test"), SCALAR_ELEMENT, 4, None);
982        let mut texel = Array::new(&device, Some("test"), SCALAR_ELEMENT, 4, Some(ROOMY));
983        let mut encoder = device.create_command_encoder(&Default::default());
984        buffered.upload(&device, &mut encoder, &elements);
985        texel.upload(&device, &mut encoder, &elements);
986        let _ = encoder.finish();
987    }
988
989    #[test]
990    fn growing_an_array_replaces_its_resource_only_when_it_is_too_small() {
991        let (device, _queue) = noop_device();
992        let mut array = Array::new(&device, Some("test"), SCALAR_ELEMENT, 4, None);
993
994        assert!(
995            !array.grow_to(&device, Some("test"), 4),
996            "already has the room"
997        );
998        assert!(
999            array.grow_to(&device, Some("test"), 8),
1000            "the array outgrew it"
1001        );
1002        assert_eq!(array.capacity(), 8);
1003    }
1004
1005    #[test]
1006    fn growing_a_texel_array_stays_on_the_texel_path() {
1007        let (device, _queue) = noop_device();
1008        let mut array = Array::new(&device, Some("test"), MATRIX_ELEMENT, 4, Some(ROOMY));
1009        assert!(array.is_texel());
1010
1011        assert!(array.grow_to(&device, Some("test"), 64));
1012
1013        assert!(
1014            array.is_texel(),
1015            "a grown array keeps the resource it can read"
1016        );
1017        assert_eq!(array.capacity(), 64);
1018    }
1019
1020    #[test]
1021    fn a_layout_entry_follows_the_resource_the_array_holds() {
1022        let (device, _queue) = noop_device();
1023        let buffered = Array::new(&device, Some("test"), MATRIX_ELEMENT, 4, None);
1024        let texel = Array::new(&device, Some("test"), MATRIX_ELEMENT, 4, Some(ROOMY));
1025
1026        let buffer_entry =
1027            buffered
1028                .handle()
1029                .layout_entry(2, wgpu::ShaderStages::VERTEX, MATRIX_ELEMENT);
1030        assert!(matches!(buffer_entry.ty, wgpu::BindingType::Buffer { .. }));
1031
1032        let texel_entry =
1033            texel
1034                .handle()
1035                .layout_entry(2, wgpu::ShaderStages::VERTEX, MATRIX_ELEMENT);
1036        assert!(matches!(texel_entry.ty, wgpu::BindingType::Texture { .. }));
1037    }
1038
1039    #[test]
1040    fn a_bind_group_accepts_an_array_of_either_path() {
1041        let (device, _queue) = noop_device();
1042        for max_dimension in [None, Some(ROOMY)] {
1043            let array = Array::new(&device, Some("test"), MATRIX_ELEMENT, 4, max_dimension);
1044            let entry = array
1045                .handle()
1046                .layout_entry(0, wgpu::ShaderStages::VERTEX, MATRIX_ELEMENT);
1047            let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1048                label: Some("test::array"),
1049                entries: &[entry],
1050            });
1051            let _bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1052                label: Some("test::array"),
1053                layout: &layout,
1054                entries: &[wgpu::BindGroupEntry {
1055                    binding: 0,
1056                    resource: array.handle().binding_resource(),
1057                }],
1058            });
1059        }
1060    }
1061}