Skip to main content

oxiui_render_wgpu/gpu/
geometry.rs

1//! CPU-side geometry building for [`WgpuBackend`].
2//!
3//! This module owns:
4//!
5//! - `DrawSegment` — a scissor-bounded range in the solid vertex buffer.
6//! - `GradientDraw` — per-draw gradient vertex + uniform data.
7//! - `build_geometry` — the main CPU geometry builder that walks a
8//!   [`DrawList`] and emits `(Vertex[], DrawSegment[], GradientDraw[],
9//!   TexturedDraw[])`.
10//! - Visibility culling helpers: `cmd_bounds`, `rect_from_points`,
11//!   `rects_intersect`, `scissor_to_rect`.
12//! - Stroke/dashed-line emitters and gradient uniform builders.
13//!
14//! [`WgpuBackend`]: super::renderer::WgpuBackend
15
16use oxiui_core::geometry::Rect;
17use oxiui_core::paint::{DrawCommand, DrawList, GradientStop, ImageFilter};
18use oxiui_core::Color;
19
20use crate::clip::{ClipRect, ClipStack};
21use crate::gpu::buffer::{
22    push_circle_quad, push_ellipse_quad, push_gradient_quad, push_line_quad, push_nine_slice_quads,
23    push_rect_quad, push_rounded_rect_per_corner_quad, push_rounded_rect_quad, push_textured_quad,
24    GradientUniforms, GradientVertex, LineQuadParams, TexQuadParams, Vertex, MAX_GRADIENT_STOPS,
25};
26use crate::gpu::tessellator::{tessellate_fill, tessellate_stroke};
27use crate::gpu::texture::TexturedDraw;
28
29// ── DrawSegment ───────────────────────────────────────────────────────────────
30
31/// A scissor-bounded range of vertices in the solid vertex buffer.
32#[derive(Clone, Copy, Debug)]
33pub(crate) struct DrawSegment {
34    pub(crate) start: u32,
35    pub(crate) end: u32,
36    pub(crate) scissor: Option<[u32; 4]>,
37}
38
39// ── GradientDraw ──────────────────────────────────────────────────────────────
40
41/// Per-draw gradient vertex + uniform data.
42pub(crate) struct GradientDraw {
43    pub(crate) verts: Vec<GradientVertex>,
44    pub(crate) uniforms: GradientUniforms,
45    pub(crate) scissor: Option<[u32; 4]>,
46}
47
48// ── BackdropBlurDraw ──────────────────────────────────────────────────────────
49
50/// Data for a single `BackdropBlur` draw command.
51///
52/// These are collected by [`build_geometry`] and will be executed by the
53/// renderer as a copy-from-colour + Gaussian blur + write-back pass.
54#[allow(dead_code)] // fields consumed in the renderer's backdrop-blur pass
55pub(crate) struct BackdropBlurDraw {
56    /// The region to blur, in pixel coordinates `[x, y, w, h]`.
57    pub(crate) rect: [f32; 4],
58    /// Blur radius in pixels.
59    pub(crate) blur_radius: f32,
60}
61
62// ── Scissor helpers ───────────────────────────────────────────────────────────
63
64/// Compute scissor from clip stack, clamped to the viewport.
65pub(crate) fn scissor_from_stack(
66    stack: &ClipStack,
67    viewport_w: u32,
68    viewport_h: u32,
69) -> Option<[u32; 4]> {
70    let raw = stack.as_scissor()?;
71    Some(clamp_scissor(raw, viewport_w, viewport_h))
72}
73
74/// Clamp a scissor rect to the viewport bounds.
75pub(crate) fn clamp_scissor([x, y, w, h]: [u32; 4], viewport_w: u32, viewport_h: u32) -> [u32; 4] {
76    let x = x.min(viewport_w);
77    let y = y.min(viewport_h);
78    let w = w.min(viewport_w - x);
79    let h = h.min(viewport_h - y);
80    [x, y, w, h]
81}
82
83// ── Main geometry builder ─────────────────────────────────────────────────────
84
85/// Output of [`build_geometry`]: a tuple of the four draw-data collections.
86pub(crate) type GeometryOutput = (
87    Vec<Vertex>,
88    Vec<DrawSegment>,
89    Vec<GradientDraw>,
90    Vec<TexturedDraw>,
91    Vec<BackdropBlurDraw>,
92);
93
94/// Walk `list` and emit all CPU geometry for the solid, gradient and textured passes.
95///
96/// Returns a [`GeometryOutput`] tuple:
97/// `(solid_vertices, draw_segments, gradient_draws, textured_draws, backdrop_blur_draws)`.
98pub(crate) fn build_geometry(list: &DrawList, viewport_w: u32, viewport_h: u32) -> GeometryOutput {
99    let mut verts: Vec<Vertex> = Vec::new();
100    let mut segments: Vec<DrawSegment> = Vec::new();
101    let mut gradient_draws: Vec<GradientDraw> = Vec::new();
102    let mut textured_draws: Vec<TexturedDraw> = Vec::new();
103    let mut backdrop_blur_draws: Vec<BackdropBlurDraw> = Vec::new();
104    let mut stack = ClipStack::new();
105
106    let mut current_scissor = scissor_from_stack(&stack, viewport_w, viewport_h);
107    let mut segment_start: u32 = 0;
108
109    let flush = |segs: &mut Vec<DrawSegment>, start: u32, end: u32, sc: Option<[u32; 4]>| {
110        if end > start {
111            segs.push(DrawSegment {
112                start,
113                end,
114                scissor: sc,
115            });
116        }
117    };
118
119    for cmd in list.iter() {
120        // ── Clip-stack management (always processed, never culled) ─────────
121        match cmd {
122            DrawCommand::PushClip { rect } => {
123                flush(
124                    &mut segments,
125                    segment_start,
126                    verts.len() as u32,
127                    current_scissor,
128                );
129                stack.push(ClipRect::new(
130                    rect.left(),
131                    rect.top(),
132                    rect.width(),
133                    rect.height(),
134                ));
135                current_scissor = scissor_from_stack(&stack, viewport_w, viewport_h);
136                segment_start = verts.len() as u32;
137                continue;
138            }
139            DrawCommand::PopClip => {
140                flush(
141                    &mut segments,
142                    segment_start,
143                    verts.len() as u32,
144                    current_scissor,
145                );
146                stack.pop();
147                current_scissor = scissor_from_stack(&stack, viewport_w, viewport_h);
148                segment_start = verts.len() as u32;
149                continue;
150            }
151            _ => {}
152        }
153
154        // ── Visibility culling ────────────────────────────────────────────
155        // Skip commands whose bounding rect lies entirely outside the
156        // current scissor rect.  When there is no active scissor (full
157        // viewport), nothing is culled.  When the bounding rect is
158        // unknown (None), we render conservatively (no cull).
159        if let Some(scissor) = current_scissor {
160            // Degenerate scissors (zero area) cull everything.
161            if scissor[2] == 0 || scissor[3] == 0 {
162                continue;
163            }
164            if let Some(bounds) = cmd_bounds(cmd) {
165                let scissor_rect = scissor_to_rect(scissor);
166                if !rects_intersect(&bounds, &scissor_rect) {
167                    continue;
168                }
169            }
170        }
171
172        // ── Geometry emission ─────────────────────────────────────────────
173        match cmd {
174            // Clip ops already handled above via `continue`.
175            DrawCommand::PushClip { .. } | DrawCommand::PopClip => {}
176
177            DrawCommand::FillRect { rect, color } => {
178                push_rect_quad(
179                    &mut verts,
180                    rect.left(),
181                    rect.top(),
182                    rect.width(),
183                    rect.height(),
184                    *color,
185                );
186            }
187            DrawCommand::StrokeRect {
188                rect,
189                thickness,
190                color,
191            } => {
192                emit_stroke_rect(
193                    &mut verts,
194                    rect.left(),
195                    rect.top(),
196                    rect.width(),
197                    rect.height(),
198                    *thickness,
199                    *color,
200                );
201            }
202            DrawCommand::FillRoundedRect {
203                rect,
204                radius,
205                color,
206            } => {
207                push_rounded_rect_quad(
208                    &mut verts,
209                    rect.left(),
210                    rect.top(),
211                    rect.width(),
212                    rect.height(),
213                    *radius,
214                    *color,
215                );
216            }
217            DrawCommand::FillRoundedRectPerCorner { rect, radii, color } => {
218                push_rounded_rect_per_corner_quad(
219                    &mut verts,
220                    rect.left(),
221                    rect.top(),
222                    rect.width(),
223                    rect.height(),
224                    *radii,
225                    *color,
226                );
227            }
228            DrawCommand::FillCircle {
229                center,
230                radius,
231                color,
232            } => {
233                push_circle_quad(&mut verts, center.x, center.y, *radius, *color);
234            }
235            DrawCommand::FillEllipse {
236                center,
237                rx,
238                ry,
239                color,
240            } => {
241                push_ellipse_quad(&mut verts, center.x, center.y, *rx, *ry, *color);
242            }
243            DrawCommand::Line { from, to, color } => {
244                push_line_quad(
245                    &mut verts,
246                    LineQuadParams {
247                        from_x: from.x,
248                        from_y: from.y,
249                        to_x: to.x,
250                        to_y: to.y,
251                        half_width: 0.5,
252                        color: *color,
253                        aa_smooth: false,
254                    },
255                );
256            }
257            DrawCommand::LineAa { from, to, color } => {
258                push_line_quad(
259                    &mut verts,
260                    LineQuadParams {
261                        from_x: from.x,
262                        from_y: from.y,
263                        to_x: to.x,
264                        to_y: to.y,
265                        half_width: 0.5,
266                        color: *color,
267                        aa_smooth: true,
268                    },
269                );
270            }
271            DrawCommand::LineThick {
272                from,
273                to,
274                width,
275                color,
276            } => {
277                push_line_quad(
278                    &mut verts,
279                    LineQuadParams {
280                        from_x: from.x,
281                        from_y: from.y,
282                        to_x: to.x,
283                        to_y: to.y,
284                        half_width: width * 0.5,
285                        color: *color,
286                        aa_smooth: true,
287                    },
288                );
289            }
290            DrawCommand::LineDashed {
291                from,
292                to,
293                dash_len,
294                gap_len,
295                color,
296            } => {
297                emit_dashed_line(
298                    &mut verts,
299                    DashedLineParams {
300                        x0: from.x,
301                        y0: from.y,
302                        x1: to.x,
303                        y1: to.y,
304                        dash_len: *dash_len,
305                        gap_len: *gap_len,
306                        color: *color,
307                    },
308                );
309            }
310            DrawCommand::FillPath { path, color } => {
311                tessellate_fill(&mut verts, path, *color);
312            }
313            DrawCommand::StrokePath { path, style, color } => {
314                tessellate_stroke(&mut verts, path, style, *color);
315            }
316            DrawCommand::LinearGradient {
317                rect,
318                start,
319                end,
320                stops,
321            } => {
322                if let Some(gd) = build_gradient_draw_linear(LinearGradientParams {
323                    x: rect.left(),
324                    y: rect.top(),
325                    w: rect.width(),
326                    h: rect.height(),
327                    sx: start.x,
328                    sy: start.y,
329                    ex: end.x,
330                    ey: end.y,
331                    stops,
332                    scissor: current_scissor,
333                }) {
334                    gradient_draws.push(gd);
335                }
336            }
337            DrawCommand::RadialGradient {
338                rect,
339                center,
340                radius,
341                stops,
342            } => {
343                if let Some(gd) = build_gradient_draw_radial(RadialGradientParams {
344                    x: rect.left(),
345                    y: rect.top(),
346                    w: rect.width(),
347                    h: rect.height(),
348                    cx: center.x,
349                    cy: center.y,
350                    radius: *radius,
351                    stops,
352                    scissor: current_scissor,
353                }) {
354                    gradient_draws.push(gd);
355                }
356            }
357            DrawCommand::Image {
358                image,
359                dest,
360                filter,
361            } => {
362                let mut tex_verts = Vec::new();
363                push_textured_quad(
364                    &mut tex_verts,
365                    TexQuadParams {
366                        x: dest.left(),
367                        y: dest.top(),
368                        w: dest.width(),
369                        h: dest.height(),
370                        u0: 0.0,
371                        v0: 0.0,
372                        u1: 1.0,
373                        v1: 1.0,
374                        tint: [1.0, 1.0, 1.0, 1.0],
375                    },
376                );
377                textured_draws.push(TexturedDraw {
378                    verts: tex_verts,
379                    image: image.clone(),
380                    filter: *filter,
381                    scissor: current_scissor,
382                });
383            }
384            DrawCommand::NineSlice {
385                image,
386                dest,
387                insets,
388            } => {
389                let mut tex_verts = Vec::new();
390                push_nine_slice_quads(
391                    &mut tex_verts,
392                    [dest.left(), dest.top(), dest.width(), dest.height()],
393                    image.width,
394                    image.height,
395                    *insets,
396                    [1.0, 1.0, 1.0, 1.0],
397                );
398                textured_draws.push(TexturedDraw {
399                    verts: tex_verts,
400                    image: image.clone(),
401                    filter: ImageFilter::Nearest,
402                    scissor: current_scissor,
403                });
404            }
405            DrawCommand::BackdropBlur { rect, blur_radius } => {
406                // Record a backdrop blur request.  The renderer will execute
407                // this as a copy-from-colour-texture + blur + write-back pass
408                // between the solid and the next pass.
409                backdrop_blur_draws.push(BackdropBlurDraw {
410                    rect: [rect.left(), rect.top(), rect.width(), rect.height()],
411                    blur_radius: *blur_radius,
412                });
413            }
414            // SetBlendMode, BoxShadow: BoxShadow is handled by shadow::collect_shadows;
415            // SetBlendMode is informational for now (blend mode plumbing is
416            // handled by BlendPipelineSet in the renderer).
417            // DrawText: deferred (requires glyph atlas / oxiui-text).
418            _ => {}
419        }
420    }
421
422    flush(
423        &mut segments,
424        segment_start,
425        verts.len() as u32,
426        current_scissor,
427    );
428    (
429        verts,
430        segments,
431        gradient_draws,
432        textured_draws,
433        backdrop_blur_draws,
434    )
435}
436
437// ── Visibility culling helpers ────────────────────────────────────────────────
438
439/// Compute a conservative bounding [`Rect`] for a draw command, or `None` for
440/// commands that have no geometric extent (clip stack ops) or for which a
441/// conservative bound cannot be computed cheaply (wildcard future variants).
442///
443/// The returned rect is used for scissor-intersection culling only; it need
444/// not be tight, but it must be a *superset* of the actual drawn pixels.
445pub(crate) fn cmd_bounds(cmd: &DrawCommand) -> Option<Rect> {
446    match cmd {
447        DrawCommand::FillRect { rect, .. }
448        | DrawCommand::StrokeRect { rect, .. }
449        | DrawCommand::FillRoundedRect { rect, .. }
450        | DrawCommand::FillRoundedRectPerCorner { rect, .. }
451        | DrawCommand::LinearGradient { rect, .. }
452        | DrawCommand::RadialGradient { rect, .. }
453        | DrawCommand::Image { dest: rect, .. }
454        | DrawCommand::NineSlice { dest: rect, .. }
455        | DrawCommand::DrawText { rect, .. } => Some(*rect),
456
457        DrawCommand::FillCircle { center, radius, .. } => Some(Rect::new(
458            center.x - radius,
459            center.y - radius,
460            radius * 2.0,
461            radius * 2.0,
462        )),
463
464        DrawCommand::FillEllipse { center, rx, ry, .. } => {
465            Some(Rect::new(center.x - rx, center.y - ry, rx * 2.0, ry * 2.0))
466        }
467
468        DrawCommand::Line { from, to, .. } | DrawCommand::LineAa { from, to, .. } => {
469            Some(rect_from_points(*from, *to))
470        }
471
472        DrawCommand::LineThick {
473            from, to, width, ..
474        } => {
475            let r = rect_from_points(*from, *to);
476            Some(Rect::new(
477                r.left() - width,
478                r.top() - width,
479                r.width() + width * 2.0,
480                r.height() + width * 2.0,
481            ))
482        }
483
484        DrawCommand::LineDashed { from, to, .. } => Some(rect_from_points(*from, *to)),
485
486        DrawCommand::FillPath { path, .. } => path.bounds(),
487
488        DrawCommand::StrokePath { path, style, .. } => path.bounds().map(|b| {
489            let pad = style.width / 2.0;
490            Rect::new(
491                b.left() - pad,
492                b.top() - pad,
493                b.width() + style.width,
494                b.height() + style.width,
495            )
496        }),
497
498        // Clip ops have no draw geometry.
499        DrawCommand::PushClip { .. } | DrawCommand::PopClip => None,
500
501        // BoxShadow is handled by collect_shadows/render_shadows before
502        // build_geometry runs; it does not emit solid vertices here, so we
503        // return None so it falls through to the `_ => {}` wildcard below.
504        DrawCommand::BoxShadow { .. } => None,
505
506        // Forward-compatibility: unknown variants are rendered conservatively.
507        _ => None,
508    }
509}
510
511/// Build a bounding rect that covers two points (at least 1×1 in each
512/// dimension so degenerate lines don't collapse to zero area).
513pub(crate) fn rect_from_points(
514    a: oxiui_core::geometry::Point,
515    b: oxiui_core::geometry::Point,
516) -> Rect {
517    let x = a.x.min(b.x);
518    let y = a.y.min(b.y);
519    let w = (a.x - b.x).abs().max(1.0);
520    let h = (a.y - b.y).abs().max(1.0);
521    Rect::new(x, y, w, h)
522}
523
524/// Test whether two axis-aligned rectangles overlap (at least one shared
525/// pixel column and row).  A zero-area overlap (tangent border) is NOT
526/// considered an intersection — it returns `false`.
527pub(crate) fn rects_intersect(a: &Rect, b: &Rect) -> bool {
528    a.left() < b.left() + b.width()
529        && a.left() + a.width() > b.left()
530        && a.top() < b.top() + b.height()
531        && a.top() + a.height() > b.top()
532}
533
534/// Convert a hardware scissor `[x, y, w, h]` back to a floating-point
535/// [`Rect`] for intersection tests.
536pub(crate) fn scissor_to_rect(s: [u32; 4]) -> Rect {
537    Rect::new(s[0] as f32, s[1] as f32, s[2] as f32, s[3] as f32)
538}
539
540// ── Geometry helpers ──────────────────────────────────────────────────────────
541
542pub(crate) fn emit_stroke_rect(
543    out: &mut Vec<Vertex>,
544    x: f32,
545    y: f32,
546    w: f32,
547    h: f32,
548    t: f32,
549    color: Color,
550) {
551    push_rect_quad(out, x, y, w, t, color);
552    push_rect_quad(out, x, y + h - t, w, t, color);
553    push_rect_quad(out, x, y + t, t, h - 2.0 * t, color);
554    push_rect_quad(out, x + w - t, y + t, t, h - 2.0 * t, color);
555}
556
557pub(crate) struct DashedLineParams {
558    pub(crate) x0: f32,
559    pub(crate) y0: f32,
560    pub(crate) x1: f32,
561    pub(crate) y1: f32,
562    pub(crate) dash_len: f32,
563    pub(crate) gap_len: f32,
564    pub(crate) color: Color,
565}
566
567pub(crate) fn emit_dashed_line(out: &mut Vec<Vertex>, p: DashedLineParams) {
568    let DashedLineParams {
569        x0,
570        y0,
571        x1,
572        y1,
573        dash_len,
574        gap_len,
575        color,
576    } = p;
577    let dx = x1 - x0;
578    let dy = y1 - y0;
579    let total = (dx * dx + dy * dy).sqrt();
580    if total < 1e-6 || dash_len <= 0.0 {
581        return;
582    }
583    let ux = dx / total;
584    let uy = dy / total;
585    let period = dash_len + gap_len.max(0.0);
586    if period < 1e-6 {
587        return;
588    }
589    let mut t = 0.0_f32;
590    while t < total {
591        let end = (t + dash_len).min(total);
592        push_line_quad(
593            out,
594            LineQuadParams {
595                from_x: x0 + ux * t,
596                from_y: y0 + uy * t,
597                to_x: x0 + ux * end,
598                to_y: y0 + uy * end,
599                half_width: 0.5,
600                color,
601                aa_smooth: false,
602            },
603        );
604        t += period;
605    }
606}
607
608pub(crate) struct LinearGradientParams<'a> {
609    pub(crate) x: f32,
610    pub(crate) y: f32,
611    pub(crate) w: f32,
612    pub(crate) h: f32,
613    pub(crate) sx: f32,
614    pub(crate) sy: f32,
615    pub(crate) ex: f32,
616    pub(crate) ey: f32,
617    pub(crate) stops: &'a [GradientStop],
618    pub(crate) scissor: Option<[u32; 4]>,
619}
620
621pub(crate) fn build_gradient_draw_linear(p: LinearGradientParams<'_>) -> Option<GradientDraw> {
622    let LinearGradientParams {
623        x,
624        y,
625        w,
626        h,
627        sx,
628        sy,
629        ex,
630        ey,
631        stops,
632        scissor,
633    } = p;
634    let uniforms = build_gradient_uniforms(0, [sx, sy], [ex, ey], 0.0, stops)?;
635    let mut verts = Vec::new();
636    push_gradient_quad(&mut verts, x, y, w, h);
637    Some(GradientDraw {
638        verts,
639        uniforms,
640        scissor,
641    })
642}
643
644pub(crate) struct RadialGradientParams<'a> {
645    pub(crate) x: f32,
646    pub(crate) y: f32,
647    pub(crate) w: f32,
648    pub(crate) h: f32,
649    pub(crate) cx: f32,
650    pub(crate) cy: f32,
651    pub(crate) radius: f32,
652    pub(crate) stops: &'a [GradientStop],
653    pub(crate) scissor: Option<[u32; 4]>,
654}
655
656pub(crate) fn build_gradient_draw_radial(p: RadialGradientParams<'_>) -> Option<GradientDraw> {
657    let RadialGradientParams {
658        x,
659        y,
660        w,
661        h,
662        cx,
663        cy,
664        radius,
665        stops,
666        scissor,
667    } = p;
668    let uniforms = build_gradient_uniforms(1, [cx, cy], [0.0, 0.0], radius, stops)?;
669    let mut verts = Vec::new();
670    push_gradient_quad(&mut verts, x, y, w, h);
671    Some(GradientDraw {
672        verts,
673        uniforms,
674        scissor,
675    })
676}
677
678pub(crate) fn build_gradient_uniforms(
679    gradient_type: u32,
680    p0: [f32; 2],
681    p1: [f32; 2],
682    radius: f32,
683    stops: &[GradientStop],
684) -> Option<GradientUniforms> {
685    if stops.is_empty() {
686        return None;
687    }
688    let count = stops.len().min(MAX_GRADIENT_STOPS);
689    let mut stop_offsets = [[0.0f32; 4]; MAX_GRADIENT_STOPS];
690    let mut stop_colors = [[0.0f32; 4]; MAX_GRADIENT_STOPS];
691    for (i, s) in stops.iter().take(count).enumerate() {
692        stop_offsets[i] = [s.offset, 0.0, 0.0, 0.0];
693        stop_colors[i] = [
694            s.color.0 as f32 / 255.0,
695            s.color.1 as f32 / 255.0,
696            s.color.2 as f32 / 255.0,
697            s.color.3 as f32 / 255.0,
698        ];
699    }
700    Some(GradientUniforms {
701        p0,
702        p1,
703        radius,
704        gradient_type,
705        stop_count: count as u32,
706        _pad: 0,
707        stop_offsets,
708        stop_colors,
709    })
710}