wuple 0.4.0

Simple, Performant rendering on WGPU
Documentation
mod utils;

use ab_glyph::FontRef;
use wgpu::{BindGroupLayout, TextureFormat};
use winit::{event::WindowEvent, window::Window};

use crate::builder::Builder;
use crate::{info::Info, painter::Painter};

use crate::vertex::Vertex;

use crate::App;

const BACKEND_TEXTURE_FORMAT: TextureFormat = TextureFormat::Bgra8UnormSrgb;

pub struct State {
    pub(super) instance: wgpu::Instance,
    pub(super) adapter: wgpu::Adapter,
    pub(super) device: wgpu::Device,
    pub(super) queue: wgpu::Queue,
    pub(super) size: winit::dpi::PhysicalSize<u32>,
    pub(super) window: Window,

    pub(super) surface: Option<wgpu::Surface>,
    pub(super) config: Option<wgpu::SurfaceConfiguration>,
    pub(super) flat_render_pipeline: wgpu::RenderPipeline,
    pub(super) texture_render_pipeline: wgpu::RenderPipeline,

    pub(super) texture_bind_group_layout: BindGroupLayout,
    pub(super) font: FontRef<'static>,
}

// https://sotrh.github.io/learn-wgpu/#what-is-wgpu
impl State {
    // Creating some of the wgpu types requires async code
    pub(super) async fn new<T: App + ?Sized>(window: Window, app: &mut T) -> Self {
        let size = window.inner_size();

        // The instance is a handle to our GPU
        // Backends::all => Vulkan + Metal + DX12 + Browser WebGPU
        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
            backends: utils::BACKENDS,
            dx12_shader_compiler: Default::default(),
        });

        let adapter = instance
            .request_adapter(&wgpu::RequestAdapterOptions {
                power_preference: wgpu::PowerPreference::default(),
                compatible_surface: None,
                force_fallback_adapter: false,
            })
            .await
            .unwrap();

        let (mut device, mut queue) = adapter
            .request_device(
                &wgpu::DeviceDescriptor {
                    features: wgpu::Features::empty(),
                    // WebGL doesn't support all of wgpu's features, so if
                    // we're building for the web we'll have to disable some.
                    limits: utils::limits(),
                    label: None,
                },
                None, // Trace path
            )
            .await
            .unwrap();

        // Flat Render Pipeline
        let flat_shader = device.create_shader_module(wgpu::include_wgsl!("../assets/flat.wgsl"));
        let flat_render_pipeline_layout =
            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                label: Some("Flat Render Pipeline Layout"),
                bind_group_layouts: &[],
                push_constant_ranges: &[],
            });

        let flat_render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("Flat Render Pipeline"),
            layout: Some(&flat_render_pipeline_layout),
            vertex: wgpu::VertexState {
                module: &flat_shader,
                entry_point: "vs_main",     // 1.
                buffers: &[Vertex::desc()], // 2.
            },
            fragment: Some(wgpu::FragmentState {
                // 3.
                module: &flat_shader,
                entry_point: "fs_main",
                targets: &[Some(wgpu::ColorTargetState {
                    // 4.
                    format: BACKEND_TEXTURE_FORMAT,
                    blend: Some(wgpu::BlendState::REPLACE),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
            }),
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList, // 1.
                strip_index_format: None,
                front_face: wgpu::FrontFace::Ccw, // 2.
                cull_mode: Some(wgpu::Face::Back),
                // Setting this to anything other than Fill requires Features::NON_FILL_POLYGON_MODE
                polygon_mode: wgpu::PolygonMode::Fill,
                // Requires Features::DEPTH_CLIP_CONTROL
                unclipped_depth: false,
                // Requires Features::CONSERVATIVE_RASTERIZATION
                conservative: false,
            },
            depth_stencil: None, // 1.
            multisample: wgpu::MultisampleState {
                count: 1,                         // 2.
                mask: !0,                         // 3.
                alpha_to_coverage_enabled: false, // 4.
            },
            multiview: None, // 5.
        });

        // Texture Render Pipeline
        let texture_bind_group_layout =
            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
                entries: &[
                    wgpu::BindGroupLayoutEntry {
                        binding: 0,
                        visibility: wgpu::ShaderStages::FRAGMENT,
                        ty: wgpu::BindingType::Texture {
                            multisampled: false,
                            view_dimension: wgpu::TextureViewDimension::D2,
                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 1,
                        visibility: wgpu::ShaderStages::FRAGMENT,
                        // This should match the filterable field of the
                        // corresponding Texture entry above.
                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                        count: None,
                    },
                ],
                label: Some("texture_bind_group_layout"),
            });

        let texture_shader =
            device.create_shader_module(wgpu::include_wgsl!("../assets/texture.wgsl"));
        let texture_render_pipeline_layout =
            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                label: Some("Texture Render Pipeline Layout"),
                bind_group_layouts: &[&texture_bind_group_layout],
                push_constant_ranges: &[],
            });

        let texture_render_pipeline =
            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
                label: Some("Texture Render Pipeline"),
                layout: Some(&texture_render_pipeline_layout),
                vertex: wgpu::VertexState {
                    module: &texture_shader,
                    entry_point: "vs_main",     // 1.
                    buffers: &[Vertex::desc()], // 2.
                },
                fragment: Some(wgpu::FragmentState {
                    // 3.
                    module: &texture_shader,
                    entry_point: "fs_main",
                    targets: &[Some(wgpu::ColorTargetState {
                        // 4.
                        format: BACKEND_TEXTURE_FORMAT,
                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
                        write_mask: wgpu::ColorWrites::ALL,
                    })],
                }),
                primitive: wgpu::PrimitiveState {
                    topology: wgpu::PrimitiveTopology::TriangleList, // 1.
                    strip_index_format: None,
                    front_face: wgpu::FrontFace::Ccw, // 2.
                    cull_mode: Some(wgpu::Face::Back),
                    // Setting this to anything other than Fill requires Features::NON_FILL_POLYGON_MODE
                    polygon_mode: wgpu::PolygonMode::Fill,
                    // Requires Features::DEPTH_CLIP_CONTROL
                    unclipped_depth: false,
                    // Requires Features::CONSERVATIVE_RASTERIZATION
                    conservative: false,
                },
                depth_stencil: None, // 1.
                multisample: wgpu::MultisampleState {
                    count: 1,                         // 2.
                    mask: !0,                         // 3.
                    alpha_to_coverage_enabled: false, // 4.
                },
                multiview: None, // 5.
            });

        // Font
        let font =
            FontRef::try_from_slice(include_bytes!("../assets/FiraMono-Regular.otf")).unwrap();

        {
            // Initial Data
            let info = Info { window: &window };
            let mut builder = Builder::new(
                &mut device,
                &mut queue,
                &texture_bind_group_layout,
                &font,
                &info,
            );
            app.init(&mut builder);
        }

        Self {
            instance,
            adapter,
            device,
            queue,
            size,
            window,

            surface: None,
            config: None,
            flat_render_pipeline,
            texture_render_pipeline,

            texture_bind_group_layout,
            font,
        }
    }

    // Resume
    pub fn create_surface(&mut self) {
        self.size = self.window.inner_size();

        // # Safety
        //
        // The surface needs to live as long as the window that created it.
        // State owns the window so this should be safe.
        let surface = unsafe { self.instance.create_surface(&self.window) }.unwrap();
        let surface_caps = surface.get_capabilities(&self.adapter);
        let config = wgpu::SurfaceConfiguration {
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            format: BACKEND_TEXTURE_FORMAT,
            width: self.size.width,
            height: self.size.height,
            present_mode: wgpu::PresentMode::Fifo, // VSync, supported by all platforms
            alpha_mode: surface_caps.alpha_modes[0],
            view_formats: vec![],
        };
        surface.configure(&self.device, &config);

        self.surface = Some(surface);
        self.config = Some(config);
    }

    // Suspend
    /*pub fn remove_surface(&mut self) {
        self.surface.take();
        self.config.take();
    }*/

    pub fn window(&self) -> &Window {
        &self.window
    }

    pub(super) fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
        if new_size.width > 0 && new_size.height > 0 {
            self.size = new_size;

            let config = self.config.as_mut().unwrap();
            let surface = self.surface.as_mut().unwrap();
            config.width = new_size.width;
            config.height = new_size.height;
            surface.configure(&self.device, &config);
        }
    }

    pub(super) fn event<T: App + ?Sized>(
        &mut self,
        app: &mut T,
        event: &WindowEvent,
    ) -> crate::EventResult {
        app.event(event)
    }

    pub(super) fn update<T: App + ?Sized>(&mut self, app: &mut T) {
        let info = Info {
            window: &self.window,
        };
        let mut builder = Builder::new(
            &mut self.device,
            &mut self.queue,
            &self.texture_bind_group_layout,
            &self.font,
            &info,
        );

        app.update(&mut builder, &info);
    }

    pub(super) fn render<T: App + ?Sized>(
        &mut self,
        app: &mut T,
    ) -> Result<(), wgpu::SurfaceError> {
        if self.surface.is_none() {
            return Err(wgpu::SurfaceError::Lost);
        }

        let info = Info {
            window: &self.window,
        };
        let mut builder = Builder::new(
            &mut self.device,
            &mut self.queue,
            &self.texture_bind_group_layout,
            &self.font,
            &info,
        );

        let mut painter = Painter::new();
        app.render(&mut painter, &mut builder, &info);

        let output = self.surface.as_ref().unwrap().get_current_texture()?;
        let view = output
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());

        let mut encoder = self
            .device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("Render Encoder"),
            });

        {
            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("Render Pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &view,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(painter.clear_color.into()),
                        store: true,
                    },
                })],
                depth_stencil_attachment: None,
            });

            let mut last_was_texture = false;
            let mut first_shape = true;
            for s in &painter.shapes {
                if let Some(bindgroup) = &s.bindgroup {
                    render_pass.set_pipeline(&self.texture_render_pipeline);
                    render_pass.set_bind_group(0, bindgroup, &[]);
                    last_was_texture = true;
                } else if last_was_texture || first_shape {
                    render_pass.set_pipeline(&self.flat_render_pipeline);
                    last_was_texture = false;
                }

                render_pass.set_vertex_buffer(0, s.vertex_buffer.slice(..));
                render_pass.set_index_buffer(s.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
                render_pass.draw_indexed(0..s.index_count, 0, 0..1);
                first_shape = false;
            }
        }

        // submit will accept anything that implements IntoIter
        self.queue.submit([encoder.finish()]);
        output.present();

        Ok(())
    }
}