feather_ui/
graphics.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2025 Fundament Research Institute <https://fundament.institute>
3
4use std::collections::HashMap;
5
6use crate::render::atlas::{ATLAS_FORMAT, Atlas, AtlasKind};
7use crate::render::compositor::Compositor;
8use crate::render::shape::Shape;
9use crate::render::{atlas, compositor};
10use crate::resource::{Loader, Location, MAX_VARIANCE};
11use crate::{Error, Mat4x4, render};
12use guillotiere::AllocId;
13use parking_lot::RwLock;
14use smallvec::SmallVec;
15use std::any::TypeId;
16use std::sync::Arc;
17use swash::scale::ScaleContext;
18use wgpu::{PipelineLayout, ShaderModule};
19use winit::window::CursorIcon;
20
21// Points are specified as 72 per inch, and a scale factor of 1.0 corresponds to 96 DPI, so we multiply by the
22// ratio times the scaling factor.
23#[inline]
24pub fn point_to_pixel(pt: f32, scale_factor: f32) -> f32 {
25    pt * (72.0 / 96.0) * scale_factor // * text_scale_factor
26}
27
28pub type PipelineID = TypeId;
29
30#[derive_where::derive_where(Debug)]
31#[allow(clippy::type_complexity)]
32pub(crate) struct PipelineState {
33    layout: PipelineLayout,
34    shader: ShaderModule,
35    #[derive_where(skip)]
36    generator: Box<
37        dyn Fn(&PipelineLayout, &ShaderModule, &Driver) -> Box<dyn render::AnyPipeline>
38            + Send
39            + Sync,
40    >,
41}
42
43#[derive(Debug)]
44pub struct GlyphRegion {
45    pub offset: [i32; 2],
46    pub region: atlas::Region,
47}
48
49pub(crate) type GlyphCache = HashMap<cosmic_text::CacheKey, GlyphRegion>;
50
51/// Represents a particular realized instance of a resource on the GPU. This includes the target size, DPI, and
52/// whether mipmaps have been generated.
53#[derive(Debug)]
54pub struct ResourceInstance<'a> {
55    location: Result<Box<dyn Location>, &'a dyn Location>,
56    /// If finite, this is used for vector resources, which must care about DPI beyond simply changing their size.
57    dpi: f32,
58    /// If true, mipmaps should be generated for this resource because the user expects to resize it in realtime.
59    resizable: bool,
60}
61
62impl Clone for ResourceInstance<'static> {
63    fn clone(&self) -> Self {
64        Self {
65            location: self.location.clone(),
66            dpi: self.dpi,
67            resizable: self.resizable,
68        }
69    }
70}
71
72impl std::hash::Hash for ResourceInstance<'_> {
73    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
74        match &self.location {
75            Ok(l) => l.hash(state),
76            Err(l) => l.hash(state),
77        }
78        // This works because DPI is always non-zero
79        f32::to_bits(self.dpi).hash(state);
80        self.resizable.hash(state);
81    }
82}
83
84impl PartialEq for ResourceInstance<'_> {
85    fn eq(&self, other: &Self) -> bool {
86        let l = match &self.location {
87            Ok(l) => l.as_ref(),
88            Err(l) => *l,
89        };
90
91        let r = match &other.location {
92            Ok(l) => l.as_ref(),
93            Err(l) => *l,
94        };
95
96        *l == *r && self.dpi == other.dpi && self.resizable == other.resizable
97    }
98}
99
100// We don't put NaNs in our DPI float so this is fine.
101impl Eq for ResourceInstance<'_> {}
102
103// We want to share our device/adapter state across windows, but can't create it until we have at least one window,
104// so we store a weak reference to it in App and if all windows are dropped it'll also drop these, which is usually
105// sensible behavior.
106#[allow(clippy::type_complexity)]
107#[derive_where::derive_where(Debug)]
108pub struct Driver {
109    pub(crate) glyphs: RwLock<GlyphCache>,
110    pub(crate) prefetch: RwLock<HashMap<Box<dyn Location>, Box<dyn Loader>>>,
111    pub(crate) resources:
112        RwLock<HashMap<ResourceInstance<'static>, (SmallVec<[atlas::Region; 1]>, atlas::Size)>>,
113    pub(crate) locations:
114        RwLock<HashMap<Box<dyn Location>, SmallVec<[ResourceInstance<'static>; 1]>>>,
115    pub(crate) atlas: RwLock<Atlas>,
116    pub(crate) layer_atlas: [RwLock<Atlas>; 2],
117    pub(crate) layer_composite: [RwLock<compositor::Compositor>; 2],
118    pub(crate) shared: compositor::Shared,
119    pub(crate) pipelines: RwLock<HashMap<PipelineID, Box<dyn crate::render::AnyPipeline>>>,
120    pub(crate) registry: RwLock<HashMap<PipelineID, PipelineState>>,
121    pub(crate) queue: wgpu::Queue,
122    pub(crate) device: wgpu::Device,
123    pub(crate) adapter: wgpu::Adapter,
124    pub(crate) cursor: RwLock<CursorIcon>, // This is a convenient place to track our global expected cursor
125    #[derive_where(skip)]
126    pub(crate) swash_cache: RwLock<ScaleContext>,
127    pub(crate) font_system: RwLock<cosmic_text::FontSystem>,
128}
129
130impl Drop for Driver {
131    fn drop(&mut self) {
132        for (_, mut r) in self.glyphs.get_mut().drain() {
133            r.region.id = AllocId::deserialize(u32::MAX);
134        }
135
136        for (_, (regions, _)) in self.resources.get_mut().drain() {
137            for mut region in regions {
138                region.id = AllocId::deserialize(u32::MAX);
139            }
140        }
141    }
142}
143
144impl Driver {
145    pub async fn new(
146        weak: &mut std::sync::Weak<Self>,
147        instance: &wgpu::Instance,
148        surface: &wgpu::Surface<'static>,
149        on_driver: &mut Option<Box<dyn FnOnce(std::sync::Weak<Driver>) + 'static>>,
150    ) -> eyre::Result<Arc<Self>> {
151        if let Some(driver) = weak.upgrade() {
152            return Ok(driver);
153        }
154
155        let adapter = futures_lite::future::block_on(instance.request_adapter(
156            &wgpu::RequestAdapterOptions {
157                compatible_surface: Some(surface),
158                ..Default::default()
159            },
160        ))?;
161
162        // Create the logical device and command queue
163        let (device, queue) = adapter
164            .request_device(&wgpu::DeviceDescriptor {
165                label: Some("Feather UI wgpu Device"),
166                required_features: wgpu::Features::empty(),
167                required_limits: wgpu::Limits::default(),
168                memory_hints: wgpu::MemoryHints::MemoryUsage,
169                trace: wgpu::Trace::Off,
170            })
171            .await?;
172
173        let shared = compositor::Shared::new(&device);
174        let atlas = Atlas::new(&device, 512, AtlasKind::Primary);
175        let layer0 = Atlas::new(&device, 128, AtlasKind::Layer0);
176        let layer1 = Atlas::new(&device, 128, AtlasKind::Layer1);
177        let shape_shader = Shape::<0>::shader(&device);
178        let shape_pipeline = Shape::<0>::layout(&device);
179
180        let comp1 = Compositor::new(
181            &device,
182            &shared,
183            &atlas.view,
184            &layer1.view,
185            ATLAS_FORMAT,
186            true,
187        );
188        let comp2 = Compositor::new(
189            &device,
190            &shared,
191            &atlas.view,
192            &layer0.view,
193            ATLAS_FORMAT,
194            false,
195        );
196
197        let mut driver = Self {
198            adapter,
199            device,
200            queue,
201            swash_cache: ScaleContext::new().into(),
202            prefetch: HashMap::new().into(),
203            resources: HashMap::new().into(),
204            locations: HashMap::new().into(),
205            font_system: cosmic_text::FontSystem::new().into(),
206            cursor: CursorIcon::Default.into(),
207            pipelines: HashMap::new().into(),
208            glyphs: HashMap::new().into(),
209            registry: HashMap::new().into(),
210            shared,
211            atlas: atlas.into(),
212            layer_atlas: [layer0.into(), layer1.into()],
213            layer_composite: [comp1.into(), comp2.into()],
214        };
215
216        driver.register_pipeline::<Shape<0>>(
217            shape_pipeline.clone(),
218            shape_shader.clone(),
219            Shape::<0>::create,
220        );
221        driver.register_pipeline::<Shape<1>>(
222            shape_pipeline.clone(),
223            shape_shader.clone(),
224            Shape::<1>::create,
225        );
226        driver.register_pipeline::<Shape<2>>(
227            shape_pipeline.clone(),
228            shape_shader.clone(),
229            Shape::<2>::create,
230        );
231        driver.register_pipeline::<Shape<3>>(
232            shape_pipeline.clone(),
233            shape_shader.clone(),
234            Shape::<3>::create,
235        );
236
237        let driver = Arc::new(driver);
238        *weak = Arc::downgrade(&driver);
239
240        if let Some(f) = on_driver.take() {
241            f(weak.clone());
242        }
243        Ok(driver)
244    }
245
246    pub fn register_pipeline<T: 'static>(
247        &mut self,
248        layout: PipelineLayout,
249        shader: ShaderModule,
250        generator: impl Fn(&PipelineLayout, &ShaderModule, &Self) -> Box<dyn render::AnyPipeline>
251        + Send
252        + Sync
253        + 'static,
254    ) {
255        self.registry.write().insert(
256            TypeId::of::<T>(),
257            PipelineState {
258                layout,
259                shader,
260                generator: Box::new(generator),
261            },
262        );
263    }
264
265    /// Allows replacing the shader in a pipeline, for hot-reloading.
266    pub fn reload_pipeline<T: 'static>(&self, shader: ShaderModule) {
267        let id = TypeId::of::<T>();
268        let mut registry = self.registry.write();
269        let pipeline = registry
270            .get_mut(&id)
271            .expect("Tried to reload unregistered pipeline!");
272        pipeline.shader = shader;
273        self.pipelines.write().remove(&id);
274    }
275    pub fn with_pipeline<T: crate::render::Pipeline + 'static>(&self, f: impl FnOnce(&mut T)) {
276        let id = TypeId::of::<T>();
277
278        // We can't use the result of this because it makes the lifetimes weird
279        if self.pipelines.read().get(&id).is_none() {
280            let PipelineState {
281                generator,
282                layout,
283                shader,
284            } = &self.registry.read()[&id];
285
286            self.pipelines
287                .write()
288                .insert(id, generator(layout, shader, self));
289        }
290
291        f(
292            (self.pipelines.write().get_mut(&id).unwrap().as_mut() as &mut dyn std::any::Any)
293                .downcast_mut()
294                .unwrap(),
295        );
296    }
297
298    pub fn prefetch(&self, location: &dyn Location) -> Result<(), Error> {
299        let mut resources = self.prefetch.write();
300
301        if !resources.contains_key(location) {
302            resources.insert(dyn_clone::clone_box(location), location.fetch()?);
303        }
304        Ok(())
305    }
306
307    /// This function is called during layout, outside of a render pass, which allows the texture atlas to be
308    /// immediately resized to accomdate the new resource. As a result, it assumes you don't need the region,
309    /// only the final intrinsic size.
310    pub fn load_and_resize(
311        &self,
312        location: &dyn Location,
313        size: atlas::Size,
314        dpi: f32,
315        resize: bool,
316    ) -> Result<atlas::Size, Error> {
317        let mut uvsize = atlas::Size::zero();
318        match self.load(location, size, dpi, resize, |r| {
319            uvsize = r.uv.size();
320            Ok(())
321        }) {
322            Err(Error::ResizeTextureAtlas(layers, kind)) => {
323                // Resize the texture atlas with the requested number of layers (the extent has already been changed)
324                match kind {
325                    AtlasKind::Primary => self.atlas.write(),
326                    AtlasKind::Layer0 => self.layer_atlas[0].write(),
327                    AtlasKind::Layer1 => self.layer_atlas[1].write(),
328                }
329                .resize(&self.device, &self.queue, layers);
330                self.load_and_resize(location, size, dpi, resize) // Retry load
331            }
332            Err(e) => Err(e),
333            Ok(_) => Ok(uvsize),
334        }
335    }
336
337    pub fn load(
338        &self,
339        location: &dyn Location,
340        mut size: atlas::Size,
341        dpi: f32,
342        resize: bool,
343        mut f: impl FnMut(&atlas::Region) -> Result<(), Error>,
344    ) -> Result<(), Error> {
345        use crate::resource;
346
347        if let Some((regions, native_size)) = self.resources.read().get(&ResourceInstance {
348            location: Err(location),
349            dpi: f32::INFINITY,
350            resizable: resize,
351        }) {
352            size = resource::fill_size(size, *native_size);
353
354            // Check if our requested size is within reasonable resize range - slightly bigger or smaller is fine, and
355            // much smaller is fine if we have access to mipmaps.
356            for r in regions {
357                if r.uv.size() == size
358                    || (r.uv.area() >= resource::MIN_AREA
359                        && resource::within_variance(size.width, r.uv.width(), MAX_VARIANCE)
360                        && resource::within_variance(size.height, r.uv.height(), MAX_VARIANCE))
361                    || (resize && size.width <= r.uv.width() && size.height < r.uv.height())
362                {
363                    return f(r);
364                }
365            }
366        }
367
368        // Check for a prefetched resource
369        let (region, native) = {
370            let loader;
371            let reader = self.prefetch.read();
372            let refload = if let Some(res) = reader.get(location) {
373                res
374            } else {
375                loader = location.fetch()?;
376                &loader
377            };
378            let (raw, dim) = refload.preload(size, dpi)?;
379            refload.load(self, (raw, dim), resize)?
380        };
381
382        let key = ResourceInstance {
383            location: Ok(dyn_clone::clone_box(location)),
384            dpi: f32::INFINITY,
385            resizable: resize,
386        };
387
388        self.locations
389            .write()
390            .entry(dyn_clone::clone_box(location))
391            .and_modify(|x| x.push(key.clone()))
392            .or_insert(SmallVec::from_buf([key.clone()]));
393
394        if let Some((entry, _)) = self.resources.write().get_mut(&key) {
395            entry.push(region);
396            f(entry.last().as_ref().ok_or(Error::InternalFailure)?)
397        } else {
398            f(&self
399                .resources
400                .write()
401                .entry(key)
402                .insert_entry((SmallVec::from_buf([region]), native))
403                .get()
404                .0[0])
405        }
406    }
407
408    /// Removes all loaded instances of a particular resource location. Generally used for hotloading resources that changed on disk.
409    pub fn evict(&self, location: &dyn Location) {
410        if let Some(instances) = self.locations.read().get(location) {
411            for instance in instances {
412                if let Some((regions, _)) = self.resources.write().remove(instance) {
413                    for mut region in regions {
414                        self.atlas.write().destroy(&mut region);
415                    }
416                }
417            }
418        }
419    }
420}
421
422static_assertions::assert_impl_all!(Driver: Send, Sync);
423
424static_assertions::const_assert!(size_of::<Mat4x4>() == size_of::<[f32; 16]>());
425
426// This maps x and y to the viewpoint size, maps input_z from [n,f] to [0,1], and sets
427// output_w = input_z for perspective. Requires input_w = 1
428pub fn mat4_proj(x: f32, y: f32, w: f32, h: f32, n: f32, f: f32) -> Mat4x4 {
429    Mat4x4::from_arrays([
430        [2.0 / w, 0.0, 0.0, 0.0],
431        [0.0, 2.0 / h, 0.0, 0.0],
432        [0.0, 0.0, 1.0 / (f - n), 1.0],
433        [-(2.0 * x + w) / w, -(2.0 * y + h) / h, -n / (f - n), 0.0],
434    ])
435}
436
437// Orthographic projection matrix
438pub fn mat4_ortho(x: f32, y: f32, w: f32, h: f32, n: f32, f: f32) -> Mat4x4 {
439    Mat4x4::from_arrays([
440        [2.0 / w, 0.0, 0.0, 0.0],
441        [0.0, 2.0 / h, 0.0, 0.0],
442        [0.0, 0.0, -2.0 / (f - n), 0.0],
443        [
444            -(2.0 * x + w) / w,
445            -(2.0 * y + h) / h,
446            (f + n) / (f - n),
447            1.0,
448        ],
449    ])
450}
451
452macro_rules! gen_from_array {
453    ($s:path, $t:path, $i:literal) => {
454        impl From<[$t; $i]> for $s {
455            fn from(value: [$t; $i]) -> Self {
456                Self(value)
457            }
458        }
459        impl From<&[$t; $i]> for $s {
460            fn from(value: &[$t; $i]) -> Self {
461                Self(*value)
462            }
463        }
464    };
465}
466
467#[repr(C, align(8))]
468#[derive(Clone, Copy, Debug, Default, PartialEq, bytemuck::NoUninit)]
469pub struct Vec2f(pub(crate) [f32; 2]);
470
471gen_from_array!(Vec2f, f32, 2);
472
473#[repr(C, align(16))]
474#[derive(Clone, Copy, Debug, Default, PartialEq, bytemuck::NoUninit)]
475pub struct Vec4f(pub(crate) [f32; 4]);
476
477gen_from_array!(Vec4f, f32, 4);
478
479#[repr(C, align(8))]
480#[derive(Clone, Copy, Debug, Default, PartialEq, bytemuck::NoUninit)]
481pub struct Vec2i(pub(crate) [i32; 2]);
482
483gen_from_array!(Vec2i, i32, 2);
484
485#[repr(C, align(16))]
486#[derive(Clone, Copy, Debug, Default, PartialEq, bytemuck::NoUninit)]
487pub struct Vec4i(pub(crate) [i32; 4]);
488
489gen_from_array!(Vec4i, i32, 4);