zintl_wgpu/
lib.rs

1use std::{borrow::Cow, collections::HashMap, fmt::Display};
2
3use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
4use wgpu::util::DeviceExt;
5use zintl_render::mesh::Mesh;
6use zintl_render_math::Mat4;
7use zintl_render_math::{
8    PhysicalPixelsFPoint, PhysicalPixelsPoint, PhysicalPixelsSize, TexturePoint, Viewport,
9};
10
11pub trait WindowHandle: HasWindowHandle + HasDisplayHandle + Sync + Send {}
12
13impl<T: HasWindowHandle + HasDisplayHandle + Sync + Send> WindowHandle for T {}
14
15#[repr(C)]
16#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
17pub struct Uniforms {
18    pub ortho: Mat4,
19}
20
21#[repr(C)]
22#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
23pub struct DevicePoint {
24    pub x: f32,
25    pub y: f32,
26}
27
28impl From<PhysicalPixelsFPoint> for DevicePoint {
29    #[inline]
30    fn from(point: PhysicalPixelsFPoint) -> Self {
31        Self {
32            x: point.x.value(),
33            y: point.y.value(),
34        }
35    }
36}
37
38impl From<PhysicalPixelsPoint> for DevicePoint {
39    #[inline]
40    fn from(point: PhysicalPixelsPoint) -> Self {
41        Self {
42            x: point.x.value() as f32,
43            y: point.y.value() as f32,
44        }
45    }
46}
47
48#[repr(C)]
49#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
50pub struct DeviceVertex {
51    pub position: DevicePoint,
52    pub tex_coords: TexturePoint,
53}
54
55impl DeviceVertex {
56    pub fn from_vertex(
57        vertex: &zintl_render::mesh::Vertex,
58        texture_size: PhysicalPixelsSize,
59    ) -> Self {
60        Self {
61            position: vertex.position.into(),
62            // TODO: Handle div zero errors
63            tex_coords: TexturePoint::from_physical_point(vertex.tex_coords, texture_size).unwrap(),
64        }
65    }
66}
67
68#[repr(C)]
69#[derive(Clone, Debug, Default)]
70pub struct DeviceMesh {
71    pub vertices: Vec<DeviceVertex>,
72    pub indices: Vec<u32>,
73    pub texture_id: Option<usize>,
74}
75
76impl DeviceMesh {
77    pub fn from_mesh(mesh: Mesh, texture_size: PhysicalPixelsSize) -> Self {
78        let vertices = mesh
79            .vertices
80            .into_iter()
81            .map(|v| DeviceVertex::from_vertex(&v, texture_size))
82            .collect();
83        let indices = mesh.indices;
84        let texture_id = mesh.texture_id;
85
86        DeviceMesh {
87            vertices,
88            indices,
89            texture_id,
90        }
91    }
92}
93
94#[allow(dead_code)]
95#[derive(Debug, Clone)]
96pub struct Texture {
97    native_texture: wgpu::Texture,
98    bind_group: wgpu::BindGroup,
99    size: PhysicalPixelsSize,
100}
101
102const FILL_RECT_SHADER_SRC: &str = include_str!("./shaders/fill_rect.wgsl");
103
104pub type TextureId = usize;
105
106/// A instance of a WGPU renderer
107#[derive(Debug)]
108pub struct WgpuApplication<'a> {
109    surface: wgpu::Surface<'a>,
110    device: wgpu::Device,
111    queue: wgpu::Queue,
112    config: wgpu::SurfaceConfiguration,
113    viewport: Viewport,
114    render_pipeline: wgpu::RenderPipeline,
115    textures: HashMap<TextureId, Texture>,
116    uniform_buffer: wgpu::Buffer,
117    uniform_bind_group: wgpu::BindGroup,
118    texture_bind_group_layout: wgpu::BindGroupLayout,
119}
120
121/// Error type for WgpuApplication
122#[derive(Clone, Debug)]
123pub enum WgpuApplicationError {
124    CreateSurfaceError,
125    AdapterRequestDeviceError(wgpu::RequestAdapterError),
126    CreateDeviceError,
127}
128
129impl Display for WgpuApplicationError {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        match self {
132            WgpuApplicationError::CreateSurfaceError => {
133                write!(f, "Failed to create surface")
134            }
135            WgpuApplicationError::AdapterRequestDeviceError(err) => {
136                write!(f, "Failed to find an appropriate adapter: {:?}", err)
137            }
138            WgpuApplicationError::CreateDeviceError => {
139                write!(f, "Failed to create device")
140            }
141        }
142    }
143}
144
145/// Result type for WgpuApplication
146pub type WgpuApplicationResult<T> = Result<T, WgpuApplicationError>;
147
148impl<'a> WgpuApplication<'a> {
149    async fn init_adapter(
150        instance: &wgpu::Instance,
151        surface: &wgpu::Surface<'a>,
152    ) -> WgpuApplicationResult<wgpu::Adapter> {
153        match instance
154            .request_adapter(&wgpu::RequestAdapterOptions {
155                power_preference: wgpu::PowerPreference::default(),
156                force_fallback_adapter: false,
157                compatible_surface: Some(surface),
158            })
159            .await
160        {
161            Ok(a) => Ok(a),
162            Err(err) => Err(WgpuApplicationError::AdapterRequestDeviceError(err)),
163        }
164    }
165
166    fn create_ortho_matrix(viewport: Viewport) -> Mat4 {
167        cgmath::ortho(
168            0.,
169            viewport.device_width.value() as f32,
170            viewport.device_height.value() as f32,
171            0.,
172            -1.,
173            1.,
174        )
175        .into()
176    }
177
178    fn create_vertex_buffer(device: &wgpu::Device, vertices: &[DeviceVertex]) -> wgpu::Buffer {
179        device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
180            label: Some("Vertex Buffer"),
181            contents: bytemuck::cast_slice(vertices),
182            usage: wgpu::BufferUsages::VERTEX,
183        })
184    }
185
186    fn create_index_buffer(device: &wgpu::Device, indices: &[u32]) -> wgpu::Buffer {
187        device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
188            label: Some("Index Buffer"),
189            contents: bytemuck::cast_slice(indices),
190            usage: wgpu::BufferUsages::INDEX,
191        })
192    }
193
194    fn create_uniform_buffer(device: &wgpu::Device, uniforms: &Uniforms) -> wgpu::Buffer {
195        device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
196            label: Some("Uniform Buffer"),
197            contents: bytemuck::cast_slice(&[*uniforms]),
198            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
199        })
200    }
201
202    async fn init(
203        instance: wgpu::Instance,
204        surface: wgpu::Surface<'a>,
205        viewport: Viewport,
206    ) -> WgpuApplicationResult<Self> {
207        let adapter = Self::init_adapter(&instance, &surface).await?;
208
209        let (device, queue) = match adapter
210            .request_device(&wgpu::DeviceDescriptor {
211                label: None,
212                required_features: wgpu::Features::empty(),
213                required_limits: wgpu::Limits::downlevel_webgl2_defaults()
214                    .using_resolution(adapter.limits()),
215                memory_hints: wgpu::MemoryHints::MemoryUsage,
216                trace: wgpu::Trace::Off,
217            })
218            .await
219        {
220            Ok(r) => r,
221            Err(..) => return Err(WgpuApplicationError::CreateDeviceError),
222        };
223
224        // Orthographic projection matrix
225        let ortho_matrix = Self::create_ortho_matrix(viewport);
226        let uniform_buffer = Self::create_uniform_buffer(
227            &device,
228            &Uniforms {
229                ortho: ortho_matrix.into(),
230            },
231        );
232        let uniform_bind_group_layout =
233            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
234                entries: &[wgpu::BindGroupLayoutEntry {
235                    binding: 0,
236                    visibility: wgpu::ShaderStages::VERTEX,
237                    ty: wgpu::BindingType::Buffer {
238                        ty: wgpu::BufferBindingType::Uniform,
239                        has_dynamic_offset: false,
240                        min_binding_size: None,
241                    },
242                    count: None,
243                }],
244                label: Some("uniform_bind_group_layout"),
245            });
246        let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
247            layout: &uniform_bind_group_layout,
248            entries: &[wgpu::BindGroupEntry {
249                binding: 0,
250                resource: uniform_buffer.as_entire_binding(),
251            }],
252            label: Some("uniform_bind_group"),
253        });
254
255        let texture_bind_group_layout =
256            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
257                entries: &[
258                    // Texture binding
259                    wgpu::BindGroupLayoutEntry {
260                        binding: 0,
261                        visibility: wgpu::ShaderStages::FRAGMENT,
262                        ty: wgpu::BindingType::Texture {
263                            multisampled: false,
264                            view_dimension: wgpu::TextureViewDimension::D2,
265                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
266                        },
267                        count: None,
268                    },
269                    // Sampler binding
270                    wgpu::BindGroupLayoutEntry {
271                        binding: 1,
272                        visibility: wgpu::ShaderStages::FRAGMENT,
273                        // This should match the filterable field of the
274                        // corresponding Texture entry above.
275                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
276                        count: None,
277                    },
278                ],
279                label: Some("texture_bind_group_layout"),
280            });
281
282        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
283            label: None,
284            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(FILL_RECT_SHADER_SRC)),
285        });
286
287        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
288            label: None,
289            bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout],
290            push_constant_ranges: &[],
291        });
292
293        let swapchain_capabilities = surface.get_capabilities(&adapter);
294        let swapchain_format = swapchain_capabilities.formats[0];
295
296        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
297            label: None,
298            layout: Some(&pipeline_layout),
299            vertex: wgpu::VertexState {
300                module: &shader,
301                entry_point: Some("vs_main"),
302                compilation_options: Default::default(),
303                buffers: &[wgpu::VertexBufferLayout {
304                    array_stride: std::mem::size_of::<DeviceVertex>() as wgpu::BufferAddress,
305                    step_mode: wgpu::VertexStepMode::Vertex,
306                    attributes: &[
307                        // position [x, y]
308                        wgpu::VertexAttribute {
309                            format: wgpu::VertexFormat::Float32x2,
310                            offset: 0,
311                            shader_location: 0, // Matches @location(0) in the vertex shader
312                        },
313                        // tex_coords [x, y]
314                        wgpu::VertexAttribute {
315                            format: wgpu::VertexFormat::Float32x2,
316                            offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress,
317                            shader_location: 1,
318                        },
319                    ],
320                }],
321            },
322            primitive: wgpu::PrimitiveState::default(),
323            depth_stencil: None,
324            multisample: wgpu::MultisampleState::default(),
325            fragment: Some(wgpu::FragmentState {
326                module: &shader,
327                entry_point: Some("fs_main"),
328                compilation_options: Default::default(),
329                targets: &[Some(wgpu::ColorTargetState {
330                    format: swapchain_format,
331                    blend: Some(wgpu::BlendState {
332                        color: wgpu::BlendComponent {
333                            src_factor: wgpu::BlendFactor::Src,
334                            dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
335                            operation: wgpu::BlendOperation::Add,
336                        },
337                        alpha: wgpu::BlendComponent {
338                            src_factor: wgpu::BlendFactor::One,
339                            dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
340                            operation: wgpu::BlendOperation::Add,
341                        },
342                    }),
343                    write_mask: wgpu::ColorWrites::ALL,
344                })],
345            }),
346            multiview: None,
347            cache: None,
348        });
349
350        let config = surface
351            .get_default_config(
352                &adapter,
353                viewport.device_width.value(),
354                viewport.device_height.value(),
355            )
356            .unwrap();
357        surface.configure(&device, &config);
358
359        Ok(WgpuApplication {
360            surface,
361            device,
362            queue,
363            config,
364            viewport,
365            render_pipeline,
366            uniform_buffer,
367            uniform_bind_group,
368            textures: HashMap::new(),
369            texture_bind_group_layout,
370        })
371    }
372
373    pub async fn from_window_handle<T: WindowHandle + 'a>(
374        window_handle: T,
375        viewport: Viewport,
376    ) -> WgpuApplicationResult<Self> {
377        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
378            backends: wgpu::Backends::all(),
379            ..Default::default()
380        });
381        let surface_target = wgpu::SurfaceTarget::Window(Box::new(window_handle));
382        let surface = match instance.create_surface(surface_target) {
383            Ok(s) => s,
384            Err(..) => return Err(WgpuApplicationError::CreateSurfaceError),
385        };
386
387        Self::init(instance, surface, viewport).await
388    }
389
390    fn draw_mesh(&mut self, mesh: Mesh, rpass: &mut wgpu::RenderPass<'_>) {
391        if mesh.vertices.is_empty() && mesh.indices.is_empty() {
392            return;
393        }
394        let device_mesh = if let Some(texture_id) = mesh.texture_id {
395            let texture = self.textures.get(&texture_id).expect("Texture not found");
396
397            DeviceMesh::from_mesh(mesh.clone(), texture.size)
398        } else {
399            DeviceMesh::from_mesh(mesh.clone(), PhysicalPixelsSize::new(1.into(), 1.into()))
400        };
401        self.draw_device_mesh(device_mesh, rpass);
402
403        for child in mesh.children {
404            self.draw_mesh(child, rpass);
405        }
406    }
407
408    // TODO
409    fn draw_device_mesh(&mut self, mesh: DeviceMesh, rpass: &mut wgpu::RenderPass<'_>) {
410        if !mesh.vertices.is_empty() || !mesh.indices.is_empty() {
411            if let Some(texture_id) = mesh.texture_id {
412                let texture = self.textures.get(&texture_id).expect("Texture not found");
413
414                rpass.set_bind_group(1, &texture.bind_group, &[]);
415            }
416            let vertex_buffer = Self::create_vertex_buffer(&self.device, &mesh.vertices);
417            let index_buffer = Self::create_index_buffer(&self.device, &mesh.indices);
418            rpass.set_vertex_buffer(0, vertex_buffer.slice(..));
419            rpass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
420            rpass.draw_indexed(0..mesh.indices.len() as u32, 0, 0..1);
421        }
422    }
423
424    pub fn draw(&mut self, meshes: Vec<Mesh>) {
425        let frame = self
426            .surface
427            .get_current_texture()
428            .expect("Failed to acquire next swap chain texture");
429        let view = frame
430            .texture
431            .create_view(&wgpu::TextureViewDescriptor::default());
432        let mut encoder = self
433            .device
434            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
435        {
436            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
437                label: None,
438                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
439                    view: &view,
440                    resolve_target: None,
441                    ops: wgpu::Operations {
442                        load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
443                        store: wgpu::StoreOp::Store,
444                    },
445                })],
446                depth_stencil_attachment: None,
447                timestamp_writes: None,
448                occlusion_query_set: None,
449            });
450
451            rpass.set_pipeline(&self.render_pipeline);
452            rpass.set_bind_group(0, &self.uniform_bind_group, &[]);
453
454            for mesh in meshes {
455                self.draw_mesh(mesh, &mut rpass);
456            }
457        }
458
459        self.queue.submit(Some(encoder.finish()));
460        frame.present();
461    }
462
463    pub fn resize(&mut self, new_viewport: Viewport) {
464        if self.viewport != new_viewport {
465            self.viewport = new_viewport;
466            self.reconfigure_surface_size();
467        }
468    }
469
470    fn reconfigure_surface_size(&mut self) {
471        self.config.width = self.viewport.device_width.value();
472        self.config.height = self.viewport.device_height.value();
473        let ortho = Self::create_ortho_matrix(self.viewport);
474        let uniforms = Uniforms {
475            ortho: ortho.into(),
476        };
477        self.queue
478            .write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
479        self.surface.configure(&self.device, &self.config);
480    }
481
482    pub fn register_texture(&mut self, pixels: Vec<u8>, size: PhysicalPixelsSize) -> usize {
483        let id = self.textures.len();
484        self.register_texture_with_id(id, pixels, size)
485    }
486
487    pub fn register_texture_with_id(
488        &mut self,
489        id: usize,
490        pixels: Vec<u8>,
491        size: PhysicalPixelsSize,
492    ) -> usize {
493        let texture_size = wgpu::Extent3d {
494            width: size.width.value(),
495            height: size.height.value(),
496            depth_or_array_layers: 1,
497        };
498        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
499            size: texture_size,
500            mip_level_count: 1,
501            sample_count: 1,
502            dimension: wgpu::TextureDimension::D2,
503            format: wgpu::TextureFormat::Rgba8UnormSrgb,
504            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
505            label: Some(format!("texture_{}", id).as_str()),
506            // TODO
507            view_formats: &[],
508        });
509
510        self.queue.write_texture(
511            wgpu::TexelCopyTextureInfo {
512                texture: &texture,
513                mip_level: 0,
514                origin: wgpu::Origin3d::ZERO,
515                aspect: wgpu::TextureAspect::All,
516            },
517            &pixels,
518            wgpu::TexelCopyBufferLayout {
519                offset: 0,
520                bytes_per_row: Some(4 * size.width.value()),
521                rows_per_image: Some(size.height.value()),
522            },
523            texture_size,
524        );
525
526        let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
527
528        // TODO: Cache sampler
529        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
530            address_mode_u: wgpu::AddressMode::ClampToEdge,
531            address_mode_v: wgpu::AddressMode::ClampToEdge,
532            address_mode_w: wgpu::AddressMode::ClampToEdge,
533            mag_filter: wgpu::FilterMode::Linear,
534            min_filter: wgpu::FilterMode::Linear,
535            mipmap_filter: wgpu::FilterMode::Linear,
536            ..Default::default()
537        });
538
539        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
540            layout: &self.texture_bind_group_layout,
541            entries: &[
542                wgpu::BindGroupEntry {
543                    binding: 0,
544                    resource: wgpu::BindingResource::TextureView(&texture_view),
545                },
546                wgpu::BindGroupEntry {
547                    binding: 1,
548                    resource: wgpu::BindingResource::Sampler(&sampler),
549                },
550            ],
551            label: Some(format!("bind_group_{}", id).as_str()),
552        });
553
554        let texture = Texture {
555            native_texture: texture,
556            bind_group,
557            size,
558        };
559
560        self.textures.insert(id, texture);
561        id
562    }
563
564    // TODO: fn patch_texture
565}