Skip to main content

astrelis_ui/
gpu_types.rs

1//! GPU data types for instance-based rendering.
2//!
3//! This module defines the vertex and instance data structures used for
4//! efficient GPU rendering of UI elements. All types are Pod-compatible
5//! for direct GPU upload.
6
7use astrelis_core::math::Vec2;
8use astrelis_render::{Color, wgpu};
9use bytemuck::{Pod, Zeroable};
10
11/// Instance data for quad rendering.
12///
13/// Used for drawing rectangles, rounded rectangles, and borders.
14/// Each instance represents one quad that will be drawn using
15/// instanced rendering with a shared vertex buffer.
16#[repr(C)]
17#[derive(Copy, Clone, Debug, Pod, Zeroable)]
18pub struct QuadInstance {
19    /// Position in screen space (top-left corner)
20    pub position: [f32; 2],
21    /// Size of the quad (width, height)
22    pub size: [f32; 2],
23    /// Color (RGBA)
24    pub color: [f32; 4],
25    /// Border radius for rounded corners (0 = sharp corners)
26    pub border_radius: f32,
27    /// Border thickness (0 = filled quad, >0 = border outline)
28    pub border_thickness: f32,
29    /// Depth value for z-ordering (0.0 = far, 1.0 = near)
30    pub z_depth: f32,
31    /// Padding to align to 16-byte boundary for optimal GPU performance
32    pub _padding: f32,
33}
34
35impl QuadInstance {
36    /// Create a filled quad instance.
37    pub fn filled(position: Vec2, size: Vec2, color: Color, z_depth: f32) -> Self {
38        Self {
39            position: position.into(),
40            size: size.into(),
41            color: color.into(),
42            border_radius: 0.0,
43            border_thickness: 0.0,
44            z_depth,
45            _padding: 0.0,
46        }
47    }
48
49    /// Create a rounded filled quad instance.
50    pub fn rounded(
51        position: Vec2,
52        size: Vec2,
53        color: Color,
54        border_radius: f32,
55        z_depth: f32,
56    ) -> Self {
57        Self {
58            position: position.into(),
59            size: size.into(),
60            color: color.into(),
61            border_radius,
62            border_thickness: 0.0,
63            z_depth,
64            _padding: 0.0,
65        }
66    }
67
68    /// Create a bordered quad instance.
69    pub fn bordered(
70        position: Vec2,
71        size: Vec2,
72        color: Color,
73        border_thickness: f32,
74        border_radius: f32,
75        z_depth: f32,
76    ) -> Self {
77        Self {
78            position: position.into(),
79            size: size.into(),
80            color: color.into(),
81            border_radius,
82            border_thickness,
83            z_depth,
84            _padding: 0.0,
85        }
86    }
87
88    /// Get the WGPU vertex buffer layout for quad instances.
89    pub fn vertex_layout() -> wgpu::VertexBufferLayout<'static> {
90        use wgpu::*;
91        VertexBufferLayout {
92            array_stride: std::mem::size_of::<Self>() as u64,
93            step_mode: VertexStepMode::Instance,
94            attributes: &[
95                // position
96                VertexAttribute {
97                    offset: 0,
98                    shader_location: 2,
99                    format: VertexFormat::Float32x2,
100                },
101                // size
102                VertexAttribute {
103                    offset: 8,
104                    shader_location: 3,
105                    format: VertexFormat::Float32x2,
106                },
107                // color
108                VertexAttribute {
109                    offset: 16,
110                    shader_location: 4,
111                    format: VertexFormat::Float32x4,
112                },
113                // border_radius
114                VertexAttribute {
115                    offset: 32,
116                    shader_location: 5,
117                    format: VertexFormat::Float32,
118                },
119                // border_thickness
120                VertexAttribute {
121                    offset: 36,
122                    shader_location: 6,
123                    format: VertexFormat::Float32,
124                },
125                // z_depth
126                VertexAttribute {
127                    offset: 40,
128                    shader_location: 7,
129                    format: VertexFormat::Float32,
130                },
131            ],
132        }
133    }
134}
135
136/// Instance data for text glyph rendering.
137///
138/// Each instance represents one glyph to be drawn from the font atlas.
139/// Text is rendered as individual glyph instances for maximum flexibility.
140///
141/// ## Coordinate System
142///
143/// Positions use a top-left origin coordinate system where (0, 0) is the top-left
144/// corner and Y increases downward, consistent with UI layout conventions.
145#[repr(C)]
146#[derive(Copy, Clone, Debug, Pod, Zeroable)]
147pub struct TextInstance {
148    /// Position in screen space (top-left corner of glyph bounding box)
149    pub position: [f32; 2],
150    /// Size of the glyph quad in screen space
151    pub size: [f32; 2],
152    /// Atlas UV coordinates (top-left)
153    pub atlas_uv_min: [f32; 2],
154    /// Atlas UV coordinates (bottom-right)
155    pub atlas_uv_max: [f32; 2],
156    /// Color (RGBA)
157    pub color: [f32; 4],
158    /// Depth value for z-ordering (0.0 = far, 1.0 = near)
159    pub z_depth: f32,
160    /// Padding to align to 16-byte boundary (64 bytes total)
161    pub _padding: [f32; 3],
162}
163
164impl TextInstance {
165    /// Create a new text instance.
166    pub fn new(
167        position: Vec2,
168        size: Vec2,
169        atlas_uv_min: [f32; 2],
170        atlas_uv_max: [f32; 2],
171        color: Color,
172        z_depth: f32,
173    ) -> Self {
174        Self {
175            position: position.into(),
176            size: size.into(),
177            atlas_uv_min,
178            atlas_uv_max,
179            color: color.into(),
180            z_depth,
181            _padding: [0.0; 3],
182        }
183    }
184
185    /// Get the WGPU vertex buffer layout for text instances.
186    pub fn vertex_layout() -> wgpu::VertexBufferLayout<'static> {
187        use wgpu::*;
188        VertexBufferLayout {
189            array_stride: std::mem::size_of::<Self>() as u64,
190            step_mode: VertexStepMode::Instance,
191            attributes: &[
192                // position
193                VertexAttribute {
194                    offset: 0,
195                    shader_location: 2,
196                    format: VertexFormat::Float32x2,
197                },
198                // size
199                VertexAttribute {
200                    offset: 8,
201                    shader_location: 3,
202                    format: VertexFormat::Float32x2,
203                },
204                // atlas_uv_min
205                VertexAttribute {
206                    offset: 16,
207                    shader_location: 4,
208                    format: VertexFormat::Float32x2,
209                },
210                // atlas_uv_max
211                VertexAttribute {
212                    offset: 24,
213                    shader_location: 5,
214                    format: VertexFormat::Float32x2,
215                },
216                // color
217                VertexAttribute {
218                    offset: 32,
219                    shader_location: 6,
220                    format: VertexFormat::Float32x4,
221                },
222                // z_depth
223                VertexAttribute {
224                    offset: 48,
225                    shader_location: 7,
226                    format: VertexFormat::Float32,
227                },
228            ],
229        }
230    }
231}
232
233/// Instance data for image rendering.
234///
235/// Each instance represents one image quad to be drawn from a texture.
236/// Supports UV coordinates for sprite sheets and tinting.
237#[repr(C)]
238#[derive(Copy, Clone, Debug, Pod, Zeroable)]
239pub struct ImageInstance {
240    /// Position in screen space (top-left corner)
241    pub position: [f32; 2],
242    /// Size of the image quad in screen space
243    pub size: [f32; 2],
244    /// UV coordinates (top-left)
245    pub uv_min: [f32; 2],
246    /// UV coordinates (bottom-right)
247    pub uv_max: [f32; 2],
248    /// Tint color (RGBA) - multiplied with texture color
249    pub tint: [f32; 4],
250    /// Border radius for rounded corners (0 = sharp corners)
251    pub border_radius: f32,
252    /// Texture index (for texture arrays, 0 for single texture)
253    pub texture_index: u32,
254    /// Depth value for z-ordering (0.0 = far, 1.0 = near)
255    pub z_depth: f32,
256    /// Padding to align to 16-byte boundary
257    pub _padding: f32,
258}
259
260impl ImageInstance {
261    /// Create a new image instance covering the full texture.
262    pub fn new(position: Vec2, size: Vec2, z_depth: f32) -> Self {
263        Self {
264            position: position.into(),
265            size: size.into(),
266            uv_min: [0.0, 0.0],
267            uv_max: [1.0, 1.0],
268            tint: [1.0, 1.0, 1.0, 1.0],
269            border_radius: 0.0,
270            texture_index: 0,
271            z_depth,
272            _padding: 0.0,
273        }
274    }
275
276    /// Create an image instance with specific UV coordinates (for sprites).
277    pub fn with_uv(
278        position: Vec2,
279        size: Vec2,
280        uv_min: [f32; 2],
281        uv_max: [f32; 2],
282        z_depth: f32,
283    ) -> Self {
284        Self {
285            position: position.into(),
286            size: size.into(),
287            uv_min,
288            uv_max,
289            tint: [1.0, 1.0, 1.0, 1.0],
290            border_radius: 0.0,
291            texture_index: 0,
292            z_depth,
293            _padding: 0.0,
294        }
295    }
296
297    /// Create an image instance with a tint color.
298    pub fn with_tint(position: Vec2, size: Vec2, tint: Color, z_depth: f32) -> Self {
299        Self {
300            position: position.into(),
301            size: size.into(),
302            uv_min: [0.0, 0.0],
303            uv_max: [1.0, 1.0],
304            tint: tint.into(),
305            border_radius: 0.0,
306            texture_index: 0,
307            z_depth,
308            _padding: 0.0,
309        }
310    }
311
312    /// Set the tint color.
313    pub fn tint(mut self, color: Color) -> Self {
314        self.tint = color.into();
315        self
316    }
317
318    /// Set the border radius for rounded corners.
319    pub fn border_radius(mut self, radius: f32) -> Self {
320        self.border_radius = radius;
321        self
322    }
323
324    /// Set the texture index (for texture arrays).
325    pub fn texture_index(mut self, index: u32) -> Self {
326        self.texture_index = index;
327        self
328    }
329
330    /// Set the z depth for depth ordering.
331    pub fn z_depth(mut self, z_depth: f32) -> Self {
332        self.z_depth = z_depth;
333        self
334    }
335
336    /// Get the WGPU vertex buffer layout for image instances.
337    pub fn vertex_layout() -> wgpu::VertexBufferLayout<'static> {
338        use wgpu::*;
339        VertexBufferLayout {
340            array_stride: std::mem::size_of::<Self>() as u64,
341            step_mode: VertexStepMode::Instance,
342            attributes: &[
343                // position
344                VertexAttribute {
345                    offset: 0,
346                    shader_location: 2,
347                    format: VertexFormat::Float32x2,
348                },
349                // size
350                VertexAttribute {
351                    offset: 8,
352                    shader_location: 3,
353                    format: VertexFormat::Float32x2,
354                },
355                // uv_min
356                VertexAttribute {
357                    offset: 16,
358                    shader_location: 4,
359                    format: VertexFormat::Float32x2,
360                },
361                // uv_max
362                VertexAttribute {
363                    offset: 24,
364                    shader_location: 5,
365                    format: VertexFormat::Float32x2,
366                },
367                // tint
368                VertexAttribute {
369                    offset: 32,
370                    shader_location: 6,
371                    format: VertexFormat::Float32x4,
372                },
373                // border_radius
374                VertexAttribute {
375                    offset: 48,
376                    shader_location: 7,
377                    format: VertexFormat::Float32,
378                },
379                // texture_index
380                VertexAttribute {
381                    offset: 52,
382                    shader_location: 8,
383                    format: VertexFormat::Uint32,
384                },
385                // z_depth
386                VertexAttribute {
387                    offset: 56,
388                    shader_location: 9,
389                    format: VertexFormat::Float32,
390                },
391            ],
392        }
393    }
394}
395
396/// Vertex data for a unit quad (0,0 to 1,1).
397///
398/// Used as the base geometry for all quad instances.
399/// Instanced rendering will scale and position this quad.
400#[repr(C)]
401#[derive(Copy, Clone, Debug, Pod, Zeroable)]
402pub struct QuadVertex {
403    /// Position in normalized quad space (0-1)
404    pub position: [f32; 2],
405    /// UV coordinates for texturing/effects
406    pub uv: [f32; 2],
407}
408
409impl QuadVertex {
410    /// Create a new quad vertex.
411    pub const fn new(position: [f32; 2], uv: [f32; 2]) -> Self {
412        Self { position, uv }
413    }
414
415    /// Get the 6 vertices for a unit quad (two triangles).
416    pub const fn unit_quad() -> [QuadVertex; 6] {
417        [
418            // First triangle
419            QuadVertex::new([0.0, 0.0], [0.0, 0.0]),
420            QuadVertex::new([1.0, 0.0], [1.0, 0.0]),
421            QuadVertex::new([1.0, 1.0], [1.0, 1.0]),
422            // Second triangle
423            QuadVertex::new([0.0, 0.0], [0.0, 0.0]),
424            QuadVertex::new([1.0, 1.0], [1.0, 1.0]),
425            QuadVertex::new([0.0, 1.0], [0.0, 1.0]),
426        ]
427    }
428
429    /// Get the WGPU vertex buffer layout for quad vertices.
430    pub fn vertex_layout() -> wgpu::VertexBufferLayout<'static> {
431        use wgpu::*;
432        VertexBufferLayout {
433            array_stride: std::mem::size_of::<Self>() as u64,
434            step_mode: VertexStepMode::Vertex,
435            attributes: &[
436                // position
437                VertexAttribute {
438                    offset: 0,
439                    shader_location: 0,
440                    format: VertexFormat::Float32x2,
441                },
442                // uv
443                VertexAttribute {
444                    offset: 8,
445                    shader_location: 1,
446                    format: VertexFormat::Float32x2,
447                },
448            ],
449        }
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456
457    #[test]
458    fn test_quad_instance_size() {
459        // Should be aligned to 16 bytes for optimal GPU performance
460        let size = std::mem::size_of::<QuadInstance>();
461        assert_eq!(size % 16, 0, "QuadInstance should be 16-byte aligned");
462    }
463
464    #[test]
465    fn test_text_instance_size() {
466        let size = std::mem::size_of::<TextInstance>();
467        assert_eq!(size, 64, "TextInstance should be 64 bytes");
468        assert_eq!(size % 16, 0, "TextInstance should be 16-byte aligned");
469    }
470
471    #[test]
472    fn test_quad_vertex_size() {
473        let size = std::mem::size_of::<QuadVertex>();
474        assert_eq!(size, 16, "QuadVertex should be 16 bytes");
475    }
476
477    #[test]
478    fn test_unit_quad_vertices() {
479        let vertices = QuadVertex::unit_quad();
480        assert_eq!(vertices.len(), 6);
481
482        // Check first triangle
483        assert_eq!(vertices[0].position, [0.0, 0.0]);
484        assert_eq!(vertices[1].position, [1.0, 0.0]);
485        assert_eq!(vertices[2].position, [1.0, 1.0]);
486
487        // Check second triangle
488        assert_eq!(vertices[3].position, [0.0, 0.0]);
489        assert_eq!(vertices[4].position, [1.0, 1.0]);
490        assert_eq!(vertices[5].position, [0.0, 1.0]);
491    }
492
493    #[test]
494    fn test_quad_instance_creation() {
495        let instance = QuadInstance::filled(
496            Vec2::new(10.0, 20.0),
497            Vec2::new(100.0, 50.0),
498            Color::RED,
499            0.5,
500        );
501
502        assert_eq!(instance.position, [10.0, 20.0]);
503        assert_eq!(instance.size, [100.0, 50.0]);
504        assert_eq!(instance.border_thickness, 0.0);
505        assert_eq!(instance.z_depth, 0.5);
506    }
507
508    #[test]
509    fn test_text_instance_creation() {
510        let instance = TextInstance::new(
511            Vec2::new(5.0, 15.0),
512            Vec2::new(10.0, 12.0),
513            [0.1, 0.2],
514            [0.3, 0.4],
515            Color::WHITE,
516            0.75,
517        );
518
519        assert_eq!(instance.position, [5.0, 15.0]);
520        assert_eq!(instance.size, [10.0, 12.0]);
521        assert_eq!(instance.atlas_uv_min, [0.1, 0.2]);
522        assert_eq!(instance.atlas_uv_max, [0.3, 0.4]);
523        assert_eq!(instance.z_depth, 0.75);
524    }
525
526    #[test]
527    fn test_image_instance_creation() {
528        let instance = ImageInstance::new(Vec2::new(100.0, 200.0), Vec2::new(50.0, 60.0), 0.25);
529
530        assert_eq!(instance.position, [100.0, 200.0]);
531        assert_eq!(instance.size, [50.0, 60.0]);
532        assert_eq!(instance.z_depth, 0.25);
533        assert_eq!(instance.uv_min, [0.0, 0.0]);
534        assert_eq!(instance.uv_max, [1.0, 1.0]);
535    }
536
537    #[test]
538    fn test_image_instance_size() {
539        let size = std::mem::size_of::<ImageInstance>();
540        assert_eq!(size, 64, "ImageInstance should be 64 bytes");
541        assert_eq!(size % 16, 0, "ImageInstance should be 16-byte aligned");
542    }
543}