old school music tracker
1use wgpu::{
2 AddressMode, BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor,
3 BindGroupLayoutEntry, BindingResource, BindingType, BlendState, ColorTargetState, ColorWrites,
4 CommandEncoderDescriptor, Device, DeviceDescriptor, Extent3d, Face, FilterMode, FragmentState,
5 FrontFace, Instance, MultisampleState, Operations, Origin3d, PipelineCompilationOptions,
6 PipelineLayoutDescriptor, PolygonMode, PowerPreference, PrimitiveState, PrimitiveTopology,
7 Queue, RenderPassColorAttachment, RenderPassDescriptor, RenderPipeline,
8 RenderPipelineDescriptor, RequestAdapterOptions, SamplerBindingType, SamplerDescriptor,
9 ShaderModuleDescriptor, ShaderStages, Surface, SurfaceConfiguration, SurfaceError,
10 TexelCopyBufferLayout, TexelCopyTextureInfo, Texture, TextureAspect, TextureDescriptor,
11 TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureViewDescriptor,
12 TextureViewDimension, VertexState, include_wgsl,
13};
14use winit::window::Window;
15
16use crate::palettes::{PALETTE_SIZE, Palette, RGB10A2};
17
18use super::coordinates::WINDOW_SIZE;
19
20pub struct GPUState {
21 surface: Surface<'static>,
22
23 device: Device,
24 queue: Queue,
25 config: SurfaceConfiguration,
26 size: winit::dpi::PhysicalSize<u32>,
27
28 render_pipeline: RenderPipeline,
29
30 streaming_texture: Texture,
31 color_map: Texture,
32 diffuse_bind_group: BindGroup,
33}
34
35impl GPUState {
36 /// Size of the Framebuffer in GPU speech
37 const TEXTURE_SIZE: Extent3d = Extent3d {
38 width: WINDOW_SIZE.0 as u32,
39 height: WINDOW_SIZE.1 as u32,
40 depth_or_array_layers: 1,
41 };
42
43 const COLOR_MAP_SIZE: Extent3d = Extent3d {
44 width: PALETTE_SIZE as u32,
45 height: 1,
46 depth_or_array_layers: 1,
47 };
48
49 pub async fn new(window: std::sync::Arc<Window>) -> Self {
50 let size = window.inner_size();
51 let instance = Instance::default();
52
53 let surface: Surface<'static> = instance.create_surface(window).unwrap();
54
55 let adapter = instance
56 .request_adapter(&RequestAdapterOptions {
57 // i do very little on the GPU (only scale the whole image once per frame)
58 // so i think that this setting should be correct.
59 power_preference: PowerPreference::LowPower,
60 compatible_surface: Some(&surface),
61 force_fallback_adapter: false,
62 })
63 .await
64 .unwrap();
65
66 let (device, queue) = adapter
67 .request_device(&DeviceDescriptor {
68 label: None,
69 required_features: wgpu::Features::empty(),
70 required_limits: wgpu::Limits::downlevel_webgl2_defaults(),
71 // i only have two buffers, one constant size and the other one changing with the window size
72 // so i don't need a lot of allocations and they don't have to be that fast
73 memory_hints: wgpu::MemoryHints::MemoryUsage,
74 trace: wgpu::Trace::Off,
75 })
76 .await
77 .unwrap();
78
79 let surface_caps = surface.get_capabilities(&adapter);
80
81 let surface_format = *surface_caps
82 .formats
83 .iter()
84 .find(|f| !f.is_srgb())
85 .unwrap_or(&surface_caps.formats[0]);
86
87 let config = SurfaceConfiguration {
88 usage: TextureUsages::RENDER_ATTACHMENT,
89 format: surface_format,
90 width: size.width,
91 height: size.height,
92 present_mode: surface_caps.present_modes[0],
93 alpha_mode: surface_caps.alpha_modes[0],
94 view_formats: vec![],
95 desired_maximum_frame_latency: 2, // default
96 };
97
98 surface.configure(&device, &config);
99
100 let window_texture = device.create_texture(&TextureDescriptor {
101 size: Self::TEXTURE_SIZE,
102 mip_level_count: 1,
103 sample_count: 1,
104 dimension: TextureDimension::D2,
105 format: TextureFormat::R8Unorm,
106 usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
107 label: Some("window texture"),
108 view_formats: &[],
109 });
110
111 let texure_view = window_texture.create_view(&TextureViewDescriptor::default());
112 let texture_sampler = device.create_sampler(&SamplerDescriptor {
113 address_mode_u: AddressMode::ClampToEdge,
114 address_mode_v: AddressMode::ClampToEdge,
115 address_mode_w: AddressMode::ClampToEdge,
116 mag_filter: FilterMode::Nearest,
117 min_filter: FilterMode::Nearest,
118 mipmap_filter: FilterMode::Nearest,
119 ..Default::default()
120 });
121
122 let color_map = device.create_texture(&TextureDescriptor {
123 size: Self::COLOR_MAP_SIZE,
124 mip_level_count: 1,
125 sample_count: 1,
126 dimension: TextureDimension::D1,
127 format: TextureFormat::Rgb10a2Unorm,
128 usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING,
129 label: Some("color map"),
130 view_formats: &[],
131 });
132 let color_view = color_map.create_view(&TextureViewDescriptor::default());
133
134 let color_map_sampler = device.create_sampler(&wgpu::wgt::SamplerDescriptor {
135 label: Some("color map sampler"),
136 address_mode_u: AddressMode::Repeat,
137 address_mode_v: AddressMode::Repeat,
138 address_mode_w: AddressMode::Repeat,
139 mag_filter: FilterMode::Nearest,
140 min_filter: FilterMode::Nearest,
141 mipmap_filter: FilterMode::Nearest,
142 ..Default::default()
143 });
144
145 let texture_bind_group_layout =
146 device.create_bind_group_layout(&BindGroupLayoutDescriptor {
147 label: Some("texture_bind_group_layout"),
148 entries: &[
149 BindGroupLayoutEntry {
150 binding: 0,
151 visibility: ShaderStages::FRAGMENT,
152 ty: BindingType::Texture {
153 sample_type: TextureSampleType::Float { filterable: false },
154 view_dimension: TextureViewDimension::D2,
155 multisampled: false,
156 },
157 count: None,
158 },
159 BindGroupLayoutEntry {
160 binding: 1,
161 visibility: ShaderStages::FRAGMENT,
162 ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
163 count: None,
164 },
165 BindGroupLayoutEntry {
166 binding: 2,
167 visibility: ShaderStages::FRAGMENT,
168 ty: BindingType::Texture {
169 sample_type: TextureSampleType::Float { filterable: false },
170 view_dimension: TextureViewDimension::D1,
171 multisampled: false,
172 },
173 count: None,
174 },
175 BindGroupLayoutEntry {
176 binding: 3,
177 visibility: ShaderStages::FRAGMENT,
178 ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
179 count: None,
180 },
181 ],
182 });
183
184 let diffuse_bind_group = device.create_bind_group(&BindGroupDescriptor {
185 label: Some("diffuse_bind_group"),
186 layout: &texture_bind_group_layout,
187 entries: &[
188 BindGroupEntry {
189 binding: 0,
190 resource: BindingResource::TextureView(&texure_view),
191 },
192 BindGroupEntry {
193 binding: 1,
194 resource: BindingResource::Sampler(&texture_sampler),
195 },
196 BindGroupEntry {
197 binding: 2,
198 resource: BindingResource::TextureView(&color_view),
199 },
200 BindGroupEntry {
201 binding: 3,
202 resource: BindingResource::Sampler(&color_map_sampler),
203 },
204 ],
205 });
206
207 const SHADER_DESCRIPTOR: ShaderModuleDescriptor = include_wgsl!("shader.wgsl");
208 let shader = device.create_shader_module(SHADER_DESCRIPTOR);
209
210 // print shader compilation errors
211 #[cfg(debug_assertions)]
212 {
213 let compilation_info = shader.get_compilation_info().await;
214 println!(
215 "{} Shader Compilation Messages",
216 compilation_info.messages.len()
217 );
218 for message in compilation_info.messages {
219 println!("{message:?}");
220 }
221 }
222
223 let render_pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
224 label: Some("Render Pipeline Layout"),
225 bind_group_layouts: &[&texture_bind_group_layout],
226 push_constant_ranges: &[],
227 });
228
229 let render_pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
230 label: Some("Render Pipeline"),
231 layout: Some(&render_pipeline_layout),
232 vertex: VertexState {
233 module: &shader,
234 // only one entrypoint in the shader
235 entry_point: None,
236 buffers: &[],
237 compilation_options: PipelineCompilationOptions::default(),
238 },
239 fragment: Some(FragmentState {
240 module: &shader,
241 // only one entry point in the shader
242 entry_point: None,
243 targets: &[Some(ColorTargetState {
244 format: config.format,
245 blend: Some(BlendState::REPLACE),
246 write_mask: ColorWrites::COLOR,
247 })],
248 compilation_options: PipelineCompilationOptions::default(),
249 }),
250
251 primitive: PrimitiveState {
252 topology: PrimitiveTopology::TriangleList,
253 strip_index_format: None,
254 front_face: FrontFace::Ccw,
255 cull_mode: Some(Face::Back),
256 polygon_mode: PolygonMode::Fill,
257 unclipped_depth: false,
258 conservative: false,
259 },
260 depth_stencil: None,
261 multisample: MultisampleState {
262 count: 1,
263 mask: !0,
264 alpha_to_coverage_enabled: false,
265 },
266 multiview: None,
267 cache: None,
268 });
269
270 Self {
271 surface,
272 device,
273 queue,
274 config,
275 size,
276 render_pipeline,
277 diffuse_bind_group,
278 streaming_texture: window_texture,
279 color_map,
280 }
281 }
282
283 /// on next render the new palette will be used
284 pub fn queue_palette_update(&mut self, palette: Palette<RGB10A2>) {
285 let palette = palette.0.map(|c| c.0.to_le_bytes());
286
287 // the queue will remember this and on the next render submission this will be submitted as well
288 self.queue.write_texture(
289 TexelCopyTextureInfo {
290 texture: &self.color_map,
291 mip_level: 0,
292 origin: Origin3d::ZERO,
293 aspect: TextureAspect::All,
294 },
295 palette.as_flattened(),
296 TexelCopyBufferLayout {
297 offset: 0,
298 bytes_per_row: Some((PALETTE_SIZE * size_of::<u32>()) as u32),
299 rows_per_image: Some(1),
300 },
301 Self::COLOR_MAP_SIZE,
302 );
303 }
304
305 pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
306 if new_size.width > 0 && new_size.height > 0 {
307 self.size = new_size;
308 self.config.width = new_size.width;
309 self.config.height = new_size.height;
310 self.surface.configure(&self.device, &self.config);
311 }
312 }
313
314 pub fn reinit_surface(&self) {
315 if self.size.width > 0 && self.size.height > 0 {
316 self.surface.configure(&self.device, &self.config);
317 }
318 }
319
320 pub fn render(
321 &mut self,
322 framebuffer: &[[u8; WINDOW_SIZE.0]; WINDOW_SIZE.1],
323 ) -> Result<(), SurfaceError> {
324 // SAFETY:
325
326 let framebuffer = framebuffer.as_flattened();
327 assert!(framebuffer.len() == WINDOW_SIZE.0 * WINDOW_SIZE.1);
328
329 const BYTES_PER_ROW: Option<u32> = Some(WINDOW_SIZE.0 as u32);
330 const ROWS_PER_IMAGE: Option<u32> = Some(WINDOW_SIZE.1 as u32);
331
332 // push framebuffer to GPU-Texture
333 self.queue.write_texture(
334 TexelCopyTextureInfo {
335 texture: &self.streaming_texture,
336 mip_level: 0,
337 origin: Origin3d::ZERO,
338 aspect: TextureAspect::All,
339 },
340 framebuffer,
341 TexelCopyBufferLayout {
342 offset: 0,
343 bytes_per_row: BYTES_PER_ROW,
344 rows_per_image: ROWS_PER_IMAGE,
345 },
346 Self::TEXTURE_SIZE,
347 );
348
349 let output = self.surface.get_current_texture()?;
350
351 let view = output
352 .texture
353 .create_view(&TextureViewDescriptor::default());
354
355 let mut encoder = self
356 .device
357 .create_command_encoder(&CommandEncoderDescriptor {
358 label: Some("Render Encoder"),
359 });
360
361 let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
362 label: Some("Render Pass"),
363 color_attachments: &[Some(RenderPassColorAttachment {
364 view: &view,
365 resolve_target: None,
366 ops: Operations {
367 load: wgpu::LoadOp::Clear(wgpu::Color {
368 r: 0.1,
369 g: 0.2,
370 b: 0.3,
371 a: 1.0,
372 }),
373 store: wgpu::StoreOp::Store,
374 },
375 depth_slice: None,
376 })],
377 depth_stencil_attachment: None,
378 timestamp_writes: None,
379 occlusion_query_set: None,
380 });
381
382 render_pass.set_pipeline(&self.render_pipeline);
383 render_pass.set_bind_group(0, &self.diffuse_bind_group, &[]);
384
385 render_pass.draw(0..3, 0..1);
386 // before finishing the encoding the render_pass must be dropped
387 drop(render_pass);
388
389 self.queue.submit(std::iter::once(encoder.finish()));
390 output.present();
391
392 Ok(())
393 }
394}