Skip to main content

oximedia_gpu/
vertex_buffer.rs

1//! Vertex buffer layout descriptions and management.
2//!
3//! Defines vertex attribute formats, stride calculations, and an in-memory
4//! vertex buffer abstraction for use with GPU render pipelines.
5
6#![allow(dead_code)]
7
8/// The data format and dimensionality of a single vertex attribute.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum VertexAttribute {
11    /// Single `f32` component (e.g. a scalar weight).
12    Float32,
13    /// Two `f32` components (e.g. UV texture coordinates).
14    Float32x2,
15    /// Three `f32` components (e.g. position XYZ or normal).
16    Float32x3,
17    /// Four `f32` components (e.g. RGBA colour or XYZW position).
18    Float32x4,
19    /// Single `u32` (e.g. material index).
20    Uint32,
21    /// Two `u32` components.
22    Uint32x2,
23    /// Single `i32`.
24    Sint32,
25    /// Four `u8` normalised to [0, 1] (e.g. packed colours).
26    Unorm8x4,
27}
28
29impl VertexAttribute {
30    /// Size of this attribute in bytes.
31    #[must_use]
32    pub const fn byte_size(self) -> usize {
33        match self {
34            Self::Float32 => 4,
35            Self::Float32x2 => 8,
36            Self::Float32x3 => 12,
37            Self::Float32x4 => 16,
38            Self::Uint32 => 4,
39            Self::Uint32x2 => 8,
40            Self::Sint32 => 4,
41            Self::Unorm8x4 => 4,
42        }
43    }
44
45    /// Number of scalar components in this attribute.
46    #[must_use]
47    pub const fn component_count(self) -> usize {
48        match self {
49            Self::Float32 | Self::Uint32 | Self::Sint32 => 1,
50            Self::Float32x2 | Self::Uint32x2 => 2,
51            Self::Float32x3 => 3,
52            Self::Float32x4 | Self::Unorm8x4 => 4,
53        }
54    }
55}
56
57/// A named slot in a vertex layout, pairing a semantic name with its format.
58#[derive(Debug, Clone)]
59pub struct VertexSlot {
60    /// Semantic name used in shaders (e.g. `"POSITION"`, `"TEXCOORD"`).
61    pub name: String,
62    /// Data format of this slot.
63    pub attribute: VertexAttribute,
64    /// Byte offset from the start of the vertex record.
65    pub offset: usize,
66}
67
68/// Describes the memory layout of a single interleaved vertex record.
69///
70/// Attributes are stored in insertion order; the stride is computed
71/// automatically from the sum of all attribute sizes.
72#[derive(Debug, Clone, Default)]
73pub struct VertexLayout {
74    slots: Vec<VertexSlot>,
75}
76
77impl VertexLayout {
78    /// Create an empty layout.
79    #[must_use]
80    pub fn new() -> Self {
81        Self::default()
82    }
83
84    /// Append a named attribute to the layout.
85    ///
86    /// Returns `&mut self` for builder-style chaining.
87    pub fn add(&mut self, name: impl Into<String>, attribute: VertexAttribute) -> &mut Self {
88        let offset = self.stride();
89        self.slots.push(VertexSlot {
90            name: name.into(),
91            attribute,
92            offset,
93        });
94        self
95    }
96
97    /// Total byte size of one vertex record (sum of all attribute sizes).
98    #[must_use]
99    pub fn stride(&self) -> usize {
100        self.slots.iter().map(|s| s.attribute.byte_size()).sum()
101    }
102
103    /// Number of attributes in the layout.
104    #[must_use]
105    pub fn attribute_count(&self) -> usize {
106        self.slots.len()
107    }
108
109    /// Iterate over the slots in declaration order.
110    #[must_use]
111    pub fn slots(&self) -> &[VertexSlot] {
112        &self.slots
113    }
114
115    /// Return the slot with `name`, if present.
116    #[must_use]
117    pub fn slot_by_name(&self, name: &str) -> Option<&VertexSlot> {
118        self.slots.iter().find(|s| s.name == name)
119    }
120}
121
122/// Errors that can occur when working with a [`VertexBuffer`].
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum VertexBufferError {
125    /// The raw byte data length is not a multiple of the layout stride.
126    StrideMismatch {
127        /// Stride implied by the layout.
128        stride: usize,
129        /// Actual byte length of the data.
130        data_len: usize,
131    },
132    /// The buffer is empty and no vertices could be retrieved.
133    Empty,
134}
135
136impl std::fmt::Display for VertexBufferError {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        match self {
139            Self::StrideMismatch { stride, data_len } => write!(
140                f,
141                "data length {data_len} is not a multiple of stride {stride}"
142            ),
143            Self::Empty => write!(f, "vertex buffer is empty"),
144        }
145    }
146}
147
148impl std::error::Error for VertexBufferError {}
149
150/// An in-memory buffer of interleaved vertex data with an associated layout.
151///
152/// # Example
153///
154/// ```
155/// use oximedia_gpu::vertex_buffer::{VertexAttribute, VertexLayout, VertexBuffer};
156///
157/// let mut layout = VertexLayout::new();
158/// layout.add("POSITION", VertexAttribute::Float32x3);
159/// layout.add("TEXCOORD", VertexAttribute::Float32x2);
160///
161/// // 1 vertex = 20 bytes
162/// let data = vec![0u8; 20];
163/// let vb = VertexBuffer::new(layout, data).expect("valid vertex buffer");
164/// assert_eq!(vb.vertex_count(), 1);
165/// assert_eq!(vb.stride(), 20);
166/// ```
167#[derive(Debug, Clone)]
168pub struct VertexBuffer {
169    layout: VertexLayout,
170    data: Vec<u8>,
171}
172
173impl VertexBuffer {
174    /// Create a new vertex buffer, validating that `data.len()` is a multiple
175    /// of the layout stride.
176    ///
177    /// # Errors
178    ///
179    /// Returns [`VertexBufferError::StrideMismatch`] if the data is not aligned
180    /// to the layout stride, or [`VertexBufferError::Empty`] if the stride is
181    /// zero.
182    pub fn new(layout: VertexLayout, data: Vec<u8>) -> Result<Self, VertexBufferError> {
183        let stride = layout.stride();
184        if stride == 0 {
185            return Err(VertexBufferError::Empty);
186        }
187        if data.len() % stride != 0 {
188            return Err(VertexBufferError::StrideMismatch {
189                stride,
190                data_len: data.len(),
191            });
192        }
193        Ok(Self { layout, data })
194    }
195
196    /// Byte stride between consecutive vertex records.
197    #[must_use]
198    pub fn stride(&self) -> usize {
199        self.layout.stride()
200    }
201
202    /// Number of complete vertex records stored in the buffer.
203    #[must_use]
204    pub fn vertex_count(&self) -> usize {
205        let s = self.stride();
206        self.data.len().checked_div(s).unwrap_or(0)
207    }
208
209    /// Total size of the raw byte data.
210    #[must_use]
211    pub fn byte_len(&self) -> usize {
212        self.data.len()
213    }
214
215    /// Raw byte slice for the entire buffer.
216    #[must_use]
217    pub fn as_bytes(&self) -> &[u8] {
218        &self.data
219    }
220
221    /// Return the layout describing this buffer's format.
222    #[must_use]
223    pub fn layout(&self) -> &VertexLayout {
224        &self.layout
225    }
226
227    /// Return the raw bytes for vertex at `index`, or `None` if out of range.
228    #[must_use]
229    pub fn vertex_bytes(&self, index: usize) -> Option<&[u8]> {
230        let s = self.stride();
231        let start = index * s;
232        let end = start + s;
233        self.data.get(start..end)
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn attribute_float32_size() {
243        assert_eq!(VertexAttribute::Float32.byte_size(), 4);
244    }
245
246    #[test]
247    fn attribute_float32x3_size() {
248        assert_eq!(VertexAttribute::Float32x3.byte_size(), 12);
249    }
250
251    #[test]
252    fn attribute_unorm8x4_size() {
253        assert_eq!(VertexAttribute::Unorm8x4.byte_size(), 4);
254    }
255
256    #[test]
257    fn attribute_component_counts() {
258        assert_eq!(VertexAttribute::Float32.component_count(), 1);
259        assert_eq!(VertexAttribute::Float32x2.component_count(), 2);
260        assert_eq!(VertexAttribute::Float32x3.component_count(), 3);
261        assert_eq!(VertexAttribute::Float32x4.component_count(), 4);
262    }
263
264    #[test]
265    fn layout_stride_single_attr() {
266        let mut l = VertexLayout::new();
267        l.add("POS", VertexAttribute::Float32x3);
268        assert_eq!(l.stride(), 12);
269    }
270
271    #[test]
272    fn layout_stride_multiple_attrs() {
273        let mut l = VertexLayout::new();
274        l.add("POS", VertexAttribute::Float32x3);
275        l.add("UV", VertexAttribute::Float32x2);
276        assert_eq!(l.stride(), 20);
277    }
278
279    #[test]
280    fn layout_offsets_are_cumulative() {
281        let mut l = VertexLayout::new();
282        l.add("POS", VertexAttribute::Float32x3);
283        l.add("UV", VertexAttribute::Float32x2);
284        assert_eq!(l.slots()[0].offset, 0);
285        assert_eq!(l.slots()[1].offset, 12);
286    }
287
288    #[test]
289    fn layout_slot_by_name() {
290        let mut l = VertexLayout::new();
291        l.add("NORMAL", VertexAttribute::Float32x3);
292        assert!(l.slot_by_name("NORMAL").is_some());
293        assert!(l.slot_by_name("UV").is_none());
294    }
295
296    #[test]
297    fn vertex_buffer_create_ok() {
298        let mut l = VertexLayout::new();
299        l.add("POS", VertexAttribute::Float32x3);
300        let vb =
301            VertexBuffer::new(l, vec![0u8; 24]).expect("vertex buffer creation should succeed");
302        assert_eq!(vb.vertex_count(), 2);
303    }
304
305    #[test]
306    fn vertex_buffer_stride_mismatch_error() {
307        let mut l = VertexLayout::new();
308        l.add("POS", VertexAttribute::Float32x3);
309        let err = VertexBuffer::new(l, vec![0u8; 13]).unwrap_err();
310        matches!(err, VertexBufferError::StrideMismatch { .. });
311    }
312
313    #[test]
314    fn vertex_buffer_empty_layout_error() {
315        let l = VertexLayout::new();
316        let err = VertexBuffer::new(l, vec![]).unwrap_err();
317        assert_eq!(err, VertexBufferError::Empty);
318    }
319
320    #[test]
321    fn vertex_buffer_vertex_bytes_valid() {
322        let mut l = VertexLayout::new();
323        l.add("POS", VertexAttribute::Uint32);
324        let data: Vec<u8> = (0u8..8).collect();
325        let vb = VertexBuffer::new(l, data.clone()).expect("vertex buffer creation should succeed");
326        assert_eq!(vb.vertex_bytes(0), Some(&data[0..4]));
327        assert_eq!(vb.vertex_bytes(1), Some(&data[4..8]));
328        assert!(vb.vertex_bytes(2).is_none());
329    }
330
331    #[test]
332    fn vertex_buffer_byte_len() {
333        let mut l = VertexLayout::new();
334        l.add("POS", VertexAttribute::Float32x4);
335        let vb =
336            VertexBuffer::new(l, vec![0u8; 32]).expect("vertex buffer creation should succeed");
337        assert_eq!(vb.byte_len(), 32);
338    }
339}