feather_ui/
graphics.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2025 Fundament Software SPC <https://fundament.software>
3
4use std::collections::HashMap;
5
6use crate::render;
7use crate::render::atlas::ATLAS_FORMAT;
8use crate::render::{atlas, compositor};
9use guillotiere::AllocId;
10use parking_lot::RwLock;
11use std::any::TypeId;
12use std::sync::Arc;
13use swash::scale::ScaleContext;
14use ultraviolet::{Mat4, Vec2, Vec4};
15use wgpu::{PipelineLayout, ShaderModule};
16use winit::window::CursorIcon;
17
18// Points are specified as 72 per inch, and a scale factor of 1.0 corresponds to 96 DPI, so we multiply by the
19// ratio times the scaling factor.
20#[inline]
21pub fn point_to_pixel(pt: f32, scale_factor: f32) -> f32 {
22    pt * (72.0 / 96.0) * scale_factor // * text_scale_factor
23}
24
25#[inline]
26pub fn pixel_to_vec(p: winit::dpi::PhysicalPosition<f32>) -> Vec2 {
27    Vec2::new(p.x, p.y)
28}
29
30pub type PipelineID = TypeId;
31
32#[derive_where::derive_where(Debug)]
33#[allow(clippy::type_complexity)]
34pub(crate) struct PipelineState {
35    layout: PipelineLayout,
36    shader: ShaderModule,
37    #[derive_where(skip)]
38    generator: Box<
39        dyn Fn(&PipelineLayout, &ShaderModule, &Driver) -> Box<dyn render::AnyPipeline>
40            + Send
41            + Sync,
42    >,
43}
44
45#[derive(Debug)]
46pub struct GlyphRegion {
47    pub offset: [i32; 2],
48    pub region: atlas::Region,
49}
50
51pub(crate) type GlyphCache = HashMap<cosmic_text::CacheKey, GlyphRegion>;
52
53// We want to share our device/adapter state across windows, but can't create it until we have at least one window,
54// so we store a weak reference to it in App and if all windows are dropped it'll also drop these, which is usually
55// sensible behavior.
56#[derive_where::derive_where(Debug)]
57pub struct Driver {
58    pub(crate) glyphs: RwLock<GlyphCache>,
59    pub(crate) atlas: RwLock<atlas::Atlas>,
60    pub(crate) layer_atlas: [RwLock<atlas::Atlas>; 2],
61    pub(crate) layer_composite: [RwLock<compositor::Compositor>; 2],
62    pub(crate) shared: compositor::Shared,
63    pub(crate) pipelines: RwLock<HashMap<PipelineID, Box<dyn crate::render::AnyPipeline>>>,
64    pub(crate) registry: RwLock<HashMap<PipelineID, PipelineState>>,
65    pub(crate) queue: wgpu::Queue,
66    pub(crate) device: wgpu::Device,
67    pub(crate) adapter: wgpu::Adapter,
68    pub(crate) cursor: RwLock<CursorIcon>, // This is a convenient place to track our global expected cursor
69    #[derive_where(skip)]
70    pub(crate) swash_cache: RwLock<ScaleContext>,
71    pub(crate) font_system: RwLock<cosmic_text::FontSystem>,
72}
73
74impl Drop for Driver {
75    fn drop(&mut self) {
76        for (_, mut r) in self.glyphs.get_mut().drain() {
77            r.region.id = AllocId::deserialize(u32::MAX);
78        }
79    }
80}
81
82impl Driver {
83    pub async fn new(
84        weak: &mut std::sync::Weak<Self>,
85        instance: &wgpu::Instance,
86        surface: &wgpu::Surface<'static>,
87        on_driver: &mut Option<Box<dyn FnOnce(std::sync::Weak<Driver>) + 'static>>,
88    ) -> eyre::Result<Arc<Self>> {
89        if let Some(driver) = weak.upgrade() {
90            return Ok(driver);
91        }
92
93        let adapter = futures_lite::future::block_on(instance.request_adapter(
94            &wgpu::RequestAdapterOptions {
95                compatible_surface: Some(surface),
96                ..Default::default()
97            },
98        ))?;
99
100        // Create the logical device and command queue
101        let (device, queue) = adapter
102            .request_device(&wgpu::DeviceDescriptor {
103                label: Some("Feather UI wgpu Device"),
104                required_features: wgpu::Features::empty(),
105                required_limits: wgpu::Limits::default(),
106                memory_hints: wgpu::MemoryHints::MemoryUsage,
107                trace: wgpu::Trace::Off,
108            })
109            .await?;
110
111        let shared = compositor::Shared::new(&device);
112        let atlas = atlas::Atlas::new(&device, 512, atlas::AtlasKind::Primary);
113        let layer0 = atlas::Atlas::new(&device, 128, atlas::AtlasKind::Layer0);
114        let layer1 = atlas::Atlas::new(&device, 128, atlas::AtlasKind::Layer1);
115        let shape_shader = crate::render::shape::Shape::<0>::shader(&device);
116        let shape_pipeline = crate::render::shape::Shape::<0>::layout(&device);
117
118        let comp1 = crate::render::compositor::Compositor::new(
119            &device,
120            &shared,
121            &atlas.view,
122            &layer1.view,
123            ATLAS_FORMAT,
124            true,
125        );
126        let comp2 = crate::render::compositor::Compositor::new(
127            &device,
128            &shared,
129            &atlas.view,
130            &layer0.view,
131            ATLAS_FORMAT,
132            false,
133        );
134
135        let mut driver = Self {
136            adapter,
137            device,
138            queue,
139            swash_cache: ScaleContext::new().into(),
140            font_system: cosmic_text::FontSystem::new().into(),
141            cursor: CursorIcon::Default.into(),
142            pipelines: HashMap::new().into(),
143            glyphs: HashMap::new().into(),
144            registry: HashMap::new().into(),
145            shared,
146            atlas: atlas.into(),
147            layer_atlas: [layer0.into(), layer1.into()],
148            layer_composite: [comp1.into(), comp2.into()],
149        };
150
151        driver.register_pipeline::<crate::render::shape::Shape<0>>(
152            shape_pipeline.clone(),
153            shape_shader.clone(),
154            crate::render::shape::Shape::<0>::create,
155        );
156        driver.register_pipeline::<crate::render::shape::Shape<1>>(
157            shape_pipeline.clone(),
158            shape_shader.clone(),
159            crate::render::shape::Shape::<1>::create,
160        );
161        driver.register_pipeline::<crate::render::shape::Shape<2>>(
162            shape_pipeline.clone(),
163            shape_shader.clone(),
164            crate::render::shape::Shape::<2>::create,
165        );
166        driver.register_pipeline::<crate::render::shape::Shape<3>>(
167            shape_pipeline.clone(),
168            shape_shader.clone(),
169            crate::render::shape::Shape::<3>::create,
170        );
171
172        let driver = Arc::new(driver);
173        *weak = Arc::downgrade(&driver);
174
175        if let Some(f) = on_driver.take() {
176            f(weak.clone());
177        }
178        Ok(driver)
179    }
180
181    pub fn register_pipeline<T: 'static>(
182        &mut self,
183        layout: PipelineLayout,
184        shader: ShaderModule,
185        generator: impl Fn(&PipelineLayout, &ShaderModule, &Self) -> Box<dyn render::AnyPipeline>
186        + Send
187        + Sync
188        + 'static,
189    ) {
190        self.registry.write().insert(
191            TypeId::of::<T>(),
192            PipelineState {
193                layout,
194                shader,
195                generator: Box::new(generator),
196            },
197        );
198    }
199
200    /// Allows replacing the shader in a pipeline, for hot-reloading.
201    pub fn reload_pipeline<T: 'static>(&self, shader: ShaderModule) {
202        let id = TypeId::of::<T>();
203        let mut registry = self.registry.write();
204        let pipeline = registry
205            .get_mut(&id)
206            .expect("Tried to reload unregistered pipeline!");
207        pipeline.shader = shader;
208        self.pipelines.write().remove(&id);
209    }
210    pub fn with_pipeline<T: crate::render::Pipeline + 'static>(&self, f: impl FnOnce(&mut T)) {
211        let id = TypeId::of::<T>();
212
213        // We can't use the result of this because it makes the lifetimes weird
214        if self.pipelines.read().get(&id).is_none() {
215            let PipelineState {
216                generator,
217                layout,
218                shader,
219            } = &self.registry.read()[&id];
220
221            self.pipelines
222                .write()
223                .insert(id, generator(layout, shader, self));
224        }
225
226        f(
227            (self.pipelines.write().get_mut(&id).unwrap().as_mut() as &mut dyn std::any::Any)
228                .downcast_mut()
229                .unwrap(),
230        );
231    }
232}
233
234static_assertions::assert_impl_all!(Driver: Send, Sync);
235
236// This maps x and y to the viewpoint size, maps input_z from [n,f] to [0,1], and sets
237// output_w = input_z for perspective. Requires input_w = 1
238pub fn mat4_proj(x: f32, y: f32, w: f32, h: f32, n: f32, f: f32) -> Mat4 {
239    Mat4 {
240        cols: [
241            Vec4::new(2.0 / w, 0.0, 0.0, 0.0),
242            Vec4::new(0.0, 2.0 / h, 0.0, 0.0),
243            Vec4::new(0.0, 0.0, 1.0 / (f - n), 1.0),
244            Vec4::new(-(2.0 * x + w) / w, -(2.0 * y + h) / h, -n / (f - n), 0.0),
245        ],
246    }
247}
248
249// Orthographic projection matrix
250pub fn mat4_ortho(x: f32, y: f32, w: f32, h: f32, n: f32, f: f32) -> Mat4 {
251    Mat4 {
252        cols: [
253            Vec4::new(2.0 / w, 0.0, 0.0, 0.0),
254            Vec4::new(0.0, 2.0 / h, 0.0, 0.0),
255            Vec4::new(0.0, 0.0, -2.0 / (f - n), 0.0),
256            Vec4::new(
257                -(2.0 * x + w) / w,
258                -(2.0 * y + h) / h,
259                (f + n) / (f - n),
260                1.0,
261            ),
262        ],
263    }
264}
265
266macro_rules! gen_from_array {
267    ($s:path, $t:path, $i:literal) => {
268        impl From<[$t; $i]> for $s {
269            fn from(value: [$t; $i]) -> Self {
270                Self(value)
271            }
272        }
273        impl From<&[$t; $i]> for $s {
274            fn from(value: &[$t; $i]) -> Self {
275                Self(*value)
276            }
277        }
278    };
279}
280
281#[repr(C, align(8))]
282#[derive(Clone, Copy, Debug, Default, PartialEq, bytemuck::NoUninit)]
283pub struct Vec2f(pub(crate) [f32; 2]);
284
285gen_from_array!(Vec2f, f32, 2);
286
287#[repr(C, align(16))]
288#[derive(Clone, Copy, Debug, Default, PartialEq, bytemuck::NoUninit)]
289pub struct Vec4f(pub(crate) [f32; 4]);
290
291gen_from_array!(Vec4f, f32, 4);
292
293#[repr(C, align(8))]
294#[derive(Clone, Copy, Debug, Default, PartialEq, bytemuck::NoUninit)]
295pub struct Vec2i(pub(crate) [i32; 2]);
296
297gen_from_array!(Vec2i, i32, 2);
298
299#[repr(C, align(16))]
300#[derive(Clone, Copy, Debug, Default, PartialEq, bytemuck::NoUninit)]
301pub struct Vec4i(pub(crate) [i32; 4]);
302
303gen_from_array!(Vec4i, i32, 4);