1use anyhow::{anyhow, Result};
7use pixel8_runtime::fb::{Framebuffer, HEIGHT, WIDTH};
8use std::sync::Arc;
9use winit::window::Window;
10
11const SHADER: &str = r#"
12@group(0) @binding(0) var screen_tex: texture_2d<f32>;
13@group(0) @binding(1) var screen_samp: sampler;
14
15struct VsOut {
16 @builtin(position) pos: vec4<f32>,
17 @location(0) uv: vec2<f32>,
18};
19
20@vertex
21fn vs_main(@builtin(vertex_index) i: u32) -> VsOut {
22 // Fullscreen triangle; the viewport restricts it to the screen rect.
23 var out: VsOut;
24 let x = f32(i32(i & 1u) * 4 - 1);
25 let y = f32(i32(i >> 1u) * 4 - 1);
26 out.pos = vec4<f32>(x, y, 0.0, 1.0);
27 out.uv = vec2<f32>((x + 1.0) * 0.5, 1.0 - (y + 1.0) * 0.5);
28 return out;
29}
30
31@fragment
32fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
33 return textureSample(screen_tex, screen_samp, in.uv);
34}
35"#;
36
37#[derive(Clone, Copy, Default)]
39pub struct Viewport {
40 pub x: f32,
41 pub y: f32,
42 pub w: f32,
43 pub h: f32,
44}
45
46impl Viewport {
47 pub fn window_to_screen(&self, px: f64, py: f64) -> (i32, i32) {
49 if self.w <= 0.0 || self.h <= 0.0 {
50 return (0, 0);
51 }
52 let x = ((px as f32 - self.x) / self.w * WIDTH as f32).floor() as i32;
53 let y = ((py as f32 - self.y) / self.h * HEIGHT as f32).floor() as i32;
54 (x, y)
55 }
56}
57
58pub struct Gpu {
59 surface: wgpu::Surface<'static>,
60 device: wgpu::Device,
61 queue: wgpu::Queue,
62 config: wgpu::SurfaceConfiguration,
63 pipeline: wgpu::RenderPipeline,
64 bind_group: wgpu::BindGroup,
65 texture: wgpu::Texture,
66 rgba: Vec<u8>,
67 viewport: Viewport,
68}
69
70impl Gpu {
71 pub fn new(
72 window: Arc<Window>,
73 display: winit::event_loop::OwnedDisplayHandle,
74 ) -> Result<Self> {
75 pollster::block_on(Self::new_async(window, display))
76 }
77
78 async fn new_async(
79 window: Arc<Window>,
80 display: winit::event_loop::OwnedDisplayHandle,
81 ) -> Result<Self> {
82 let size = window.inner_size();
83 let instance = wgpu::Instance::new(
84 wgpu::InstanceDescriptor::new_with_display_handle(Box::new(display)).with_env(),
85 );
86 let surface = instance.create_surface(window)?;
87 let adapter = instance
88 .request_adapter(&wgpu::RequestAdapterOptions {
89 power_preference: wgpu::PowerPreference::LowPower,
90 compatible_surface: Some(&surface),
91 force_fallback_adapter: false,
92 })
93 .await
94 .map_err(|e| anyhow!("no graphics adapter found: {e}"))?;
95 let (device, queue) = adapter
96 .request_device(&wgpu::DeviceDescriptor::default())
97 .await
98 .map_err(|e| anyhow!("could not create graphics device: {e}"))?;
99
100 let mut config = surface
101 .get_default_config(&adapter, size.width.max(1), size.height.max(1))
102 .ok_or_else(|| anyhow!("surface is not supported by the adapter"))?;
103 config.present_mode = wgpu::PresentMode::AutoVsync;
104 surface.configure(&device, &config);
105
106 let texture = device.create_texture(&wgpu::TextureDescriptor {
107 label: Some("pixel8 screen"),
108 size: wgpu::Extent3d {
109 width: WIDTH as u32,
110 height: HEIGHT as u32,
111 depth_or_array_layers: 1,
112 },
113 mip_level_count: 1,
114 sample_count: 1,
115 dimension: wgpu::TextureDimension::D2,
116 format: wgpu::TextureFormat::Rgba8UnormSrgb,
117 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
118 view_formats: &[],
119 });
120 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
121 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
122 mag_filter: wgpu::FilterMode::Nearest,
123 min_filter: wgpu::FilterMode::Nearest,
124 ..Default::default()
125 });
126
127 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
128 label: Some("pixel8 blit"),
129 source: wgpu::ShaderSource::Wgsl(SHADER.into()),
130 });
131 let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
132 label: None,
133 entries: &[
134 wgpu::BindGroupLayoutEntry {
135 binding: 0,
136 visibility: wgpu::ShaderStages::FRAGMENT,
137 ty: wgpu::BindingType::Texture {
138 sample_type: wgpu::TextureSampleType::Float { filterable: true },
139 view_dimension: wgpu::TextureViewDimension::D2,
140 multisampled: false,
141 },
142 count: None,
143 },
144 wgpu::BindGroupLayoutEntry {
145 binding: 1,
146 visibility: wgpu::ShaderStages::FRAGMENT,
147 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
148 count: None,
149 },
150 ],
151 });
152 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
153 label: None,
154 layout: &bgl,
155 entries: &[
156 wgpu::BindGroupEntry {
157 binding: 0,
158 resource: wgpu::BindingResource::TextureView(&view),
159 },
160 wgpu::BindGroupEntry {
161 binding: 1,
162 resource: wgpu::BindingResource::Sampler(&sampler),
163 },
164 ],
165 });
166 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
167 label: None,
168 bind_group_layouts: &[Some(&bgl)],
169 immediate_size: 0,
170 });
171 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
172 label: Some("pixel8 blit"),
173 layout: Some(&layout),
174 vertex: wgpu::VertexState {
175 module: &shader,
176 entry_point: Some("vs_main"),
177 compilation_options: Default::default(),
178 buffers: &[],
179 },
180 fragment: Some(wgpu::FragmentState {
181 module: &shader,
182 entry_point: Some("fs_main"),
183 compilation_options: Default::default(),
184 targets: &[Some(wgpu::ColorTargetState {
185 format: config.format,
186 blend: None,
187 write_mask: wgpu::ColorWrites::ALL,
188 })],
189 }),
190 primitive: wgpu::PrimitiveState::default(),
191 depth_stencil: None,
192 multisample: wgpu::MultisampleState::default(),
193 multiview_mask: None,
194 cache: None,
195 });
196
197 Ok(Self {
198 surface,
199 device,
200 queue,
201 config,
202 pipeline,
203 bind_group,
204 texture,
205 rgba: vec![0; WIDTH as usize * HEIGHT as usize * 4],
206 viewport: Viewport::default(),
207 })
208 }
209
210 pub fn resize(&mut self, width: u32, height: u32) {
211 if width > 0 && height > 0 {
212 self.config.width = width;
213 self.config.height = height;
214 self.surface.configure(&self.device, &self.config);
215 }
216 }
217
218 pub fn viewport(&self) -> Viewport {
219 self.viewport
220 }
221
222 fn compute_viewport(&mut self) {
223 let (sw, sh) = (self.config.width as f32, self.config.height as f32);
224 let scale = (sw / WIDTH as f32).min(sh / HEIGHT as f32);
227 let scale = if scale >= 1.0 { scale.floor() } else { scale };
228 let w = WIDTH as f32 * scale;
229 let h = HEIGHT as f32 * scale;
230 self.viewport = Viewport {
231 x: ((sw - w) / 2.0).floor(),
232 y: ((sh - h) / 2.0).floor(),
233 w,
234 h,
235 };
236 }
237
238 pub fn render(&mut self, fb: &Framebuffer) -> Result<()> {
239 fb.write_rgba(&mut self.rgba);
240 self.queue.write_texture(
241 wgpu::TexelCopyTextureInfo {
242 texture: &self.texture,
243 mip_level: 0,
244 origin: wgpu::Origin3d::ZERO,
245 aspect: wgpu::TextureAspect::All,
246 },
247 &self.rgba,
248 wgpu::TexelCopyBufferLayout {
249 offset: 0,
250 bytes_per_row: Some(WIDTH as u32 * 4),
251 rows_per_image: None,
252 },
253 wgpu::Extent3d {
254 width: WIDTH as u32,
255 height: HEIGHT as u32,
256 depth_or_array_layers: 1,
257 },
258 );
259
260 use wgpu::CurrentSurfaceTexture as Cst;
261 let frame = match self.surface.get_current_texture() {
262 Cst::Success(f) | Cst::Suboptimal(f) => f,
263 Cst::Lost | Cst::Outdated => {
264 self.surface.configure(&self.device, &self.config);
265 match self.surface.get_current_texture() {
266 Cst::Success(f) | Cst::Suboptimal(f) => f,
267 _ => return Ok(()), }
269 }
270 Cst::Timeout | Cst::Occluded | Cst::Validation => return Ok(()),
272 };
273 let view = frame
274 .texture
275 .create_view(&wgpu::TextureViewDescriptor::default());
276 let mut encoder = self
277 .device
278 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
279 {
280 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
281 label: Some("pixel8 blit"),
282 multiview_mask: None,
283 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
284 view: &view,
285 depth_slice: None,
286 resolve_target: None,
287 ops: wgpu::Operations {
288 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
289 store: wgpu::StoreOp::Store,
290 },
291 })],
292 depth_stencil_attachment: None,
293 timestamp_writes: None,
294 occlusion_query_set: None,
295 });
296 self.compute_viewport();
297 let vp = self.viewport;
298 pass.set_viewport(vp.x, vp.y, vp.w, vp.h, 0.0, 1.0);
299 pass.set_pipeline(&self.pipeline);
300 pass.set_bind_group(0, &self.bind_group, &[]);
301 pass.draw(0..3, 0..1);
302 }
303 self.queue.submit([encoder.finish()]);
304 frame.present();
305 Ok(())
306 }
307}