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).unwrap();
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        if s == 0 {
207            0
208        } else {
209            self.data.len() / s
210        }
211    }
212
213    /// Total size of the raw byte data.
214    #[must_use]
215    pub fn byte_len(&self) -> usize {
216        self.data.len()
217    }
218
219    /// Raw byte slice for the entire buffer.
220    #[must_use]
221    pub fn as_bytes(&self) -> &[u8] {
222        &self.data
223    }
224
225    /// Return the layout describing this buffer's format.
226    #[must_use]
227    pub fn layout(&self) -> &VertexLayout {
228        &self.layout
229    }
230
231    /// Return the raw bytes for vertex at `index`, or `None` if out of range.
232    #[must_use]
233    pub fn vertex_bytes(&self, index: usize) -> Option<&[u8]> {
234        let s = self.stride();
235        let start = index * s;
236        let end = start + s;
237        self.data.get(start..end)
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn attribute_float32_size() {
247        assert_eq!(VertexAttribute::Float32.byte_size(), 4);
248    }
249
250    #[test]
251    fn attribute_float32x3_size() {
252        assert_eq!(VertexAttribute::Float32x3.byte_size(), 12);
253    }
254
255    #[test]
256    fn attribute_unorm8x4_size() {
257        assert_eq!(VertexAttribute::Unorm8x4.byte_size(), 4);
258    }
259
260    #[test]
261    fn attribute_component_counts() {
262        assert_eq!(VertexAttribute::Float32.component_count(), 1);
263        assert_eq!(VertexAttribute::Float32x2.component_count(), 2);
264        assert_eq!(VertexAttribute::Float32x3.component_count(), 3);
265        assert_eq!(VertexAttribute::Float32x4.component_count(), 4);
266    }
267
268    #[test]
269    fn layout_stride_single_attr() {
270        let mut l = VertexLayout::new();
271        l.add("POS", VertexAttribute::Float32x3);
272        assert_eq!(l.stride(), 12);
273    }
274
275    #[test]
276    fn layout_stride_multiple_attrs() {
277        let mut l = VertexLayout::new();
278        l.add("POS", VertexAttribute::Float32x3);
279        l.add("UV", VertexAttribute::Float32x2);
280        assert_eq!(l.stride(), 20);
281    }
282
283    #[test]
284    fn layout_offsets_are_cumulative() {
285        let mut l = VertexLayout::new();
286        l.add("POS", VertexAttribute::Float32x3);
287        l.add("UV", VertexAttribute::Float32x2);
288        assert_eq!(l.slots()[0].offset, 0);
289        assert_eq!(l.slots()[1].offset, 12);
290    }
291
292    #[test]
293    fn layout_slot_by_name() {
294        let mut l = VertexLayout::new();
295        l.add("NORMAL", VertexAttribute::Float32x3);
296        assert!(l.slot_by_name("NORMAL").is_some());
297        assert!(l.slot_by_name("UV").is_none());
298    }
299
300    #[test]
301    fn vertex_buffer_create_ok() {
302        let mut l = VertexLayout::new();
303        l.add("POS", VertexAttribute::Float32x3);
304        let vb = VertexBuffer::new(l, vec![0u8; 24]).unwrap();
305        assert_eq!(vb.vertex_count(), 2);
306    }
307
308    #[test]
309    fn vertex_buffer_stride_mismatch_error() {
310        let mut l = VertexLayout::new();
311        l.add("POS", VertexAttribute::Float32x3);
312        let err = VertexBuffer::new(l, vec![0u8; 13]).unwrap_err();
313        matches!(err, VertexBufferError::StrideMismatch { .. });
314    }
315
316    #[test]
317    fn vertex_buffer_empty_layout_error() {
318        let l = VertexLayout::new();
319        let err = VertexBuffer::new(l, vec![]).unwrap_err();
320        assert_eq!(err, VertexBufferError::Empty);
321    }
322
323    #[test]
324    fn vertex_buffer_vertex_bytes_valid() {
325        let mut l = VertexLayout::new();
326        l.add("POS", VertexAttribute::Uint32);
327        let data: Vec<u8> = (0u8..8).collect();
328        let vb = VertexBuffer::new(l, data.clone()).unwrap();
329        assert_eq!(vb.vertex_bytes(0), Some(&data[0..4]));
330        assert_eq!(vb.vertex_bytes(1), Some(&data[4..8]));
331        assert!(vb.vertex_bytes(2).is_none());
332    }
333
334    #[test]
335    fn vertex_buffer_byte_len() {
336        let mut l = VertexLayout::new();
337        l.add("POS", VertexAttribute::Float32x4);
338        let vb = VertexBuffer::new(l, vec![0u8; 32]).unwrap();
339        assert_eq!(vb.byte_len(), 32);
340    }
341}