ailloli_ui_render_wgpu/
frame_prep.rs

1//! Frame resource preparation (single-pass compositing).
2//!
3//! [`PreparedResources::prepare`] is the **only** step that touches the GPU /
4//! atlas / icon cache before the per-frame plan is built. It walks the frame's
5//! layers, ensures every text glyph is rasterized + pinned in the atlas, and
6//! every icon is allocated in the icon cache. The resulting [`PreparedResources`]
7//! is then consumed by the **pure-CPU** [`crate::frame_plan::FrameRenderPlan::build_cpu`]
8//! to assemble vertex arenas and batches without any device/queue access.
9//!
10//! This split keeps the plan testable without a GPU and matches the single-pass compositing
11//! invariant "no `device.create_*` / `queue.write_*` between `begin_render_pass`
12//! and `drop(rpass)`".
13
14use std::collections::{HashMap, HashSet};
15use std::sync::Arc;
16
17use ailloli_ui_core::math::Scale;
18use ailloli_ui_runtime::DrawCmd;
19
20use crate::icons::{IconCache, IconKey};
21use crate::renderer::LayerPass;
22use crate::text::{Glyph, GlyphKey, TextAtlas};
23
24/// Resources pinned / allocated for the upcoming frame.
25///
26/// - `glyphs` : every glyph requested by any `DrawCmd::Text` in the frame's
27///   layers, looked up (or rasterized + uploaded) and pinned in the text atlas
28///   for the duration of the frame.
29/// - `icons`  : every icon requested by any `DrawCmd::Image`, materialized in
30///   the icon cache (bind group + texture).
31///
32/// `build_cpu` later reads these maps to emit `PlannedBatch`es with the
33/// correct `TextureBindKind`.
34///
35/// # Examples
36///
37/// ```
38/// use ailloli_ui_render_wgpu::PreparedResources;
39/// let prepared = PreparedResources::default();
40/// assert!(prepared.glyphs.is_empty() && prepared.icons.is_empty());
41/// ```
42#[derive(Debug, Default)]
43pub struct PreparedResources {
44    /// Pinned glyphs keyed by face, glyph, physical size, and DPR bucket.
45    ///
46    /// Each value is `(atlas_page, glyph_metrics)`.
47    pub glyphs: HashMap<GlyphKey, (u8, Glyph)>,
48    /// Exact icon keys uploaded or found in the persistent icon cache.
49    pub icons: HashSet<IconKey>,
50}
51
52impl PreparedResources {
53    /// Walks `layers` and prepares atlas glyphs + icon cache entries.
54    ///
55    /// Touches `device`, `queue`, `atlas` and `icon_cache`. Callers must have
56    /// already called `atlas.start_frame()` (Renderer keeps this contract).
57    /// Physical glyph sizes are rounded and clamped to `8..=128`; icon sizes to
58    /// `8..=256`; the DPR bucket is `round(dpr * 100)` clamped to
59    /// `1..=u16::MAX`. Missing face blobs increment atlas diagnostics and omit
60    /// that glyph rather than panicking.
61    ///
62    /// # Examples
63    ///
64    /// ```no_run
65    /// use std::{collections::HashMap, sync::Arc};
66    /// use ailloli_ui_core::math::Scale;
67    /// use ailloli_ui_render_wgpu::{frame_prep::PreparedResources,
68    ///     icons::IconCache, text::TextAtlas};
69    /// fn prepare(atlas: &mut TextAtlas, icons: &mut IconCache,
70    ///     device: &wgpu::Device, queue: &wgpu::Queue,
71    ///     layout: &wgpu::BindGroupLayout) -> PreparedResources {
72    ///     atlas.start_frame();
73    ///     let faces: HashMap<u64, Arc<[u8]>> = HashMap::new();
74    ///     PreparedResources::prepare(&[], Scale::new(1.0), atlas, icons,
75    ///         device, queue, layout, &faces)
76    /// }
77    /// ```
78    #[allow(clippy::too_many_arguments)]
79    pub fn prepare(
80        layers: &[LayerPass<'_>],
81        scale: Scale,
82        atlas: &mut TextAtlas,
83        icon_cache: &mut IconCache,
84        device: &wgpu::Device,
85        queue: &wgpu::Queue,
86        texture_bind_group_layout: &wgpu::BindGroupLayout,
87        face_blobs: &HashMap<u64, Arc<[u8]>>,
88    ) -> Self {
89        let mut glyphs: HashMap<GlyphKey, (u8, Glyph)> = HashMap::new();
90        let mut icons: HashSet<IconKey> = HashSet::new();
91        let scale_100 = (scale.dpr * 100.0).round().clamp(1.0, u16::MAX as f32) as u16;
92
93        for layer in layers {
94            for cmd in layer.cmds {
95                match cmd {
96                    DrawCmd::Text(dt) => {
97                        for gi in dt.layout.glyphs() {
98                            let physical_px_size = ((gi.px_size as f32) * scale.dpr).round();
99                            let key = GlyphKey {
100                                face_id: gi.face_id,
101                                font_index: gi.font_index,
102                                px_size: physical_px_size.clamp(8.0, 128.0) as u16,
103                                glyph_id: gi.glyph_id,
104                                scale_100,
105                            };
106                            if glyphs.contains_key(&key) {
107                                // Already pinned via earlier layer this frame.
108                                continue;
109                            }
110                            let Some(blob) = face_blobs.get(&gi.face_id) else {
111                                atlas.record_missing_face();
112                                continue;
113                            };
114                            if let Some(entry) = atlas.get_or_rasterize_pinned(
115                                device,
116                                queue,
117                                texture_bind_group_layout,
118                                key,
119                                blob.as_ref(),
120                            ) {
121                                glyphs.insert(key, entry);
122                            }
123                        }
124                    }
125                    DrawCmd::Image(img) => {
126                        let physical_px_size = img.rect.w.max(img.rect.h) * scale.dpr;
127                        let key = IconKey {
128                            icon: img.icon.clone(),
129                            px_size: physical_px_size.round().clamp(8.0, 256.0) as u16,
130                            scale_100,
131                        };
132                        if icons.contains(&key) {
133                            continue;
134                        }
135                        let _ = icon_cache.get_or_create(
136                            device,
137                            queue,
138                            texture_bind_group_layout,
139                            &key,
140                        );
141                        icons.insert(key);
142                    }
143                    DrawCmd::Rect(_)
144                    | DrawCmd::RRect(_)
145                    | DrawCmd::Border(_)
146                    | DrawCmd::BoxShadow(_)
147                    | DrawCmd::RingProgress(_)
148                    | DrawCmd::Polyline(_) => {
149                        // Solid primitives don't need GPU resources beyond the
150                        // pipelines + clip bindings, which the renderer owns.
151                    }
152                }
153            }
154        }
155
156        Self { glyphs, icons }
157    }
158}