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 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 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 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}