Skip to main content

oxiui_render_wgpu/gpu/
buffer.rs

1//! Vertex / uniform data layouts for the headless solid-fill + gradient pipelines.
2//!
3//! All structs are `#[repr(C)]` and implement [`bytemuck::Pod`] /
4//! [`bytemuck::Zeroable`] so they can be uploaded to the GPU as raw bytes with
5//! a guaranteed, stable memory layout.
6//!
7//! # Vertex layout (56 bytes = 14 × f32)
8//!
9//! The struct was extended from the original 48-byte form to add an `extra`
10//! field (`[f32; 2]`) that carries per-kind auxiliary parameters:
11//!
12//! | `kind` | `local`          | `shape_xy`      | `shape_r`           | `extra`              |
13//! |--------|------------------|-----------------|---------------------|----------------------|
14//! | 0 rect | pixel pos        | –               | –                   | –                    |
15//! | 1 circle | pixel pos      | centre (cx,cy)  | radius              | –                    |
16//! | 2 rrect-uniform | pixel pos | centre (cx,cy) | radius             | half-size (hw,hh)    |
17//! | 3 rrect-per-corner | pixel pos | centre (cx,cy) | pack16(tl,tr)  | pack16(br,bl), pack16(hw,hh) |
18//! | 4 ellipse | pixel pos     | centre (cx,cy)  | –                   | (rx, ry)             |
19//! | 5 line-seg | pixel pos    | from (ax,ay)    | half_width (+0.5=aa)| to (bx,by)           |
20//!
21//! For kind=3 the four corner radii and the half-extents are packed as u16
22//! pairs into f32 bit patterns (see `pack_u16_pair`).
23
24use oxiui_core::Color;
25
26// ── Vertex ─────────────────────────────────────────────────────────────────
27
28/// A single vertex fed to `solid.wgsl`.
29#[repr(C)]
30#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
31pub struct Vertex {
32    /// Pixel-space quad-corner position (`@location(0)`).
33    pub position: [f32; 2],
34    /// Straight-alpha RGBA colour in `[0, 1]` (`@location(1)`).
35    pub color: [f32; 4],
36    /// Pixel-space position used for SDF evaluation (`@location(2)`).
37    pub local: [f32; 2],
38    /// Shape parameter XY: centre for circle/ellipse/rounded-rect;
39    /// line-segment start point for lines (`@location(3)`).
40    pub shape_xy: [f32; 2],
41    /// Shape parameter R: radius for circle/uniform-rrect; packed u16 pair
42    /// (tl, tr) for per-corner rrect; (half_width + 0.5 if AA) for lines
43    /// (`@location(4)`).
44    pub shape_r: f32,
45    /// Primitive discriminator (`@location(5)`):
46    /// `0` = rect, `1` = circle, `2` = rrect-uniform, `3` = rrect-per-corner,
47    /// `4` = ellipse, `5` = line-segment.
48    pub kind: f32,
49    /// Auxiliary parameters (`@location(6)`):
50    /// - kind 2 (rrect-uniform): `[hw, hh]` half-extents.
51    /// - kind 3 (rrect-per-corner): `[pack16(br,bl), pack16(hw,hh)]`.
52    /// - kind 4 (ellipse): `[rx, ry]`.
53    /// - kind 5 (line-seg): `[to_x, to_y]` endpoint B.
54    /// - all others: `[0, 0]`.
55    pub extra: [f32; 2],
56}
57
58// ── Kind discriminator constants ─────────────────────────────────────────────
59
60/// Primitive discriminator value for a solid rectangle.
61pub const KIND_RECT: f32 = 0.0;
62/// Primitive discriminator value for an SDF circle.
63pub const KIND_CIRCLE: f32 = 1.0;
64/// Primitive discriminator value for a uniformly-rounded rectangle (SDF).
65pub const KIND_ROUNDED_RECT: f32 = 2.0;
66/// Primitive discriminator value for a per-corner rounded rectangle (SDF).
67pub const KIND_ROUNDED_RECT_PC: f32 = 3.0;
68/// Primitive discriminator value for an SDF ellipse.
69pub const KIND_ELLIPSE: f32 = 4.0;
70/// Primitive discriminator value for a line-segment SDF.
71pub const KIND_LINE_SEG: f32 = 5.0;
72
73// Compile-time layout guard: (2+4+2+2+1+1+2) f32 = 14 f32 = 56 bytes.
74const _: () = assert!(core::mem::size_of::<Vertex>() == 56);
75const _: () = assert!(core::mem::align_of::<Vertex>() == 4);
76
77impl Vertex {
78    /// Convert an 8-bit [`Color`] into straight-alpha `[f32; 4]` in `[0, 1]`.
79    #[inline]
80    pub fn color_to_f32(color: Color) -> [f32; 4] {
81        [
82            color.0 as f32 / 255.0,
83            color.1 as f32 / 255.0,
84            color.2 as f32 / 255.0,
85            color.3 as f32 / 255.0,
86        ]
87    }
88}
89
90// ── Globals (uniform) ────────────────────────────────────────────────────────
91
92/// Per-frame uniform block matching the WGSL `Globals` struct.
93///
94/// Padded to 16 bytes to satisfy the uniform-buffer alignment rules.
95#[repr(C)]
96#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
97pub struct Globals {
98    /// Viewport `[width, height]` in physical pixels.
99    pub viewport: [f32; 2],
100    /// Padding to round the struct up to a 16-byte boundary.
101    pub _pad: [f32; 2],
102}
103
104const _: () = assert!(core::mem::size_of::<Globals>() == 16);
105
106impl Globals {
107    /// Construct a [`Globals`] from a viewport size in pixels.
108    #[inline]
109    pub fn new(width: u32, height: u32) -> Self {
110        Self {
111            viewport: [width as f32, height as f32],
112            _pad: [0.0, 0.0],
113        }
114    }
115}
116
117// ── Gradient vertex / uniform ─────────────────────────────────────────────────
118
119/// A single vertex fed to `gradient.wgsl`.
120#[repr(C)]
121#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
122pub struct GradientVertex {
123    /// Pixel-space quad-corner position (`@location(0)`).
124    pub position: [f32; 2],
125    /// Pixel-space position passed to the fragment stage for gradient sampling
126    /// (`@location(1)`).
127    pub local: [f32; 2],
128}
129
130const _: () = assert!(core::mem::size_of::<GradientVertex>() == 16);
131
132/// Maximum number of gradient colour stops supported in a single gradient draw.
133pub const MAX_GRADIENT_STOPS: usize = 8;
134
135/// Per-gradient uniform block sent to `gradient.wgsl`.
136///
137/// Follows std140 layout: each field is aligned to its natural boundary,
138/// the total size is a multiple of 16 bytes.
139#[repr(C)]
140#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
141pub struct GradientUniforms {
142    /// Linear: gradient start point (pixel space).
143    /// Radial: gradient centre point (pixel space).
144    pub p0: [f32; 2],
145    /// Linear: gradient end point (pixel space).
146    /// Radial: unused (zeroed).
147    pub p1: [f32; 2],
148    /// Radial: outer radius in pixels.  0 for linear.
149    pub radius: f32,
150    /// Gradient type: 0 = linear, 1 = radial.
151    pub gradient_type: u32,
152    /// Number of active colour stops (1–8).
153    pub stop_count: u32,
154    /// Padding to align the arrays on 16-byte boundaries.
155    pub _pad: u32,
156    /// Per-stop offset packed into `.x` (y/z/w = 0).
157    pub stop_offsets: [[f32; 4]; MAX_GRADIENT_STOPS],
158    /// Per-stop RGBA colour in `[0, 1]`.
159    pub stop_colors: [[f32; 4]; MAX_GRADIENT_STOPS],
160}
161
162// Size: p0(8) + p1(8) + radius(4) + gradient_type(4) + stop_count(4) + _pad(4)
163//       + stop_offsets(8*16=128) + stop_colors(8*16=128) = 32 + 256 = 288 bytes.
164const _: () = assert!(core::mem::size_of::<GradientUniforms>() == 288);
165
166// ── Packing helpers ───────────────────────────────────────────────────────────
167
168/// Pack two `u16` values into the bit pattern of a `f32`.
169///
170/// The WGSL shader unpacks with `bitcast<u32>(v) >> 16u` and `& 0xffffu`.
171/// Values must be in `[0, 65535]`.
172#[inline]
173pub fn pack_u16_pair(hi: u16, lo: u16) -> f32 {
174    f32::from_bits(((hi as u32) << 16) | (lo as u32))
175}
176
177// ── Quad emitters ────────────────────────────────────────────────────────────
178
179/// Append six vertices (two triangles) covering the axis-aligned rectangle
180/// `(x, y, w, h)` with a uniform `color`, tagged as a solid rectangle.
181pub fn push_rect_quad(out: &mut Vec<Vertex>, x: f32, y: f32, w: f32, h: f32, color: Color) {
182    let rgba = Vertex::color_to_f32(color);
183    let x1 = x + w;
184    let y1 = y + h;
185    let corners = [[x, y], [x, y1], [x1, y1], [x, y], [x1, y1], [x1, y]];
186    for c in corners {
187        out.push(Vertex {
188            position: c,
189            color: rgba,
190            local: c,
191            shape_xy: [0.0, 0.0],
192            shape_r: 0.0,
193            kind: KIND_RECT,
194            extra: [0.0, 0.0],
195        });
196    }
197}
198
199/// Append six vertices covering the bounding quad of the circle centred at
200/// `(cx, cy)` with `radius`, tagged as an SDF circle.
201pub fn push_circle_quad(out: &mut Vec<Vertex>, cx: f32, cy: f32, radius: f32, color: Color) {
202    let rgba = Vertex::color_to_f32(color);
203    let r = radius + 1.0;
204    let x0 = cx - r;
205    let y0 = cy - r;
206    let x1 = cx + r;
207    let y1 = cy + r;
208    let corners = [[x0, y0], [x0, y1], [x1, y1], [x0, y0], [x1, y1], [x1, y0]];
209    for c in corners {
210        out.push(Vertex {
211            position: c,
212            color: rgba,
213            local: c,
214            shape_xy: [cx, cy],
215            shape_r: radius,
216            kind: KIND_CIRCLE,
217            extra: [0.0, 0.0],
218        });
219    }
220}
221
222/// Append six vertices for a uniformly-rounded rectangle (SDF).
223///
224/// The quad is inflated by 1 px on all sides to avoid clipping the AA rim.
225pub fn push_rounded_rect_quad(
226    out: &mut Vec<Vertex>,
227    x: f32,
228    y: f32,
229    w: f32,
230    h: f32,
231    radius: f32,
232    color: Color,
233) {
234    let rgba = Vertex::color_to_f32(color);
235    let r = radius.min(w * 0.5).min(h * 0.5).max(0.0);
236    let cx = x + w * 0.5;
237    let cy = y + h * 0.5;
238    let hw = w * 0.5;
239    let hh = h * 0.5;
240    let pad = 1.0_f32;
241    let x0 = x - pad;
242    let y0 = y - pad;
243    let x1 = x + w + pad;
244    let y1 = y + h + pad;
245    let corners = [[x0, y0], [x0, y1], [x1, y1], [x0, y0], [x1, y1], [x1, y0]];
246    for c in corners {
247        out.push(Vertex {
248            position: c,
249            color: rgba,
250            local: c,
251            shape_xy: [cx, cy],
252            shape_r: r,
253            kind: KIND_ROUNDED_RECT,
254            extra: [hw, hh],
255        });
256    }
257}
258
259/// Append six vertices for a per-corner rounded rectangle (SDF).
260///
261/// `radii` is `[top-left, top-right, bottom-right, bottom-left]`.
262///
263/// The four radii and the half-extents are packed into the vertex using
264/// integer-arithmetic encoding that avoids GPU subnormal/denormal issues:
265///
266/// * `shape_r`  = `tl_r * 256.0 + tr_r`  (radii clamped to `[0, 255]`)
267/// * `extra[0]` = `br_r * 256.0 + bl_r`
268/// * `extra[1]` = `hw_i * 4096.0 + hh_i` (half-extents clamped to `[0, 4095]`)
269///
270/// The WGSL shader unpacks with `floor(v / base)` / `mod(v, base)`.
271/// All packed values stay below `2^24`, so f32 represents them exactly.
272pub fn push_rounded_rect_per_corner_quad(
273    out: &mut Vec<Vertex>,
274    x: f32,
275    y: f32,
276    w: f32,
277    h: f32,
278    radii: [f32; 4],
279    color: Color,
280) {
281    let rgba = Vertex::color_to_f32(color);
282    let [tl, tr, br, bl] = radii;
283    let hw = w * 0.5;
284    let hh = h * 0.5;
285    let cx = x + hw;
286    let cy = y + hh;
287    let clamp_r = |r: f32| r.clamp(0.0, hw.min(hh).min(255.0));
288    let tl = clamp_r(tl);
289    let tr = clamp_r(tr);
290    let br = clamp_r(br);
291    let bl = clamp_r(bl);
292    let hw_c = hw.clamp(0.0, 4095.0);
293    let hh_c = hh.clamp(0.0, 4095.0);
294    // Encode using integer arithmetic within exact f32 range (< 2^24).
295    let r_packed = tl.floor() * 256.0 + tr.floor();
296    let brbl_packed = br.floor() * 256.0 + bl.floor();
297    let hwhh_packed = hw_c.floor() * 4096.0 + hh_c.floor();
298    let pad = 1.0_f32;
299    let x0 = x - pad;
300    let y0 = y - pad;
301    let x1 = x + w + pad;
302    let y1 = y + h + pad;
303    let corners = [[x0, y0], [x0, y1], [x1, y1], [x0, y0], [x1, y1], [x1, y0]];
304    for c in corners {
305        out.push(Vertex {
306            position: c,
307            color: rgba,
308            local: c,
309            shape_xy: [cx, cy],
310            shape_r: r_packed,
311            kind: KIND_ROUNDED_RECT_PC,
312            extra: [brbl_packed, hwhh_packed],
313        });
314    }
315}
316
317/// Append six vertices for an SDF ellipse centred at `(cx, cy)` with
318/// horizontal radius `rx` and vertical radius `ry`.
319pub fn push_ellipse_quad(out: &mut Vec<Vertex>, cx: f32, cy: f32, rx: f32, ry: f32, color: Color) {
320    let rgba = Vertex::color_to_f32(color);
321    let pad = 1.0_f32;
322    let x0 = cx - rx - pad;
323    let y0 = cy - ry - pad;
324    let x1 = cx + rx + pad;
325    let y1 = cy + ry + pad;
326    let corners = [[x0, y0], [x0, y1], [x1, y1], [x0, y0], [x1, y1], [x1, y0]];
327    for c in corners {
328        out.push(Vertex {
329            position: c,
330            color: rgba,
331            local: c,
332            shape_xy: [cx, cy],
333            shape_r: 0.0,
334            kind: KIND_ELLIPSE,
335            extra: [rx, ry],
336        });
337    }
338}
339
340/// Parameters for a line-segment SDF quad.
341pub struct LineQuadParams {
342    /// Start x coordinate.
343    pub from_x: f32,
344    /// Start y coordinate.
345    pub from_y: f32,
346    /// End x coordinate.
347    pub to_x: f32,
348    /// End y coordinate.
349    pub to_y: f32,
350    /// Half-width of the line stroke.
351    pub half_width: f32,
352    /// Line colour.
353    pub color: Color,
354    /// `true` = anti-aliased edges; `false` = hard clip.
355    pub aa_smooth: bool,
356}
357
358/// Append six vertices for a line-segment SDF quad.
359///
360/// The quad is expanded perpendicular to the line by `half_width + 1.0` pixels
361/// to ensure the anti-aliased edge is not clipped.
362///
363/// When `aa_smooth` is `true`, the shader uses `smoothstep` for soft edges.
364/// When `false`, the edge is hard-clipped.
365pub fn push_line_quad(out: &mut Vec<Vertex>, params: LineQuadParams) {
366    let LineQuadParams {
367        from_x,
368        from_y,
369        to_x,
370        to_y,
371        half_width,
372        color,
373        aa_smooth,
374    } = params;
375    let rgba = Vertex::color_to_f32(color);
376    let dx = to_x - from_x;
377    let dy = to_y - from_y;
378    let len = (dx * dx + dy * dy).sqrt().max(1e-6);
379    // Perpendicular unit vector (rotated 90° CCW).
380    let nx = -dy / len;
381    let ny = dx / len;
382    // Also expand along the line direction (caps) by half_width.
383    let lx = dx / len;
384    let ly = dy / len;
385    let expand = half_width + 1.0;
386    let cap = half_width + 1.0;
387    // Quad corners: a = from-side start, b = from-side end,
388    //               c = to-side end,     d = to-side start.
389    let ax = from_x - lx * cap + nx * expand;
390    let ay = from_y - ly * cap + ny * expand;
391    let bx = from_x - lx * cap - nx * expand;
392    let by = from_y - ly * cap - ny * expand;
393    let cx = to_x + lx * cap - nx * expand;
394    let cy_v = to_y + ly * cap - ny * expand;
395    let dxp = to_x + lx * cap + nx * expand;
396    let dyp = to_y + ly * cap + ny * expand;
397    // Two CCW triangles: (a, b, c) and (a, c, d).
398    let corners = [
399        [ax, ay],
400        [bx, by],
401        [cx, cy_v],
402        [ax, ay],
403        [cx, cy_v],
404        [dxp, dyp],
405    ];
406    // Encode aa_smooth in the fractional part of shape_r:
407    // shape_r = half_width + 0.5 → aa, half_width + 0.0 → hard.
408    let shape_r_val = if aa_smooth {
409        half_width + 0.5
410    } else {
411        half_width
412    };
413    for c in corners {
414        out.push(Vertex {
415            position: c,
416            color: rgba,
417            local: c,
418            shape_xy: [from_x, from_y],
419            shape_r: shape_r_val,
420            kind: KIND_LINE_SEG,
421            extra: [to_x, to_y],
422        });
423    }
424}
425
426/// Append three vertices (one triangle) for a path fill triangle.
427pub fn push_triangle(
428    out: &mut Vec<Vertex>,
429    p0: [f32; 2],
430    p1: [f32; 2],
431    p2: [f32; 2],
432    color: Color,
433) {
434    let rgba = Vertex::color_to_f32(color);
435    for c in [p0, p1, p2] {
436        out.push(Vertex {
437            position: c,
438            color: rgba,
439            local: c,
440            shape_xy: [0.0, 0.0],
441            shape_r: 0.0,
442            kind: KIND_RECT,
443            extra: [0.0, 0.0],
444        });
445    }
446}
447
448// ── TexVertex ─────────────────────────────────────────────────────────────────
449
450/// A single vertex fed to `textured.wgsl`.
451///
452/// 32 bytes = 2 (position) + 2 (uv) + 4 (tint) f32 values.
453#[repr(C)]
454#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
455pub struct TexVertex {
456    /// Pixel-space quad-corner position (`@location(0)`).
457    pub position: [f32; 2],
458    /// Texture UV in `[0, 1]` (`@location(1)`).
459    pub uv: [f32; 2],
460    /// RGBA tint multiplier (`@location(2)`); normally `[1, 1, 1, 1]`.
461    pub tint: [f32; 4],
462}
463
464const _: () = assert!(core::mem::size_of::<TexVertex>() == 32);
465
466// ── Textured quad emitters ────────────────────────────────────────────────────
467
468/// Parameters for a textured quad.
469pub struct TexQuadParams {
470    /// Quad x position (pixel space).
471    pub x: f32,
472    /// Quad y position (pixel space).
473    pub y: f32,
474    /// Quad width (pixel space).
475    pub w: f32,
476    /// Quad height (pixel space).
477    pub h: f32,
478    /// UV left edge.
479    pub u0: f32,
480    /// UV top edge.
481    pub v0: f32,
482    /// UV right edge.
483    pub u1: f32,
484    /// UV bottom edge.
485    pub v1: f32,
486    /// RGBA tint multiplier (component-wise; use `[1,1,1,1]` for no tint).
487    pub tint: [f32; 4],
488}
489
490/// Append six textured vertices (two triangles) for the given [`TexQuadParams`].
491///
492/// The UV rectangle `(u0, v0) → (u1, v1)` is mapped over the destination quad.
493/// `tint` is multiplied component-wise with the sampled texel in the shader.
494pub fn push_textured_quad(out: &mut Vec<TexVertex>, p: TexQuadParams) {
495    let TexQuadParams {
496        x,
497        y,
498        w,
499        h,
500        u0,
501        v0,
502        u1,
503        v1,
504        tint,
505    } = p;
506    let x1 = x + w;
507    let y1 = y + h;
508    // CCW winding: TL, BL, BR, TL, BR, TR
509    let corners = [
510        ([x, y], [u0, v0]),
511        ([x, y1], [u0, v1]),
512        ([x1, y1], [u1, v1]),
513        ([x, y], [u0, v0]),
514        ([x1, y1], [u1, v1]),
515        ([x1, y], [u1, v0]),
516    ];
517    for (pos, uv) in corners {
518        out.push(TexVertex {
519            position: pos,
520            uv,
521            tint,
522        });
523    }
524}
525
526/// Append the 9 quads for a nine-slice image.
527///
528/// `dest` is `[x, y, w, h]` in pixel space (the destination rectangle).
529/// `img_w`/`img_h` are the source image dimensions in pixels.
530/// `insets` is `[top, right, bottom, left]` in source pixels.
531///
532/// The nine regions are:
533///   TL corner | T edge   | TR corner
534///   L  edge   | Centre   | R  edge
535///   BL corner | B edge   | BR corner
536///
537/// Insets are clamped so `left+right <= img_w` and `top+bottom <= img_h`.
538/// Destination insets scale proportionally.  Any region with zero source or
539/// destination dimension is skipped.
540pub fn push_nine_slice_quads(
541    out: &mut Vec<TexVertex>,
542    dest: [f32; 4],
543    img_w: u32,
544    img_h: u32,
545    insets: [u32; 4],
546    tint: [f32; 4],
547) {
548    let [dst_x, dst_y, dst_w, dst_h] = dest;
549
550    // ── Clamp source insets so they don't overlap ─────────────────────────────
551    let iw = img_w as f32;
552    let ih = img_h as f32;
553
554    let raw_top = insets[0] as f32;
555    let raw_right = insets[1] as f32;
556    let raw_bottom = insets[2] as f32;
557    let raw_left = insets[3] as f32;
558
559    // Scale down if left+right > img_w or top+bottom > img_h
560    let lr_sum = raw_left + raw_right;
561    let tb_sum = raw_top + raw_bottom;
562    let (src_left, src_right) = if lr_sum > iw && lr_sum > 0.0 {
563        let scale = iw / lr_sum;
564        (raw_left * scale, raw_right * scale)
565    } else {
566        (raw_left, raw_right)
567    };
568    let (src_top, src_bottom) = if tb_sum > ih && tb_sum > 0.0 {
569        let scale = ih / tb_sum;
570        (raw_top * scale, raw_bottom * scale)
571    } else {
572        (raw_top, raw_bottom)
573    };
574
575    // ── Destination insets (pixel-space sizes matching corner regions) ─────────
576    // Scale destination corner sizes to match the source inset proportions,
577    // but clamp so they don't exceed the destination rect dimensions.
578    let dst_left = src_left.min(dst_w * 0.5);
579    let dst_right = src_right.min(dst_w - dst_left);
580    let dst_top = src_top.min(dst_h * 0.5);
581    let dst_bottom = src_bottom.min(dst_h - dst_top);
582
583    // ── Source UV breakpoints ─────────────────────────────────────────────────
584    let u0 = 0.0_f32;
585    let u1 = src_left / iw;
586    let u2 = (iw - src_right) / iw;
587    let u3 = 1.0_f32;
588
589    let v0 = 0.0_f32;
590    let v1 = src_top / ih;
591    let v2 = (ih - src_bottom) / ih;
592    let v3 = 1.0_f32;
593
594    // ── Destination pixel breakpoints ─────────────────────────────────────────
595    let dx0 = dst_x;
596    let dx1 = dst_x + dst_left;
597    let dx2 = dst_x + dst_w - dst_right;
598    let dx3 = dst_x + dst_w;
599
600    let dy0 = dst_y;
601    let dy1 = dst_y + dst_top;
602    let dy2 = dst_y + dst_h - dst_bottom;
603    let dy3 = dst_y + dst_h;
604
605    // ── Emit each of the 9 regions (row-major, skip degenerate) ──────────────
606    // Row 0: TL corner, T edge, TR corner
607    // Row 1: L edge,   Centre, R edge
608    // Row 2: BL corner, B edge, BR corner
609
610    let regions: [([f32; 4], [f32; 4]); 9] = [
611        // (dest [x,y,w,h], src_uv [u0,v0,u1,v1])
612        ([dx0, dy0, dx1 - dx0, dy1 - dy0], [u0, v0, u1, v1]), // TL
613        ([dx1, dy0, dx2 - dx1, dy1 - dy0], [u1, v0, u2, v1]), // T
614        ([dx2, dy0, dx3 - dx2, dy1 - dy0], [u2, v0, u3, v1]), // TR
615        ([dx0, dy1, dx1 - dx0, dy2 - dy1], [u0, v1, u1, v2]), // L
616        ([dx1, dy1, dx2 - dx1, dy2 - dy1], [u1, v1, u2, v2]), // Centre
617        ([dx2, dy1, dx3 - dx2, dy2 - dy1], [u2, v1, u3, v2]), // R
618        ([dx0, dy2, dx1 - dx0, dy3 - dy2], [u0, v2, u1, v3]), // BL
619        ([dx1, dy2, dx2 - dx1, dy3 - dy2], [u1, v2, u2, v3]), // B
620        ([dx2, dy2, dx3 - dx2, dy3 - dy2], [u2, v2, u3, v3]), // BR
621    ];
622
623    for ([rx, ry, rw, rh], [ru0, rv0, ru1, rv1]) in regions {
624        // Skip degenerate regions (zero destination or zero source UV span)
625        if rw <= 0.0 || rh <= 0.0 {
626            continue;
627        }
628        if (ru1 - ru0).abs() <= 0.0 || (rv1 - rv0).abs() <= 0.0 {
629            continue;
630        }
631        push_textured_quad(
632            out,
633            TexQuadParams {
634                x: rx,
635                y: ry,
636                w: rw,
637                h: rh,
638                u0: ru0,
639                v0: rv0,
640                u1: ru1,
641                v1: rv1,
642                tint,
643            },
644        );
645    }
646}
647
648/// Append six gradient vertices covering `(x, y, w, h)`.
649pub fn push_gradient_quad(out: &mut Vec<GradientVertex>, x: f32, y: f32, w: f32, h: f32) {
650    let x1 = x + w;
651    let y1 = y + h;
652    let corners = [[x, y], [x, y1], [x1, y1], [x, y], [x1, y1], [x1, y]];
653    for c in corners {
654        out.push(GradientVertex {
655            position: c,
656            local: c,
657        });
658    }
659}
660
661// ── BlurUniforms ──────────────────────────────────────────────────────────────
662
663/// Uniform block for the separable Gaussian blur pass.
664///
665/// Matches the `BlurUniforms` struct in `blur.wgsl`.
666#[repr(C)]
667#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
668pub struct BlurUniforms {
669    /// Blur direction: `[1.0, 0.0]` for horizontal, `[0.0, 1.0]` for vertical.
670    pub direction: [f32; 2],
671    /// Per-texel step size: `[1.0/viewport_w, 1.0/viewport_h]`.
672    pub texel_size: [f32; 2],
673    /// Blur radius in pixels (0 = no blur, 1 tap each side).
674    pub radius: f32,
675    /// Gaussian sigma: `max(blur_radius, 1.0) / 2.0`.
676    pub sigma: f32,
677    /// Padding to 32 bytes.
678    pub _pad: [f32; 2],
679}
680
681const _: () = assert!(core::mem::size_of::<BlurUniforms>() == 32);
682
683// ── CompUniforms ──────────────────────────────────────────────────────────────
684
685/// Uniform block for the shadow composite pass.
686///
687/// Matches the `CompUniforms` struct in `composite.wgsl`.
688#[repr(C)]
689#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
690pub struct CompUniforms {
691    /// Shadow tint colour in `[0, 1]` premultiplied RGBA.
692    pub tint: [f32; 4],
693    /// Per-texel step size: `[1.0/viewport_w, 1.0/viewport_h]`.
694    pub texel_size: [f32; 2],
695    /// Padding to 32 bytes.
696    pub _pad: [f32; 2],
697}
698
699const _: () = assert!(core::mem::size_of::<CompUniforms>() == 32);
700
701// ── Fullscreen quad emitter ───────────────────────────────────────────────────
702
703/// Push a fullscreen quad (covers `[0,0]` to `[w, h]`) as 6 [`GradientVertex`]
704/// vertices.
705///
706/// Used by the blur and composite passes which draw over the entire viewport.
707/// The `local` field carries pixel-space coordinates used as UV source in those
708/// shaders.
709pub fn push_fullscreen_quad(out: &mut Vec<GradientVertex>, w: f32, h: f32) {
710    // Corner definitions: (position, local) — both in pixel space.
711    let corners = [
712        ([0.0_f32, 0.0_f32], [0.0_f32, 0.0_f32]), // TL
713        ([w, 0.0], [w, 0.0]),                     // TR
714        ([0.0, h], [0.0, h]),                     // BL
715        ([w, h], [w, h]),                         // BR
716    ];
717    // Triangle 1: TL, TR, BL
718    out.push(GradientVertex {
719        position: corners[0].0,
720        local: corners[0].1,
721    });
722    out.push(GradientVertex {
723        position: corners[1].0,
724        local: corners[1].1,
725    });
726    out.push(GradientVertex {
727        position: corners[2].0,
728        local: corners[2].1,
729    });
730    // Triangle 2: TR, BR, BL
731    out.push(GradientVertex {
732        position: corners[1].0,
733        local: corners[1].1,
734    });
735    out.push(GradientVertex {
736        position: corners[3].0,
737        local: corners[3].1,
738    });
739    out.push(GradientVertex {
740        position: corners[2].0,
741        local: corners[2].1,
742    });
743}
744
745// ── Tests ─────────────────────────────────────────────────────────────────────
746
747#[cfg(test)]
748mod tests {
749    use super::*;
750
751    #[test]
752    fn vertex_size_is_56_bytes() {
753        assert_eq!(core::mem::size_of::<Vertex>(), 56);
754    }
755
756    #[test]
757    fn globals_size_is_16_bytes() {
758        assert_eq!(core::mem::size_of::<Globals>(), 16);
759    }
760
761    #[test]
762    fn gradient_vertex_size_is_16_bytes() {
763        assert_eq!(core::mem::size_of::<GradientVertex>(), 16);
764    }
765
766    #[test]
767    fn gradient_uniforms_size_is_288_bytes() {
768        assert_eq!(core::mem::size_of::<GradientUniforms>(), 288);
769    }
770
771    #[test]
772    fn color_to_f32_maps_full_range() {
773        let white = Vertex::color_to_f32(Color(255, 255, 255, 255));
774        assert!((white[0] - 1.0).abs() < 1e-6);
775        assert!((white[3] - 1.0).abs() < 1e-6);
776        let black = Vertex::color_to_f32(Color(0, 0, 0, 0));
777        assert_eq!(black, [0.0, 0.0, 0.0, 0.0]);
778    }
779
780    #[test]
781    fn rect_quad_emits_six_vertices() {
782        let mut v = Vec::new();
783        push_rect_quad(&mut v, 1.0, 2.0, 3.0, 4.0, Color(255, 0, 0, 255));
784        assert_eq!(v.len(), 6);
785        for vert in &v {
786            assert_eq!(vert.kind, KIND_RECT);
787        }
788        let xs: Vec<f32> = v.iter().map(|vt| vt.position[0]).collect();
789        assert!(xs.contains(&1.0));
790        assert!(xs.contains(&4.0));
791    }
792
793    #[test]
794    fn circle_quad_emits_six_vertices_with_center() {
795        let mut v = Vec::new();
796        push_circle_quad(&mut v, 10.0, 10.0, 5.0, Color(0, 255, 0, 255));
797        assert_eq!(v.len(), 6);
798        for vert in &v {
799            assert_eq!(vert.kind, KIND_CIRCLE);
800            assert_eq!(vert.shape_xy, [10.0, 10.0]);
801            assert!((vert.shape_r - 5.0).abs() < 1e-6);
802        }
803    }
804
805    #[test]
806    fn vertices_are_pod_castable() {
807        let mut v = Vec::new();
808        push_rect_quad(&mut v, 0.0, 0.0, 1.0, 1.0, Color(1, 2, 3, 4));
809        let bytes: &[u8] = bytemuck::cast_slice(&v);
810        assert_eq!(bytes.len(), 6 * 56);
811    }
812
813    #[test]
814    fn rounded_rect_quad_emits_six_vertices() {
815        let mut v = Vec::new();
816        push_rounded_rect_quad(&mut v, 10.0, 10.0, 80.0, 40.0, 8.0, Color(0, 0, 255, 255));
817        assert_eq!(v.len(), 6);
818        for vert in &v {
819            assert_eq!(vert.kind, KIND_ROUNDED_RECT);
820        }
821    }
822
823    #[test]
824    fn rounded_rect_pc_quad_emits_six_vertices() {
825        let mut v = Vec::new();
826        push_rounded_rect_per_corner_quad(
827            &mut v,
828            10.0,
829            10.0,
830            80.0,
831            40.0,
832            [4.0, 8.0, 4.0, 8.0],
833            Color(0, 0, 255, 255),
834        );
835        assert_eq!(v.len(), 6);
836        for vert in &v {
837            assert_eq!(vert.kind, KIND_ROUNDED_RECT_PC);
838        }
839    }
840
841    #[test]
842    fn ellipse_quad_emits_six_vertices() {
843        let mut v = Vec::new();
844        push_ellipse_quad(&mut v, 50.0, 50.0, 30.0, 20.0, Color(255, 255, 0, 255));
845        assert_eq!(v.len(), 6);
846        for vert in &v {
847            assert_eq!(vert.kind, KIND_ELLIPSE);
848            assert!((vert.extra[0] - 30.0).abs() < 1e-4);
849            assert!((vert.extra[1] - 20.0).abs() < 1e-4);
850        }
851    }
852
853    #[test]
854    fn line_quad_emits_six_vertices() {
855        let mut v = Vec::new();
856        push_line_quad(
857            &mut v,
858            LineQuadParams {
859                from_x: 0.0,
860                from_y: 0.0,
861                to_x: 100.0,
862                to_y: 0.0,
863                half_width: 2.0,
864                color: Color(255, 0, 0, 255),
865                aa_smooth: true,
866            },
867        );
868        assert_eq!(v.len(), 6);
869        for vert in &v {
870            assert_eq!(vert.kind, KIND_LINE_SEG);
871        }
872    }
873
874    #[test]
875    fn push_triangle_emits_three_vertices() {
876        let mut v = Vec::new();
877        push_triangle(
878            &mut v,
879            [0.0, 0.0],
880            [10.0, 0.0],
881            [5.0, 8.0],
882            Color(255, 255, 255, 255),
883        );
884        assert_eq!(v.len(), 3);
885        for vert in &v {
886            assert_eq!(vert.kind, KIND_RECT);
887        }
888    }
889
890    #[test]
891    fn gradient_quad_emits_six_vertices() {
892        let mut v = Vec::new();
893        push_gradient_quad(&mut v, 0.0, 0.0, 100.0, 50.0);
894        assert_eq!(v.len(), 6);
895    }
896
897    #[test]
898    fn pack_u16_pair_round_trips() {
899        let packed = pack_u16_pair(42, 1000);
900        let bits = packed.to_bits();
901        let hi = (bits >> 16) as u16;
902        let lo = (bits & 0xffff) as u16;
903        assert_eq!(hi, 42);
904        assert_eq!(lo, 1000);
905    }
906
907    #[test]
908    fn tex_vertex_size_is_32_bytes() {
909        assert_eq!(core::mem::size_of::<TexVertex>(), 32);
910    }
911
912    #[test]
913    fn textured_quad_emits_six_vertices() {
914        let mut v = Vec::new();
915        push_textured_quad(
916            &mut v,
917            TexQuadParams {
918                x: 0.0,
919                y: 0.0,
920                w: 100.0,
921                h: 50.0,
922                u0: 0.0,
923                v0: 0.0,
924                u1: 1.0,
925                v1: 1.0,
926                tint: [1.0, 1.0, 1.0, 1.0],
927            },
928        );
929        assert_eq!(v.len(), 6);
930        // All vertices have the same tint
931        for vert in &v {
932            assert_eq!(vert.tint, [1.0, 1.0, 1.0, 1.0]);
933        }
934        // UV corners should span [0,0] to [1,1]
935        let us: Vec<f32> = v.iter().map(|vt| vt.uv[0]).collect();
936        assert!(us.contains(&0.0_f32));
937        assert!(us.contains(&1.0_f32));
938    }
939
940    #[test]
941    fn nine_slice_quads_emit_at_most_54_vertices() {
942        let mut v = Vec::new();
943        // 12x12 image, 4px insets on each side
944        push_nine_slice_quads(
945            &mut v,
946            [0.0, 0.0, 64.0, 64.0],
947            12,
948            12,
949            [4, 4, 4, 4],
950            [1.0, 1.0, 1.0, 1.0],
951        );
952        // 9 regions × 6 vertices = 54
953        assert_eq!(v.len(), 54);
954    }
955
956    #[test]
957    fn blur_uniforms_size_is_32_bytes() {
958        assert_eq!(core::mem::size_of::<BlurUniforms>(), 32);
959    }
960
961    #[test]
962    fn comp_uniforms_size_is_32_bytes() {
963        assert_eq!(core::mem::size_of::<CompUniforms>(), 32);
964    }
965
966    #[test]
967    fn fullscreen_quad_emits_six_vertices() {
968        let mut v = Vec::new();
969        push_fullscreen_quad(&mut v, 100.0, 200.0);
970        assert_eq!(v.len(), 6);
971        // Check corners cover 0..100 and 0..200
972        let xs: Vec<f32> = v.iter().map(|vt| vt.position[0]).collect();
973        let ys: Vec<f32> = v.iter().map(|vt| vt.position[1]).collect();
974        assert!(xs.contains(&0.0_f32));
975        assert!(xs.contains(&100.0_f32));
976        assert!(ys.contains(&0.0_f32));
977        assert!(ys.contains(&200.0_f32));
978    }
979
980    #[test]
981    fn nine_slice_degenerate_insets_skip_regions() {
982        let mut v = Vec::new();
983        // Zero insets → only centre region (6 verts), corner/edge regions are
984        // degenerate because src UV span is 0.
985        push_nine_slice_quads(
986            &mut v,
987            [0.0, 0.0, 64.0, 64.0],
988            12,
989            12,
990            [0, 0, 0, 0],
991            [1.0, 1.0, 1.0, 1.0],
992        );
993        // Only the centre should be non-degenerate: 6 vertices
994        assert_eq!(v.len(), 6);
995    }
996}