yakui_wgpu/
lib.rs

1#![allow(clippy::new_without_default)]
2#![doc = include_str!("../README.md")]
3
4mod bindgroup_cache;
5mod buffer;
6mod pipeline_cache;
7mod samplers;
8mod texture;
9
10use std::collections::HashMap;
11use std::mem::size_of;
12use std::ops::Range;
13use std::sync::Arc;
14
15use buffer::Buffer;
16use bytemuck::{Pod, Zeroable};
17use glam::UVec2;
18use thunderdome::{Arena, Index};
19use yakui_core::geometry::{Rect, Vec2, Vec4};
20use yakui_core::paint::{PaintDom, Pipeline, Texture, TextureChange, TextureFormat};
21use yakui_core::{ManagedTextureId, TextureId};
22
23use self::bindgroup_cache::TextureBindgroupCache;
24use self::bindgroup_cache::TextureBindgroupCacheEntry;
25use self::pipeline_cache::PipelineCache;
26use self::samplers::Samplers;
27use self::texture::{GpuManagedTexture, GpuTexture};
28
29pub struct YakuiWgpu {
30    main_pipeline: PipelineCache,
31    text_pipeline: PipelineCache,
32    samplers: Samplers,
33    textures: Arena<GpuTexture>,
34    managed_textures: HashMap<ManagedTextureId, GpuManagedTexture>,
35    texture_bindgroup_cache: TextureBindgroupCache,
36
37    vertices: Buffer,
38    indices: Buffer,
39    commands: Vec<DrawCommand>,
40}
41
42#[derive(Debug, Clone)]
43pub struct SurfaceInfo<'a> {
44    pub format: wgpu::TextureFormat,
45    pub sample_count: u32,
46    pub color_attachment: &'a wgpu::TextureView,
47    pub resolve_target: Option<&'a wgpu::TextureView>,
48}
49
50#[derive(Debug, Clone, Copy, Zeroable, Pod)]
51#[repr(C)]
52struct Vertex {
53    pos: Vec2,
54    texcoord: Vec2,
55    color: Vec4,
56}
57
58impl Vertex {
59    const DESCRIPTOR: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
60        array_stride: size_of::<Self>() as u64,
61        step_mode: wgpu::VertexStepMode::Vertex,
62        attributes: &wgpu::vertex_attr_array![
63            0 => Float32x2,
64            1 => Float32x2,
65            2 => Float32x4,
66        ],
67    };
68}
69
70impl YakuiWgpu {
71    pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
72        let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
73            label: Some("yakui Bind Group Layout"),
74            entries: &[
75                wgpu::BindGroupLayoutEntry {
76                    binding: 0,
77                    visibility: wgpu::ShaderStages::FRAGMENT,
78                    ty: wgpu::BindingType::Texture {
79                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
80                        view_dimension: wgpu::TextureViewDimension::D2,
81                        multisampled: false,
82                    },
83                    count: None,
84                },
85                wgpu::BindGroupLayoutEntry {
86                    binding: 1,
87                    visibility: wgpu::ShaderStages::FRAGMENT,
88                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
89                    count: None,
90                },
91            ],
92        });
93
94        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
95            label: Some("yakui Main Pipeline Layout"),
96            bind_group_layouts: &[&layout],
97            push_constant_ranges: &[],
98        });
99
100        let main_pipeline = PipelineCache::new(pipeline_layout);
101
102        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
103            label: Some("yakui Text Pipeline Layout"),
104            bind_group_layouts: &[&layout],
105            push_constant_ranges: &[],
106        });
107
108        let text_pipeline = PipelineCache::new(pipeline_layout);
109
110        let samplers = Samplers::new(device);
111
112        let default_texture_data =
113            Texture::new(TextureFormat::Rgba8Srgb, UVec2::new(1, 1), vec![255; 4]);
114        let default_texture = GpuManagedTexture::new(device, queue, &default_texture_data);
115        let default_bindgroup = bindgroup_cache::bindgroup(
116            device,
117            &layout,
118            &samplers,
119            &default_texture.view,
120            default_texture.min_filter,
121            default_texture.mag_filter,
122            wgpu::FilterMode::Nearest,
123            wgpu::AddressMode::ClampToEdge,
124        );
125
126        Self {
127            main_pipeline,
128            text_pipeline,
129            samplers,
130            textures: Arena::new(),
131            managed_textures: HashMap::new(),
132
133            texture_bindgroup_cache: TextureBindgroupCache::new(layout, default_bindgroup),
134            vertices: Buffer::new(wgpu::BufferUsages::VERTEX),
135            indices: Buffer::new(wgpu::BufferUsages::INDEX),
136            commands: Vec::new(),
137        }
138    }
139
140    /// Creates a `TextureId` from an existing wgpu texture that then be used by
141    /// any yakui widgets.
142    pub fn add_texture(
143        &mut self,
144        view: impl Into<Arc<wgpu::TextureView>>,
145        min_filter: wgpu::FilterMode,
146        mag_filter: wgpu::FilterMode,
147        mipmap_filter: wgpu::FilterMode,
148        address_mode: wgpu::AddressMode,
149    ) -> TextureId {
150        let index = self.textures.insert(GpuTexture {
151            view: view.into(),
152            min_filter,
153            mag_filter,
154            mipmap_filter,
155            address_mode,
156        });
157        TextureId::User(index.to_bits())
158    }
159
160    /// Update an existing texture with a new texture view.
161    ///
162    /// ## Panics
163    ///
164    /// Will panic if `TextureId` was not created from a previous call to
165    /// `add_texture`.
166    pub fn update_texture(&mut self, id: TextureId, view: impl Into<Arc<wgpu::TextureView>>) {
167        let index = match id {
168            TextureId::User(bits) => Index::from_bits(bits).expect("invalid user texture"),
169            _ => panic!("invalid user texture"),
170        };
171
172        let existing = self
173            .textures
174            .get_mut(index)
175            .expect("user texture does not exist");
176        existing.view = view.into();
177    }
178
179    #[must_use = "YakuiWgpu::paint returns a command buffer which MUST be submitted to wgpu."]
180    pub fn paint(
181        &mut self,
182        state: &mut yakui_core::Yakui,
183        device: &wgpu::Device,
184        queue: &wgpu::Queue,
185        surface: SurfaceInfo<'_>,
186    ) -> wgpu::CommandBuffer {
187        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
188            label: Some("yakui Encoder"),
189        });
190
191        self.paint_with_encoder(state, device, queue, &mut encoder, surface);
192
193        encoder.finish()
194    }
195
196    pub fn paint_with_encoder(
197        &mut self,
198        state: &mut yakui_core::Yakui,
199        device: &wgpu::Device,
200        queue: &wgpu::Queue,
201        encoder: &mut wgpu::CommandEncoder,
202        surface: SurfaceInfo<'_>,
203    ) {
204        profiling::scope!("yakui-wgpu paint_with_encoder");
205
206        let paint = state.paint();
207
208        self.update_textures(device, paint, queue);
209
210        let layers = paint.layers();
211        if layers.iter().all(|layer| layer.calls.is_empty()) {
212            return;
213        }
214
215        self.update_buffers(device, paint);
216
217        let vertices = self.vertices.upload(device, queue);
218        let indices = self.indices.upload(device, queue);
219        let commands = &self.commands;
220
221        if paint.surface_size() == Vec2::ZERO {
222            return;
223        }
224
225        {
226            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
227                label: Some("yakui Render Pass"),
228                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
229                    view: surface.color_attachment,
230                    resolve_target: surface.resolve_target,
231                    ops: wgpu::Operations {
232                        load: wgpu::LoadOp::Load,
233                        store: wgpu::StoreOp::Store,
234                    },
235                })],
236                ..Default::default()
237            });
238
239            render_pass.set_vertex_buffer(0, vertices.slice(..));
240            render_pass.set_index_buffer(indices.slice(..), wgpu::IndexFormat::Uint32);
241
242            let mut last_clip = None;
243
244            let main_pipeline = self.main_pipeline.get(
245                device,
246                surface.format,
247                surface.sample_count,
248                make_main_pipeline,
249            );
250
251            let text_pipeline = self.text_pipeline.get(
252                device,
253                surface.format,
254                surface.sample_count,
255                make_text_pipeline,
256            );
257
258            for command in commands {
259                match command.pipeline {
260                    Pipeline::Main => render_pass.set_pipeline(main_pipeline),
261                    Pipeline::Text => render_pass.set_pipeline(text_pipeline),
262                    _ => continue,
263                }
264
265                if command.clip != last_clip {
266                    last_clip = command.clip;
267
268                    let surface = paint.surface_size().as_uvec2();
269
270                    match command.clip {
271                        Some(rect) => {
272                            let pos = rect.pos().as_uvec2();
273                            let size = rect.size().as_uvec2();
274
275                            let max = (pos + size).min(surface);
276                            let size = UVec2::new(
277                                max.x.saturating_sub(pos.x),
278                                max.y.saturating_sub(pos.y),
279                            );
280
281                            // If the scissor rect isn't valid, we can skip this
282                            // entire draw call.
283                            if pos.x > surface.x || pos.y > surface.y || size.x == 0 || size.y == 0
284                            {
285                                continue;
286                            }
287
288                            render_pass.set_scissor_rect(pos.x, pos.y, size.x, size.y);
289                        }
290                        None => {
291                            render_pass.set_scissor_rect(0, 0, surface.x, surface.y);
292                        }
293                    }
294                }
295
296                let bindgroup = command
297                    .bind_group_entry
298                    .map(|entry| self.texture_bindgroup_cache.get(&entry))
299                    .unwrap_or(&self.texture_bindgroup_cache.default);
300
301                render_pass.set_bind_group(0, bindgroup, &[]);
302                render_pass.draw_indexed(command.index_range.clone(), 0, 0..1);
303            }
304        }
305    }
306
307    fn update_buffers(&mut self, device: &wgpu::Device, paint: &PaintDom) {
308        profiling::scope!("update_buffers");
309
310        self.vertices.clear();
311        self.indices.clear();
312        self.commands.clear();
313        self.texture_bindgroup_cache.clear();
314
315        let commands = paint
316            .layers()
317            .iter()
318            .flat_map(|layer| &layer.calls)
319            .map(|call| {
320                let vertices = call.vertices.iter().map(|vertex| Vertex {
321                    pos: vertex.position,
322                    texcoord: vertex.texcoord,
323                    color: vertex.color,
324                });
325
326                let base = self.vertices.len() as u32;
327                let indices = call.indices.iter().map(|&index| base + index as u32);
328
329                let start = self.indices.len() as u32;
330                let end = start + indices.len() as u32;
331
332                self.vertices.extend(vertices);
333                self.indices.extend(indices);
334
335                let bind_group_entry = call
336                    .texture
337                    .and_then(|id| match id {
338                        TextureId::Managed(managed) => {
339                            let texture = self.managed_textures.get(&managed)?;
340                            Some((
341                                id,
342                                &texture.view,
343                                texture.min_filter,
344                                texture.mag_filter,
345                                wgpu::FilterMode::Nearest,
346                                texture.address_mode,
347                            ))
348                        }
349                        TextureId::User(bits) => {
350                            let index = Index::from_bits(bits)?;
351                            let texture = self.textures.get(index)?;
352                            Some((
353                                id,
354                                &texture.view,
355                                texture.min_filter,
356                                texture.mag_filter,
357                                texture.mipmap_filter,
358                                texture.address_mode,
359                            ))
360                        }
361                    })
362                    .map(
363                        |(id, view, min_filter, mag_filter, mipmap_filter, address_mode)| {
364                            let entry = TextureBindgroupCacheEntry {
365                                id,
366                                min_filter,
367                                mag_filter,
368                                mipmap_filter,
369                                address_mode,
370                            };
371                            self.texture_bindgroup_cache.update(
372                                device,
373                                entry,
374                                view,
375                                &self.samplers,
376                            );
377                            entry
378                        },
379                    );
380
381                DrawCommand {
382                    index_range: start..end,
383                    bind_group_entry,
384                    pipeline: call.pipeline,
385                    clip: call.clip,
386                }
387            });
388
389        self.commands.extend(commands);
390    }
391
392    fn update_textures(&mut self, device: &wgpu::Device, paint: &PaintDom, queue: &wgpu::Queue) {
393        profiling::scope!("update_textures");
394
395        for (id, texture) in paint.textures() {
396            self.managed_textures
397                .entry(id)
398                .or_insert_with(|| GpuManagedTexture::new(device, queue, texture));
399        }
400
401        for (id, change) in paint.texture_edits() {
402            match change {
403                TextureChange::Added => {
404                    let texture = paint.texture(id).unwrap();
405                    self.managed_textures
406                        .insert(id, GpuManagedTexture::new(device, queue, texture));
407                }
408
409                TextureChange::Removed => {
410                    self.managed_textures.remove(&id);
411                }
412
413                TextureChange::Modified => {
414                    if let Some(existing) = self.managed_textures.get_mut(&id) {
415                        let texture = paint.texture(id).unwrap();
416                        existing.update(device, queue, texture);
417                    }
418                }
419            }
420        }
421    }
422}
423
424struct DrawCommand {
425    index_range: Range<u32>,
426    bind_group_entry: Option<TextureBindgroupCacheEntry>,
427    pipeline: Pipeline,
428    clip: Option<Rect>,
429}
430
431fn make_main_pipeline(
432    device: &wgpu::Device,
433    layout: &wgpu::PipelineLayout,
434    format: wgpu::TextureFormat,
435    samples: u32,
436) -> wgpu::RenderPipeline {
437    let main_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
438        label: Some("Main Shader"),
439        source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/main.wgsl").into()),
440    });
441
442    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
443        label: Some("yakui Main Pipeline"),
444        layout: Some(layout),
445        vertex: wgpu::VertexState {
446            module: &main_shader,
447            entry_point: "vs_main",
448            compilation_options: Default::default(),
449            buffers: &[Vertex::DESCRIPTOR],
450        },
451        fragment: Some(wgpu::FragmentState {
452            module: &main_shader,
453            entry_point: "fs_main",
454            compilation_options: Default::default(),
455            targets: &[Some(wgpu::ColorTargetState {
456                format,
457                blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
458                write_mask: wgpu::ColorWrites::ALL,
459            })],
460        }),
461        primitive: wgpu::PrimitiveState {
462            topology: wgpu::PrimitiveTopology::TriangleList,
463            strip_index_format: None,
464            front_face: wgpu::FrontFace::Ccw,
465            cull_mode: None,
466            polygon_mode: wgpu::PolygonMode::Fill,
467            unclipped_depth: false,
468            conservative: false,
469        },
470        depth_stencil: None,
471        multisample: wgpu::MultisampleState {
472            count: samples,
473            ..Default::default()
474        },
475        multiview: None,
476        cache: None,
477    })
478}
479
480fn make_text_pipeline(
481    device: &wgpu::Device,
482    layout: &wgpu::PipelineLayout,
483    format: wgpu::TextureFormat,
484    samples: u32,
485) -> wgpu::RenderPipeline {
486    let text_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
487        label: Some("Text Shader"),
488        source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/text.wgsl").into()),
489    });
490
491    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
492        label: Some("yakui Text Pipeline"),
493        layout: Some(layout),
494        vertex: wgpu::VertexState {
495            module: &text_shader,
496            entry_point: "vs_main",
497            compilation_options: Default::default(),
498            buffers: &[Vertex::DESCRIPTOR],
499        },
500        fragment: Some(wgpu::FragmentState {
501            module: &text_shader,
502            entry_point: "fs_main",
503            compilation_options: Default::default(),
504            targets: &[Some(wgpu::ColorTargetState {
505                format,
506                blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
507                write_mask: wgpu::ColorWrites::ALL,
508            })],
509        }),
510        primitive: wgpu::PrimitiveState {
511            topology: wgpu::PrimitiveTopology::TriangleList,
512            strip_index_format: None,
513            front_face: wgpu::FrontFace::Ccw,
514            cull_mode: None,
515            polygon_mode: wgpu::PolygonMode::Fill,
516            unclipped_depth: false,
517            conservative: false,
518        },
519        depth_stencil: None,
520        multisample: wgpu::MultisampleState {
521            count: samples,
522            ..Default::default()
523        },
524        multiview: None,
525        cache: None,
526    })
527}