dvd_render/
render.rs

1use ab_glyph::Font;
2use std::collections::HashMap;
3use core::num::NonZeroU8;
4use crate::sequence::GridSequence;
5use crate::atlas::populate_atlas;
6
7#[inline]
8fn compute_output_size<const W: usize, const H: usize>(font_width: u32, font_height: u32) -> (u32, u32) {
9	let output_width = W as u32 * font_width;
10	let output_height = H as u32 * font_height;
11	(output_width, output_height)
12}
13
14pub struct WgpuRenderer<const W: usize, const H: usize> {
15	sequence: GridSequence<W, H>,
16	lut: HashMap<char, u32>,
17	device: wgpu::Device,
18	queue: wgpu::Queue,
19	idx_grid: wgpu::Buffer,
20	output_img: wgpu::Texture,
21	color_grid: wgpu::Buffer,
22	pipeline: wgpu::ComputePipeline,
23	bind_group: wgpu::BindGroup,
24	output_width: u32,
25	output_height: u32
26}
27
28fn round_up_aligned(n: u32) -> u32 {
29	use wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as ALIGN;
30
31	(ALIGN * (n / ALIGN)) + ALIGN
32}
33
34impl<const W: usize, const H: usize> WgpuRenderer<W, H> {
35	pub async fn new<F: Font>(font: F, sequence: GridSequence<W, H>) -> Self {
36		let populated_atlas = populate_atlas(font, &sequence);
37
38		let (output_width, output_height) = compute_output_size::<W, H>(
39			populated_atlas.font_width,
40			populated_atlas.font_height
41		);
42
43		let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
44			backends: wgpu::Backends::all(),
45			flags: wgpu::InstanceFlags::VALIDATION,
46			backend_options: wgpu::BackendOptions::default()
47		});
48
49		let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions::default()).await.unwrap();
50		// I expect that the size of the atlas should be bounded by the size of the output
51		let max_buf_size = round_up_aligned(output_width * 4) as u64 * output_height as u64;
52
53		let (device, queue) = adapter.request_device(&wgpu::DeviceDescriptor {
54			required_features: wgpu::Features::SHADER_INT64,
55			required_limits: wgpu::Limits {
56				max_buffer_size: max_buf_size,
57				max_texture_dimension_2d: output_width.max(output_height),
58				..wgpu::Limits::default()
59			},
60			memory_hints: wgpu::MemoryHints::Performance,
61			label: Some("device"),
62			trace: wgpu::Trace::Off
63		}).await.unwrap();
64
65		let idx_grid = device.create_buffer(&wgpu::BufferDescriptor {
66			label: Some("idx_grid"),
67			size: (H * W * 4) as u64,
68			usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::STORAGE,
69			mapped_at_creation: false
70		});
71
72		let atlas = device.create_buffer(&wgpu::BufferDescriptor {
73			label: Some("atlas"),
74			size: populated_atlas.buffer.len() as u64,
75			usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::STORAGE,
76			mapped_at_creation: false
77		});
78
79		// TODO: investigate efficiency of `write_buffer`
80		queue.write_buffer(&atlas, 0, &populated_atlas.buffer);
81
82		let output_img_size = wgpu::Extent3d {
83			width: output_width,
84			height: output_height,
85			depth_or_array_layers: 1
86		};
87
88		let output_img = device.create_texture(&wgpu::TextureDescriptor {
89			label: Some("output_img"),
90			size: output_img_size,
91			mip_level_count: 1,
92			sample_count: 1,
93			dimension: wgpu::TextureDimension::D2,
94			format: wgpu::TextureFormat::Rgba8Uint,
95			usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::COPY_SRC,
96			view_formats: &[]
97		});
98
99		let grid_width_uniform = device.create_buffer(&wgpu::BufferDescriptor {
100			label: Some("grid_width_uniform"),
101			size: 4,
102			usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
103			mapped_at_creation: false
104		});
105
106		let grid_height_uniform = device.create_buffer(&wgpu::BufferDescriptor {
107			label: Some("grid_height_uniform"),
108			size: 4,
109			usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
110			mapped_at_creation: false
111		});
112
113		queue.write_buffer(&grid_width_uniform, 0, &(W as u32).to_ne_bytes());
114		queue.write_buffer(&grid_height_uniform, 0, &(H as u32).to_ne_bytes());
115
116		let img_width_uniform = device.create_buffer(&wgpu::BufferDescriptor {
117			label: Some("img_width_uniform"),
118			size: 4,
119			usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
120			mapped_at_creation: false
121		});
122
123		let img_height_uniform = device.create_buffer(&wgpu::BufferDescriptor {
124			label: Some("img_height_uniform"),
125			size: 4,
126			usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
127			mapped_at_creation: false
128		});
129
130		queue.write_buffer(&img_width_uniform, 0, &output_width.to_ne_bytes());
131		queue.write_buffer(&img_height_uniform, 0, &output_height.to_ne_bytes());
132
133		let color_grid = device.create_buffer(&wgpu::BufferDescriptor {
134			label: Some("color_grid"),
135			size: (W * H * 8) as u64,
136			usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::STORAGE,
137			mapped_at_creation: false
138		});
139
140		let shader = device.create_shader_module(wgpu::include_wgsl!("shader.wgsl"));
141
142		let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
143			label: Some("bind_group_layout"),
144			entries: &[
145				// idx_grid
146				wgpu::BindGroupLayoutEntry {
147					binding: 0,
148					visibility: wgpu::ShaderStages::COMPUTE,
149					ty: wgpu::BindingType::Buffer {
150						ty: wgpu::BufferBindingType::Storage {
151							read_only: true
152						},
153						has_dynamic_offset: false,
154						min_binding_size: None
155					},
156					count: None
157				},
158				// atlas
159				wgpu::BindGroupLayoutEntry {
160					binding: 1,
161					visibility: wgpu::ShaderStages::COMPUTE,
162					ty: wgpu::BindingType::Buffer {
163						ty: wgpu::BufferBindingType::Storage {
164							read_only: true
165						},
166						has_dynamic_offset: false,
167						min_binding_size: None
168					},
169					count: None
170				},
171				// output_img
172				wgpu::BindGroupLayoutEntry {
173					binding: 2,
174					visibility: wgpu::ShaderStages::COMPUTE,
175					ty: wgpu::BindingType::StorageTexture {
176						access: wgpu::StorageTextureAccess::WriteOnly,
177						format: wgpu::TextureFormat::Rgba8Uint,
178						view_dimension: wgpu::TextureViewDimension::D2
179					},
180					count: None
181				},
182				// grid_width
183				wgpu::BindGroupLayoutEntry {
184					binding: 3,
185					visibility: wgpu::ShaderStages::COMPUTE,
186					ty: wgpu::BindingType::Buffer {
187						ty: wgpu::BufferBindingType::Uniform,
188						has_dynamic_offset: false,
189						min_binding_size: None
190					},
191					count: None
192				},
193				// grid_height
194				wgpu::BindGroupLayoutEntry {
195					binding: 4,
196					visibility: wgpu::ShaderStages::COMPUTE,
197					ty: wgpu::BindingType::Buffer {
198						ty: wgpu::BufferBindingType::Uniform,
199						has_dynamic_offset: false,
200						min_binding_size: None
201					},
202					count: None
203				},
204				// grid_width
205				wgpu::BindGroupLayoutEntry {
206					binding: 5,
207					visibility: wgpu::ShaderStages::COMPUTE,
208					ty: wgpu::BindingType::Buffer {
209						ty: wgpu::BufferBindingType::Uniform,
210						has_dynamic_offset: false,
211						min_binding_size: None
212					},
213					count: None
214				},
215				// grid_height
216				wgpu::BindGroupLayoutEntry {
217					binding: 6,
218					visibility: wgpu::ShaderStages::COMPUTE,
219					ty: wgpu::BindingType::Buffer {
220						ty: wgpu::BufferBindingType::Uniform,
221						has_dynamic_offset: false,
222						min_binding_size: None
223					},
224					count: None
225				},
226				// color_grid
227				wgpu::BindGroupLayoutEntry {
228					binding: 7,
229					visibility: wgpu::ShaderStages::COMPUTE,
230					ty: wgpu::BindingType::Buffer {
231						ty: wgpu::BufferBindingType::Storage {
232							read_only: true
233						},
234						has_dynamic_offset: false,
235						min_binding_size: None
236					},
237					count: None
238				}
239			]
240		});
241
242		let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
243			label: Some("pipeline_layout"),
244			bind_group_layouts: &[&bind_group_layout],
245			push_constant_ranges: &[]
246		});
247
248		let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
249			label: Some("pipeline"),
250			layout: Some(&pipeline_layout),
251			module: &shader,
252			entry_point: Some("sample_atlas"),
253			compilation_options: wgpu::PipelineCompilationOptions::default(),
254			cache: None
255		});
256
257		let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
258			label: Some("bind_group"),
259			layout: &bind_group_layout,
260			entries: &[
261				wgpu::BindGroupEntry {
262					binding: 0,
263					resource: idx_grid.as_entire_binding()
264				},
265				wgpu::BindGroupEntry {
266					binding: 1,
267					resource: atlas.as_entire_binding()
268				},
269				wgpu::BindGroupEntry {
270					binding: 2,
271					resource: wgpu::BindingResource::TextureView(
272						&output_img.create_view(&wgpu::TextureViewDescriptor {
273							label: Some("output_img_view"),
274							format: Some(wgpu::TextureFormat::Rgba8Uint),
275							dimension: Some(wgpu::TextureViewDimension::D2),
276							usage: Some(wgpu::TextureUsages::STORAGE_BINDING),
277							aspect: wgpu::TextureAspect::All,
278							base_mip_level: 0,
279							mip_level_count: None,
280							base_array_layer: 0,
281							array_layer_count: None
282						})
283					)
284				},
285				wgpu::BindGroupEntry {
286					binding: 3,
287					resource: grid_width_uniform.as_entire_binding()
288				},
289				wgpu::BindGroupEntry {
290					binding: 4,
291					resource: grid_height_uniform.as_entire_binding()
292				},
293				wgpu::BindGroupEntry {
294					binding: 5,
295					resource: img_width_uniform.as_entire_binding()
296				},
297				wgpu::BindGroupEntry {
298					binding: 6,
299					resource: img_height_uniform.as_entire_binding()
300				},
301				wgpu::BindGroupEntry {
302					binding: 7,
303					resource: color_grid.as_entire_binding()
304				}
305			]
306		});
307
308		Self {
309			sequence,
310			lut: populated_atlas.lut,
311			device,
312			queue,
313			idx_grid,
314			output_img,
315			pipeline,
316			bind_group,
317			output_width,
318			output_height,
319			color_grid
320		}
321	}
322}
323
324pub struct RenderedFrame {
325	pub img: image::RgbaImage,
326	pub frame_hold: NonZeroU8
327}
328
329impl RenderedFrame {
330	fn deserialize(width: u32, height: u32, data: Vec<u8>, frame_hold: NonZeroU8, bytes_per_row: usize) -> Self {
331		let mut buf = Vec::with_capacity(width as usize * height as usize * 4);
332		let used_row_bytes = width as usize * 4;
333
334		for row in data.chunks(bytes_per_row) {
335			buf.extend_from_slice(&row[..used_row_bytes]);
336		}
337
338		Self {
339			img: image::RgbaImage::from_raw(
340				width,
341				height,
342				buf
343			).unwrap(),
344			frame_hold
345		}
346	}
347}
348
349#[inline]
350fn int_div_round_up(divisor: u32, dividend: u32) -> u32 {
351	(divisor / dividend) + match divisor % dividend {
352		0 => 0,
353		_ => 1
354	}
355}
356
357impl<const W: usize, const H: usize> Iterator for WgpuRenderer<W, H> {
358	type Item = RenderedFrame;
359
360	fn next(&mut self) -> Option<Self::Item> {
361		let frame = self.sequence.pop()?;
362
363		let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
364			label: Some("encoder")
365		});
366
367		let frame_hold = frame.frame_hold;
368		self.queue.write_buffer(&self.color_grid, 0, &frame.serialize_colors());
369		self.queue.write_buffer(&self.idx_grid, 0, &frame.serialize(&self.lut));
370
371		let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
372			label: Some("sample_compute_pass"),
373			timestamp_writes: None
374		});
375		compute_pass.set_pipeline(&self.pipeline);
376		compute_pass.set_bind_group(0, &self.bind_group, &[]);
377		compute_pass.dispatch_workgroups(
378			int_div_round_up(self.output_width, 16),
379			int_div_round_up(self.output_height, 16),
380			1
381		);
382		drop(compute_pass);
383
384		let padded_bytes_width = round_up_aligned(self.output_width * 4);
385		let padded_bytes = padded_bytes_width * self.output_height;
386		let map_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
387			label: Some("map_buf"),
388			size: padded_bytes as u64,
389			usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
390			mapped_at_creation: false
391		});
392
393		encoder.copy_texture_to_buffer(
394			wgpu::TexelCopyTextureInfo {
395				texture: &self.output_img,
396				mip_level: 0,
397				origin: wgpu::Origin3d::ZERO,
398				aspect: wgpu::TextureAspect::All
399			},
400			wgpu::TexelCopyBufferInfo {
401				buffer: &map_buf,
402				layout: wgpu::TexelCopyBufferLayout {
403					offset: 0,
404					bytes_per_row: Some(padded_bytes_width),
405					rows_per_image: Some(self.output_height)
406				}
407			},
408			wgpu::Extent3d {
409				width: self.output_width,
410				height: self.output_height,
411				depth_or_array_layers: 1
412			}
413		);
414
415		self.queue.submit(std::iter::once(encoder.finish()));
416
417		map_buf.map_async(wgpu::MapMode::Read, .., |r| r.unwrap());
418		self.device.poll(wgpu::PollType::Wait).unwrap();
419
420		let serialized_data = map_buf.get_mapped_range(..).to_vec();
421		Some(RenderedFrame::deserialize(
422			self.output_width,
423			self.output_height,
424			serialized_data,
425			frame_hold,
426			padded_bytes_width as usize
427		))
428	}
429}
430
431mod private {
432	use super::WgpuRenderer;
433
434	pub trait Sealed {}
435
436	impl<const W: usize, const H: usize> Sealed for WgpuRenderer<W, H> {}
437}
438
439pub trait VideoSrc: Iterator<Item = RenderedFrame> + Send + 'static + private::Sealed {
440	fn framerate(&self) -> NonZeroU8;
441	fn width(&self) -> u32;
442	fn height(&self) -> u32;
443}
444
445impl<const W: usize, const H: usize> VideoSrc for WgpuRenderer<W, H> {
446	#[inline]
447	fn framerate(&self) -> NonZeroU8 {
448		self.sequence.framerate
449	}
450
451	#[inline]
452	fn width(&self) -> u32 {
453		self.output_width
454	}
455
456	#[inline]
457	fn height(&self) -> u32 {
458		self.output_height
459	}
460}