use std::sync::Arc; #[derive(Clone, Copy, Debug)] pub enum ScaleMode { Contain, Cover, Center, Natural, } impl ScaleMode { pub fn cycle(self) -> Self { match self { Self::Contain => Self::Cover, Self::Cover => Self::Center, Self::Center => Self::Natural, Self::Natural => Self::Contain, } } } const BLIT_SHADER: &str = "\ @group(0) @binding(0) var t: texture_2d; @group(0) @binding(1) var s: sampler; struct VertexOutput { @builtin(position) position: vec4, @location(0) uv: vec2, }; @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput { var uv = vec2(f32(vi % 2u), f32(vi / 2u)); var out: VertexOutput; out.position = vec4(2.0 * uv - 1.0, 0.0, 1.0); out.uv = vec2(uv.x, 1.0 - uv.y); return out; } @fragment fn fs_main(@location(0) uv: vec2) -> @location(0) vec4 { return textureSample(t, s, uv); } "; pub struct PreviewWindow { window: Arc, surface: wgpu::Surface<'static>, surface_config: wgpu::SurfaceConfiguration, blit_pipeline: wgpu::RenderPipeline, blit_bind_group: wgpu::BindGroup, canvas_width: u32, canvas_height: u32, pub scale_mode: ScaleMode, } impl PreviewWindow { #[allow(clippy::too_many_arguments)] pub fn new( device: &wgpu::Device, window: Arc, surface: wgpu::Surface<'static>, surface_format: wgpu::TextureFormat, canvas_view: &wgpu::TextureView, canvas_width: u32, canvas_height: u32, scale_mode: ScaleMode, ) -> Self { let size = window.inner_size(); let surface_config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: surface_format, width: size.width.max(1), height: size.height.max(1), present_mode: wgpu::PresentMode::AutoVsync, alpha_mode: wgpu::CompositeAlphaMode::Auto, view_formats: vec![], desired_maximum_frame_latency: 2, }; surface.configure(device, &surface_config); let sampler_nearest = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("blit_sampler"), mag_filter: wgpu::FilterMode::Nearest, min_filter: wgpu::FilterMode::Nearest, ..Default::default() }); let blit_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("blit_bind_group_layout"), 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, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, ], }); let blit_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("blit_bind_group"), layout: &blit_bgl, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(canvas_view), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler_nearest), }, ], }); let blit_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("blit_shader"), source: wgpu::ShaderSource::Wgsl(BLIT_SHADER.into()), }); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("blit_pipeline_layout"), bind_group_layouts: &[Some(&blit_bgl)], immediate_size: 0, }); let blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("blit_pipeline"), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &blit_module, entry_point: Some("vs_main"), buffers: &[], compilation_options: wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &blit_module, entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { format: surface_format, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], compilation_options: wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleStrip, ..Default::default() }, depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview_mask: None, cache: None, }); Self { window, surface, surface_config, blit_pipeline, blit_bind_group, canvas_width, canvas_height, scale_mode, } } pub fn window(&self) -> &winit::window::Window { &self.window } pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) { if width == 0 || height == 0 { return; } self.surface_config.width = width; self.surface_config.height = height; self.surface.configure(device, &self.surface_config); } pub fn draw(&self, device: &wgpu::Device, queue: &wgpu::Queue) { let frame = match self.surface.get_current_texture() { wgpu::CurrentSurfaceTexture::Success(frame) | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame, _ => return, }; let view = frame.texture.create_view(&Default::default()); let w_scale = self.surface_config.width as f32 / self.canvas_width as f32; let h_scale = self.surface_config.height as f32 / self.canvas_height as f32; let scale = match self.scale_mode { ScaleMode::Contain => w_scale.min(h_scale), ScaleMode::Cover => w_scale.max(h_scale), ScaleMode::Center => 1.0, ScaleMode::Natural => 1.0 / (1.0 / w_scale.min(h_scale)).ceil(), }; let vp_w = self.canvas_width as f32 * scale; let vp_h = self.canvas_height as f32 * scale; let vp_x = (self.surface_config.width as f32 - vp_w) / 2.0; let vp_y = (self.surface_config.height as f32 - vp_h) / 2.0; let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("blit_encoder"), }); { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("blit_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &view, depth_slice: None, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, occlusion_query_set: None, timestamp_writes: None, multiview_mask: None, }); pass.set_viewport(vp_x, vp_y, vp_w, vp_h, 0.0, 1.0); pass.set_pipeline(&self.blit_pipeline); pass.set_bind_group(0, &self.blit_bind_group, &[]); pass.draw(0..4, 0..1); } queue.submit(std::iter::once(encoder.finish())); frame.present(); } }