diff --git a/uw8-window/src/gpu/fast_crt.rs b/uw8-window/src/gpu/fast_crt.rs new file mode 100644 index 0000000..cc8f26a --- /dev/null +++ b/uw8-window/src/gpu/fast_crt.rs @@ -0,0 +1,157 @@ +use wgpu::util::DeviceExt; +use winit::dpi::PhysicalSize; + +use super::Filter; + +pub struct FastCrtFilter { + uniform_buffer: wgpu::Buffer, + bind_group: wgpu::BindGroup, + pipeline: wgpu::RenderPipeline, +} + +impl FastCrtFilter { + pub fn new( + device: &wgpu::Device, + screen: &wgpu::TextureView, + resolution: PhysicalSize, + surface_format: wgpu::TextureFormat, + ) -> FastCrtFilter { + let uniforms = Uniforms { + texture_scale: texture_scale_from_resolution(resolution), + }; + + let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: None, + contents: bytemuck::cast_slice(&[uniforms]), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let 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, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], + label: None, + }); + + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + mag_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&screen), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: uniform_buffer.as_entire_binding(), + }, + ], + label: None, + }); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: None, + source: wgpu::ShaderSource::Wgsl(include_str!("fast_crt.wgsl").into()), + }); + + let render_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format: surface_format, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: Default::default(), + depth_stencil: None, + multisample: Default::default(), + multiview: None, + }); + + FastCrtFilter { + uniform_buffer, + bind_group, + pipeline: render_pipeline, + } + } +} + +impl Filter for FastCrtFilter { + fn resize(&self, queue: &wgpu::Queue, new_size: PhysicalSize) { + let uniforms = Uniforms { + texture_scale: texture_scale_from_resolution(new_size), + }; + queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms])); + } + + fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + render_pass.set_pipeline(&self.pipeline); + render_pass.set_bind_group(0, &self.bind_group, &[]); + render_pass.draw(0..6, 0..1); + } +} + +fn texture_scale_from_resolution(res: PhysicalSize) -> [f32; 4] { + let scale = ((res.width as f32) / 160.0).min((res.height as f32) / 120.0); + [ + scale / res.width as f32, + scale / res.height as f32, + 2.0 / scale, + 0.0, + ] +} + +#[repr(C)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct Uniforms { + texture_scale: [f32; 4], +} diff --git a/uw8-window/src/gpu/fast_crt.wgsl b/uw8-window/src/gpu/fast_crt.wgsl new file mode 100644 index 0000000..83bc536 --- /dev/null +++ b/uw8-window/src/gpu/fast_crt.wgsl @@ -0,0 +1,46 @@ +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) tex_coords: vec2, +} + +struct Uniforms { + texture_scale: vec4, +} + +@group(0) @binding(2) var uniforms: Uniforms; + +@vertex +fn vs_main( + @builtin(vertex_index) in_vertex_index: u32, +) -> VertexOutput { + var out: VertexOutput; + let i = in_vertex_index / 3u + in_vertex_index % 3u; + let x = 0.0 + f32(i % 2u) * 320.0; + let y = 0.0 + f32(i / 2u) * 240.0; + out.clip_position = vec4((vec2(x, y) - vec2(160.0, 120.0)) * uniforms.texture_scale.xy, 0.0, 1.0); + out.tex_coords = vec2(x, y); + return out; +} + +@group(0) @binding(0) var screen_texture: texture_2d; +@group(0) @binding(1) var linear_sampler: sampler; + +fn row_factor(offset: f32) -> f32 { + return 1.0 / (1.0 + offset * offset * 16.0); +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let base = round(in.tex_coords) - vec2(0.5); + let frac = in.tex_coords - base; + + let top_factor = row_factor(frac.y); + let bottom_factor = row_factor(frac.y - 1.0); + + let v = base.y + bottom_factor / (bottom_factor + top_factor); + + let u = in.tex_coords.x; + + return textureSample(screen_texture, linear_sampler, vec2(u, v) / vec2(320.0, 240.0)) * (top_factor + bottom_factor) * 2.0; +} + diff --git a/uw8-window/src/gpu/mod.rs b/uw8-window/src/gpu/mod.rs index 80ba48b..8d97a40 100644 --- a/uw8-window/src/gpu/mod.rs +++ b/uw8-window/src/gpu/mod.rs @@ -17,9 +17,11 @@ use winit::platform::unix::EventLoopExtUnix; use winit::platform::windows::EventLoopExtWindows; mod crt; +mod fast_crt; mod square; use crt::CrtFilter; +use fast_crt::FastCrtFilter; use square::SquareFilter; pub struct Window { @@ -155,6 +157,14 @@ impl Window { )) } Some(VirtualKeyCode::Key2) => { + filter = Box::new(FastCrtFilter::new( + &device, + &palette_screen_mode.screen_view, + window.inner_size(), + surface_config.format, + )) + } + Some(VirtualKeyCode::Key3) => { filter = Box::new(CrtFilter::new( &device, &palette_screen_mode.screen_view, diff --git a/uw8-window/src/gpu/square.wgsl b/uw8-window/src/gpu/square.wgsl index d917340..fcdfae8 100644 --- a/uw8-window/src/gpu/square.wgsl +++ b/uw8-window/src/gpu/square.wgsl @@ -15,8 +15,8 @@ fn vs_main( ) -> VertexOutput { var out: VertexOutput; let i = in_vertex_index / 3u + in_vertex_index % 3u; - let x = -1.0 + f32(i % 2u) * 322.0; - let y = -1.0 + f32(i / 2u) * 242.0; + let x = 0.0 + f32(i % 2u) * 320.0; + let y = 0.0 + f32(i / 2u) * 240.0; out.clip_position = vec4((vec2(x, y) - vec2(160.0, 120.0)) * uniforms.texture_scale.xy, 0.0, 1.0); out.tex_coords = vec2(x, y); return out;