blade_egui/
lib.rs

1#![allow(
2    irrefutable_let_patterns,
3    clippy::new_without_default,
4    // Conflicts with `pattern_type_mismatch`
5    clippy::needless_borrowed_reference,
6)]
7#![warn(
8    trivial_casts,
9    trivial_numeric_casts,
10    unused_extern_crates,
11    unused_qualifications,
12    // We don't match on a reference, unless required.
13    clippy::pattern_type_mismatch,
14)]
15
16const SHADER_SOURCE: &'static str = include_str!("../shader.wgsl");
17
18use blade_util::{BufferBelt, BufferBeltDescriptor};
19use std::{
20    collections::hash_map::{Entry, HashMap},
21    mem::size_of,
22    ptr,
23};
24
25#[repr(C)]
26#[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
27struct Uniforms {
28    screen_size: [f32; 2],
29    padding: [f32; 2],
30}
31
32#[derive(blade_macros::ShaderData)]
33struct Globals {
34    r_uniforms: Uniforms,
35}
36
37#[derive(blade_macros::ShaderData)]
38struct Locals {
39    r_vertex_data: blade_graphics::BufferPiece,
40    r_texture: blade_graphics::TextureView,
41    r_sampler: blade_graphics::Sampler,
42}
43
44#[derive(Debug, PartialEq)]
45pub struct ScreenDescriptor {
46    pub physical_size: (u32, u32),
47    pub scale_factor: f32,
48}
49
50impl ScreenDescriptor {
51    fn logical_size(&self) -> (f32, f32) {
52        let logical_width = self.physical_size.0 as f32 / self.scale_factor;
53        let logical_height = self.physical_size.1 as f32 / self.scale_factor;
54        (logical_width, logical_height)
55    }
56}
57
58struct GuiTexture {
59    allocation: blade_graphics::Texture,
60    view: blade_graphics::TextureView,
61    sampler: blade_graphics::Sampler,
62}
63
64#[inline]
65const fn egui_texture_filter_to_blade(filter: egui::TextureFilter) -> blade_graphics::FilterMode {
66    match filter {
67        egui::TextureFilter::Nearest => blade_graphics::FilterMode::Nearest,
68        egui::TextureFilter::Linear => blade_graphics::FilterMode::Linear,
69    }
70}
71
72impl GuiTexture {
73    fn create(
74        context: &blade_graphics::Context,
75        name: &str,
76        size: blade_graphics::Extent,
77        options: egui::TextureOptions,
78    ) -> Self {
79        let format = blade_graphics::TextureFormat::Rgba8Unorm;
80        let allocation = context.create_texture(blade_graphics::TextureDesc {
81            name,
82            format,
83            size,
84            array_layer_count: 1,
85            mip_level_count: 1,
86            dimension: blade_graphics::TextureDimension::D2,
87            usage: blade_graphics::TextureUsage::COPY | blade_graphics::TextureUsage::RESOURCE,
88            sample_count: 1,
89        });
90        let view = context.create_texture_view(
91            allocation,
92            blade_graphics::TextureViewDesc {
93                name,
94                format,
95                dimension: blade_graphics::ViewDimension::D2,
96                subresources: &blade_graphics::TextureSubresources::default(),
97            },
98        );
99        let sampler = context.create_sampler(blade_graphics::SamplerDesc {
100            name,
101            address_modes: {
102                let mode = match options.wrap_mode {
103                    egui::TextureWrapMode::ClampToEdge => blade_graphics::AddressMode::ClampToEdge,
104                    egui::TextureWrapMode::Repeat => blade_graphics::AddressMode::Repeat,
105                    egui::TextureWrapMode::MirroredRepeat => {
106                        blade_graphics::AddressMode::MirrorRepeat
107                    }
108                };
109                [mode; 3]
110            },
111            mag_filter: egui_texture_filter_to_blade(options.magnification),
112            min_filter: egui_texture_filter_to_blade(options.minification),
113            mipmap_filter: options
114                .mipmap_mode
115                .map(egui_texture_filter_to_blade)
116                .unwrap_or_default(),
117
118            ..Default::default()
119        });
120        Self {
121            allocation,
122            view,
123            sampler,
124        }
125    }
126
127    fn delete(self, context: &blade_graphics::Context) {
128        context.destroy_texture(self.allocation);
129        context.destroy_texture_view(self.view);
130        context.destroy_sampler(self.sampler);
131    }
132}
133
134//TODO: scissor test
135
136/// GUI painter based on egui.
137///
138/// It can render egui primitives into a render pass.
139pub struct GuiPainter {
140    pipeline: blade_graphics::RenderPipeline,
141    //TODO: find a better way to allocate temporary buffers.
142    belt: BufferBelt,
143    textures: HashMap<egui::TextureId, GuiTexture>,
144    //TODO: this could also look better
145    textures_dropped: Vec<GuiTexture>,
146    textures_to_delete: Vec<(GuiTexture, blade_graphics::SyncPoint)>,
147}
148
149impl GuiPainter {
150    /// Destroy the contents of the painter.
151    pub fn destroy(&mut self, context: &blade_graphics::Context) {
152        context.destroy_render_pipeline(&mut self.pipeline);
153        self.belt.destroy(context);
154        for (_, gui_texture) in self.textures.drain() {
155            gui_texture.delete(context);
156        }
157        for gui_texture in self.textures_dropped.drain(..) {
158            gui_texture.delete(context);
159        }
160        for (gui_texture, _) in self.textures_to_delete.drain(..) {
161            gui_texture.delete(context);
162        }
163    }
164
165    /// Create a new painter with a given GPU context.
166    ///
167    /// It supports renderpasses with only a color attachment,
168    /// and this attachment format must be The `output_format`.
169    #[profiling::function]
170    pub fn new(info: blade_graphics::SurfaceInfo, context: &blade_graphics::Context) -> Self {
171        let shader = context.create_shader(blade_graphics::ShaderDesc {
172            source: SHADER_SOURCE,
173        });
174        let globals_layout = <Globals as blade_graphics::ShaderData>::layout();
175        let locals_layout = <Locals as blade_graphics::ShaderData>::layout();
176        let pipeline = context.create_render_pipeline(blade_graphics::RenderPipelineDesc {
177            name: "gui",
178            data_layouts: &[&globals_layout, &locals_layout],
179            vertex: shader.at("vs_main"),
180            vertex_fetches: &[],
181            primitive: blade_graphics::PrimitiveState {
182                topology: blade_graphics::PrimitiveTopology::TriangleList,
183                ..Default::default()
184            },
185            depth_stencil: None, //TODO?
186            fragment: Some(shader.at("fs_main")),
187            color_targets: &[blade_graphics::ColorTargetState {
188                format: info.format,
189                blend: Some(blade_graphics::BlendState {
190                    color: blade_graphics::BlendComponent {
191                        src_factor: blade_graphics::BlendFactor::One,
192                        dst_factor: blade_graphics::BlendFactor::OneMinusSrcAlpha,
193                        operation: blade_graphics::BlendOperation::Add,
194                    },
195                    alpha: blade_graphics::BlendComponent {
196                        src_factor: blade_graphics::BlendFactor::OneMinusDstAlpha,
197                        dst_factor: blade_graphics::BlendFactor::One,
198                        operation: blade_graphics::BlendOperation::Add,
199                    },
200                }),
201                write_mask: blade_graphics::ColorWrites::all(),
202            }],
203            multisample_state: Default::default(),
204        });
205
206        let belt = BufferBelt::new(BufferBeltDescriptor {
207            memory: blade_graphics::Memory::Shared,
208            min_chunk_size: 0x1000,
209            alignment: 4,
210        });
211
212        Self {
213            pipeline,
214            belt,
215            textures: Default::default(),
216            textures_dropped: Vec::new(),
217            textures_to_delete: Vec::new(),
218        }
219    }
220
221    #[profiling::function]
222    fn triage_deletions(&mut self, context: &blade_graphics::Context) {
223        let valid_pos = self
224            .textures_to_delete
225            .iter()
226            .position(|&(_, ref sp)| !context.wait_for(sp, 0))
227            .unwrap_or_default();
228        for (texture, _) in self.textures_to_delete.drain(..valid_pos) {
229            context.destroy_texture_view(texture.view);
230            context.destroy_texture(texture.allocation);
231        }
232    }
233
234    /// Updates the texture used by egui for the fonts etc.
235    /// New textures should be added before the call to `execute()`,
236    /// and old textures should be removed after.
237    #[profiling::function]
238    pub fn update_textures(
239        &mut self,
240        command_encoder: &mut blade_graphics::CommandEncoder,
241        textures_delta: &egui::TexturesDelta,
242        context: &blade_graphics::Context,
243    ) {
244        if textures_delta.set.is_empty() && textures_delta.free.is_empty() {
245            return;
246        }
247
248        let mut copies = Vec::new();
249        for &(texture_id, ref image_delta) in textures_delta.set.iter() {
250            let src = match image_delta.image {
251                egui::ImageData::Color(ref c) => self.belt.alloc_pod(c.pixels.as_slice(), context),
252                egui::ImageData::Font(ref a) => {
253                    let color_iter = a.srgba_pixels(None);
254                    let stage = self.belt.alloc(
255                        (color_iter.len() * size_of::<egui::Color32>()) as u64,
256                        context,
257                    );
258                    let mut ptr = stage.data() as *mut egui::Color32;
259                    for color in color_iter {
260                        unsafe {
261                            ptr::write(ptr, color);
262                            ptr = ptr.offset(1);
263                        }
264                    }
265                    stage
266                }
267            };
268
269            let image_size = image_delta.image.size();
270            let extent = blade_graphics::Extent {
271                width: image_size[0] as u32,
272                height: image_size[1] as u32,
273                depth: 1,
274            };
275
276            let label = match texture_id {
277                egui::TextureId::Managed(m) => format!("egui_image_{}", m),
278                egui::TextureId::User(u) => format!("egui_user_image_{}", u),
279            };
280
281            let texture = match self.textures.entry(texture_id) {
282                Entry::Occupied(mut o) => {
283                    if image_delta.pos.is_none() {
284                        let texture =
285                            GuiTexture::create(context, &label, extent, image_delta.options);
286                        command_encoder.init_texture(texture.allocation);
287                        let old = o.insert(texture);
288                        self.textures_dropped.push(old);
289                    }
290                    o.into_mut()
291                }
292                Entry::Vacant(v) => {
293                    let texture = GuiTexture::create(context, &label, extent, image_delta.options);
294                    command_encoder.init_texture(texture.allocation);
295                    v.insert(texture)
296                }
297            };
298
299            let dst = blade_graphics::TexturePiece {
300                texture: texture.allocation,
301                mip_level: 0,
302                array_layer: 0,
303                origin: match image_delta.pos {
304                    Some([x, y]) => [x as u32, y as u32, 0],
305                    None => [0; 3],
306                },
307            };
308            copies.push((src, dst, extent));
309        }
310
311        if let mut transfer = command_encoder.transfer("update egui textures") {
312            for (src, dst, extent) in copies {
313                transfer.copy_buffer_to_texture(src, 4 * extent.width, dst, extent);
314            }
315        }
316
317        for texture_id in textures_delta.free.iter() {
318            let texture = self.textures.remove(texture_id).unwrap();
319            self.textures_dropped.push(texture);
320        }
321
322        self.triage_deletions(context);
323    }
324
325    /// Render the set of clipped primitives into a render pass.
326    /// The `sd` must contain dimensions of the render target.
327    #[profiling::function]
328    pub fn paint(
329        &mut self,
330        pass: &mut blade_graphics::RenderCommandEncoder,
331        paint_jobs: &[egui::epaint::ClippedPrimitive],
332        sd: &ScreenDescriptor,
333        context: &blade_graphics::Context,
334    ) {
335        let logical_size = sd.logical_size();
336        let mut pc = pass.with(&self.pipeline);
337        pc.bind(
338            0,
339            &Globals {
340                r_uniforms: Uniforms {
341                    screen_size: [logical_size.0, logical_size.1],
342                    padding: [0.0; 2],
343                },
344            },
345        );
346
347        for clipped_prim in paint_jobs {
348            let clip_rect = &clipped_prim.clip_rect;
349
350            // Make sure clip rect can fit within an `u32`.
351            let clip_min_x = (sd.scale_factor * clip_rect.min.x)
352                .clamp(0.0, sd.physical_size.0 as f32)
353                .trunc() as i32;
354            let clip_min_y = (sd.scale_factor * clip_rect.min.y)
355                .clamp(0.0, sd.physical_size.1 as f32)
356                .trunc() as i32;
357            let clip_max_x = (sd.scale_factor * clip_rect.max.x)
358                .clamp(0.0, sd.physical_size.0 as f32)
359                .ceil() as i32;
360            let clip_max_y = (sd.scale_factor * clip_rect.max.y)
361                .clamp(0.0, sd.physical_size.1 as f32)
362                .ceil() as i32;
363
364            if clip_max_x <= clip_min_x || clip_max_y == clip_min_y {
365                continue;
366            }
367
368            pc.set_scissor_rect(&blade_graphics::ScissorRect {
369                x: clip_min_x,
370                y: clip_min_y,
371                w: (clip_max_x - clip_min_x) as u32,
372                h: (clip_max_y - clip_min_y) as u32,
373            });
374
375            if let egui::epaint::Primitive::Mesh(ref mesh) = clipped_prim.primitive {
376                let texture = self.textures.get(&mesh.texture_id).unwrap();
377                let index_buf = self.belt.alloc_pod(&mesh.indices, context);
378                let vertex_buf = self.belt.alloc_pod(&mesh.vertices, context);
379
380                pc.bind(
381                    1,
382                    &Locals {
383                        r_vertex_data: vertex_buf,
384                        r_texture: texture.view,
385                        r_sampler: texture.sampler,
386                    },
387                );
388
389                pc.draw_indexed(
390                    index_buf,
391                    blade_graphics::IndexType::U32,
392                    mesh.indices.len() as u32,
393                    0,
394                    0,
395                    1,
396                );
397            }
398        }
399    }
400
401    /// Call this after submitting work at the given `sync_point`.
402    #[profiling::function]
403    pub fn after_submit(&mut self, sync_point: &blade_graphics::SyncPoint) {
404        self.textures_to_delete.extend(
405            self.textures_dropped
406                .drain(..)
407                .map(|texture| (texture, sync_point.clone())),
408        );
409        self.belt.flush(sync_point);
410    }
411}