1use image::GenericImageView;
2use wgpu::util::DeviceExt;
3
4use crate::video::{VideoFrameData, YuvColorInfo, YuvMatrix, YuvRange};
5
6pub struct Renderer {
7 pub instance: wgpu::Instance,
8 pub adapter: wgpu::Adapter,
9 pub device: wgpu::Device,
10 pub queue: wgpu::Queue,
11 pub bind_group_layout_tex: wgpu::BindGroupLayout,
12 pub bind_group_layout_uni: wgpu::BindGroupLayout,
13 pipeline_layout: wgpu::PipelineLayout,
14 shader: wgpu::ShaderModule,
15 pipeline: std::sync::Mutex<Option<(wgpu::TextureFormat, wgpu::RenderPipeline)>>,
17 nv12_bind_group_layout: wgpu::BindGroupLayout,
18 nv12_pipeline: wgpu::RenderPipeline,
19}
20
21pub struct VideoTexture {
22 output: wgpu::Texture,
23 output_view: wgpu::TextureView,
24 effects_bind_group: wgpu::BindGroup,
25 luma: wgpu::Texture,
26 chroma: wgpu::Texture,
27 conversion_buffer: wgpu::Buffer,
28 conversion_bind_group: wgpu::BindGroup,
29 width: u32,
30 height: u32,
31}
32
33impl VideoTexture {
34 pub fn texture(&self) -> &wgpu::Texture {
35 &self.output
36 }
37
38 pub fn bind_group(&self) -> &wgpu::BindGroup {
39 &self.effects_bind_group
40 }
41}
42
43#[repr(C)]
44#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
45struct YuvConversion {
46 range: [f32; 4],
47 red: [f32; 4],
48 green: [f32; 4],
49 blue: [f32; 4],
50}
51
52impl YuvConversion {
53 fn new(color: YuvColorInfo) -> Self {
54 let range = match color.range {
55 YuvRange::Limited => [16.0 / 255.0, 255.0 / 219.0, 128.0 / 255.0, 255.0 / 224.0],
56 YuvRange::Full => [0.0, 1.0, 128.0 / 255.0, 1.0],
57 };
58 let (red_cr, green_cb, green_cr, blue_cb) = match color.matrix {
59 YuvMatrix::Bt601 => (1.402, -0.344_136, -0.714_136, 1.772),
60 YuvMatrix::Bt709 => (1.5748, -0.187_324, -0.468_124, 1.8556),
61 YuvMatrix::Bt2020 => (1.4746, -0.164_553, -0.571_353, 1.8814),
62 };
63 Self {
64 range,
65 red: [1.0, 0.0, red_cr, 0.0],
66 green: [1.0, green_cb, green_cr, 0.0],
67 blue: [1.0, blue_cb, 0.0, 0.0],
68 }
69 }
70}
71
72pub struct PerOutputUniforms {
75 pub buffer: wgpu::Buffer,
76 pub bind_group: wgpu::BindGroup,
77}
78
79#[repr(C)]
80#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
81pub struct Uniforms {
82 pub time: f32,
83 pub progress: f32,
84 pub effect_type: u32,
85 pub padding: u32,
86 pub resolution: [f32; 2],
87 pub image_resolution: [f32; 2],
88 pub old_image_resolution: [f32; 2],
89 pub param_a: f32,
90 pub param_b: f32,
91 pub param_c: f32,
92 pub param_d: f32,
93 pub origin: [f32; 2],
94 pub direction: [f32; 2],
95 pub easing: u32,
96 pub scaling_mode: u32,
97}
98
99impl Uniforms {
100 pub fn from_effect(effect: &crate::animation::EffectUniforms) -> Self {
101 Self {
102 time: effect.progress,
103 progress: effect.progress,
104 effect_type: effect.effect_type,
105 padding: 0,
106 resolution: [1920.0, 1080.0],
107 image_resolution: [1920.0, 1080.0],
108 old_image_resolution: [1920.0, 1080.0],
109 param_a: effect.param_a,
110 param_b: effect.param_b,
111 param_c: effect.param_c,
112 param_d: effect.param_d,
113 origin: effect.origin,
114 direction: effect.direction,
115 easing: effect.easing,
116 scaling_mode: 0,
117 }
118 }
119}
120
121impl Default for Uniforms {
122 fn default() -> Self {
123 Self {
124 time: 0.0,
125 progress: 0.0,
126 effect_type: 0,
127 padding: 0,
128 resolution: [1920.0, 1080.0],
129 image_resolution: [1920.0, 1080.0],
130 old_image_resolution: [1920.0, 1080.0],
131 param_a: 0.0,
132 param_b: 0.0,
133 param_c: 0.0,
134 param_d: 0.0,
135 origin: [0.5, 0.5],
136 direction: [0.0, 0.0],
137 easing: 3,
138 scaling_mode: 0,
139 }
140 }
141}
142
143impl Renderer {
144 pub async fn new() -> anyhow::Result<Self> {
145 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
146 backends: wgpu::Backends::all(),
147 ..Default::default()
148 });
149
150 let adapter = instance
151 .request_adapter(&wgpu::RequestAdapterOptions {
152 power_preference: wgpu::PowerPreference::HighPerformance,
153 compatible_surface: None,
154 force_fallback_adapter: false,
155 })
156 .await
157 .ok_or_else(|| anyhow::anyhow!("Failed to find suitable adapter"))?;
158
159 let (device, queue) = adapter
160 .request_device(
161 &wgpu::DeviceDescriptor {
162 label: None,
163 required_features: wgpu::Features::empty(),
164 required_limits: wgpu::Limits::default(),
165 memory_hints: wgpu::MemoryHints::default(),
166 },
167 None,
168 )
169 .await?;
170
171 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
172 label: Some("Effects Shader"),
173 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
174 crate::shader::EFFECTS_SHADER,
175 )),
176 });
177
178 let bind_group_layout_tex =
179 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
180 entries: &[
181 wgpu::BindGroupLayoutEntry {
182 binding: 0,
183 visibility: wgpu::ShaderStages::FRAGMENT,
184 ty: wgpu::BindingType::Texture {
185 multisampled: false,
186 view_dimension: wgpu::TextureViewDimension::D2,
187 sample_type: wgpu::TextureSampleType::Float { filterable: true },
188 },
189 count: None,
190 },
191 wgpu::BindGroupLayoutEntry {
192 binding: 1,
193 visibility: wgpu::ShaderStages::FRAGMENT,
194 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
195 count: None,
196 },
197 ],
198 label: Some("texture_bind_group_layout"),
199 });
200
201 let bind_group_layout_uni =
202 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
203 entries: &[wgpu::BindGroupLayoutEntry {
204 binding: 0,
205 visibility: wgpu::ShaderStages::FRAGMENT,
206 ty: wgpu::BindingType::Buffer {
207 ty: wgpu::BufferBindingType::Uniform,
208 has_dynamic_offset: false,
209 min_binding_size: None,
210 },
211 count: None,
212 }],
213 label: Some("uniform_bind_group_layout"),
214 });
215
216 let nv12_bind_group_layout =
217 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
218 label: Some("NV12 Conversion Bind Group Layout"),
219 entries: &[
220 wgpu::BindGroupLayoutEntry {
221 binding: 0,
222 visibility: wgpu::ShaderStages::FRAGMENT,
223 ty: wgpu::BindingType::Texture {
224 multisampled: false,
225 view_dimension: wgpu::TextureViewDimension::D2,
226 sample_type: wgpu::TextureSampleType::Float { filterable: true },
227 },
228 count: None,
229 },
230 wgpu::BindGroupLayoutEntry {
231 binding: 1,
232 visibility: wgpu::ShaderStages::FRAGMENT,
233 ty: wgpu::BindingType::Texture {
234 multisampled: false,
235 view_dimension: wgpu::TextureViewDimension::D2,
236 sample_type: wgpu::TextureSampleType::Float { filterable: true },
237 },
238 count: None,
239 },
240 wgpu::BindGroupLayoutEntry {
241 binding: 2,
242 visibility: wgpu::ShaderStages::FRAGMENT,
243 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
244 count: None,
245 },
246 wgpu::BindGroupLayoutEntry {
247 binding: 3,
248 visibility: wgpu::ShaderStages::FRAGMENT,
249 ty: wgpu::BindingType::Buffer {
250 ty: wgpu::BufferBindingType::Uniform,
251 has_dynamic_offset: false,
252 min_binding_size: None,
253 },
254 count: None,
255 },
256 ],
257 });
258
259 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
260 label: Some("Render Pipeline Layout"),
261 bind_group_layouts: &[
262 &bind_group_layout_tex,
263 &bind_group_layout_tex,
264 &bind_group_layout_uni,
265 ],
266 push_constant_ranges: &[],
267 });
268
269 let nv12_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
270 label: Some("NV12 to RGB Shader"),
271 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
272 crate::shader::NV12_TO_RGB_SHADER,
273 )),
274 });
275 let nv12_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
276 label: Some("NV12 Conversion Pipeline Layout"),
277 bind_group_layouts: &[&nv12_bind_group_layout],
278 push_constant_ranges: &[],
279 });
280 let nv12_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
281 label: Some("NV12 Conversion Pipeline"),
282 layout: Some(&nv12_pipeline_layout),
283 vertex: wgpu::VertexState {
284 module: &nv12_shader,
285 entry_point: Some("vs_main"),
286 buffers: &[],
287 compilation_options: wgpu::PipelineCompilationOptions::default(),
288 },
289 fragment: Some(wgpu::FragmentState {
290 module: &nv12_shader,
291 entry_point: Some("fs_main"),
292 targets: &[Some(wgpu::ColorTargetState {
293 format: wgpu::TextureFormat::Rgba8UnormSrgb,
294 blend: Some(wgpu::BlendState::REPLACE),
295 write_mask: wgpu::ColorWrites::ALL,
296 })],
297 compilation_options: wgpu::PipelineCompilationOptions::default(),
298 }),
299 primitive: wgpu::PrimitiveState::default(),
300 depth_stencil: None,
301 multisample: wgpu::MultisampleState::default(),
302 multiview: None,
303 cache: None,
304 });
305
306 Ok(Self {
307 instance,
308 adapter,
309 device,
310 queue,
311 pipeline_layout,
312 shader,
313 bind_group_layout_tex,
314 bind_group_layout_uni,
315 pipeline: std::sync::Mutex::new(None),
316 nv12_bind_group_layout,
317 nv12_pipeline,
318 })
319 }
320
321 pub fn create_per_output_uniforms(&self) -> PerOutputUniforms {
324 let uniforms = Uniforms::default();
325 let buffer = self
326 .device
327 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
328 label: Some("Per-Output Uniform Buffer"),
329 contents: bytemuck::cast_slice(&[uniforms]),
330 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
331 });
332 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
333 layout: &self.bind_group_layout_uni,
334 entries: &[wgpu::BindGroupEntry {
335 binding: 0,
336 resource: buffer.as_entire_binding(),
337 }],
338 label: Some("per_output_uniform_bind_group"),
339 });
340 PerOutputUniforms { buffer, bind_group }
341 }
342
343 fn get_pipeline(&self, format: wgpu::TextureFormat) -> wgpu::RenderPipeline {
344 let mut cache = self.pipeline.lock().unwrap();
345 if let Some((cached_fmt, ref pipeline)) = *cache
346 && cached_fmt == format
347 {
348 return pipeline.clone();
349 }
350
351 let pipeline = self
352 .device
353 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
354 label: Some("Render Pipeline"),
355 layout: Some(&self.pipeline_layout),
356 vertex: wgpu::VertexState {
357 module: &self.shader,
358 entry_point: Some("vs_main"),
359 buffers: &[],
360 compilation_options: wgpu::PipelineCompilationOptions::default(),
361 },
362 fragment: Some(wgpu::FragmentState {
363 module: &self.shader,
364 entry_point: Some("fs_main"),
365 targets: &[Some(wgpu::ColorTargetState {
366 format,
367 blend: Some(wgpu::BlendState::REPLACE),
368 write_mask: wgpu::ColorWrites::ALL,
369 })],
370 compilation_options: wgpu::PipelineCompilationOptions::default(),
371 }),
372 primitive: wgpu::PrimitiveState {
373 topology: wgpu::PrimitiveTopology::TriangleList,
374 strip_index_format: None,
375 front_face: wgpu::FrontFace::Ccw,
376 cull_mode: None, polygon_mode: wgpu::PolygonMode::Fill,
378 unclipped_depth: false,
379 conservative: false,
380 },
381 depth_stencil: None,
382 multisample: wgpu::MultisampleState {
383 count: 1,
384 mask: !0,
385 alpha_to_coverage_enabled: false,
386 },
387 multiview: None,
388 cache: None,
389 });
390
391 *cache = Some((format, pipeline.clone()));
392 pipeline
393 }
394
395 pub fn create_texture(&self, width: u32, height: u32) -> (wgpu::Texture, wgpu::BindGroup) {
396 let size = wgpu::Extent3d {
397 width,
398 height,
399 depth_or_array_layers: 1,
400 };
401
402 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
403 label: None,
404 size,
405 mip_level_count: 1,
406 sample_count: 1,
407 dimension: wgpu::TextureDimension::D2,
408 format: wgpu::TextureFormat::Rgba8UnormSrgb,
409 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
410 view_formats: &[],
411 });
412
413 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
414 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
415 address_mode_u: wgpu::AddressMode::ClampToEdge,
416 address_mode_v: wgpu::AddressMode::ClampToEdge,
417 address_mode_w: wgpu::AddressMode::ClampToEdge,
418 mag_filter: wgpu::FilterMode::Linear,
419 min_filter: wgpu::FilterMode::Nearest,
420 mipmap_filter: wgpu::FilterMode::Nearest,
421 ..Default::default()
422 });
423 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
424 layout: &self.bind_group_layout_tex,
425 entries: &[
426 wgpu::BindGroupEntry {
427 binding: 0,
428 resource: wgpu::BindingResource::TextureView(&view),
429 },
430 wgpu::BindGroupEntry {
431 binding: 1,
432 resource: wgpu::BindingResource::Sampler(&sampler),
433 },
434 ],
435 label: None,
436 });
437
438 (texture, bind_group)
439 }
440
441 pub fn update_texture(&self, texture: &wgpu::Texture, rgba: &[u8], width: u32, height: u32) {
442 self.queue.write_texture(
443 wgpu::TexelCopyTextureInfo {
444 texture,
445 mip_level: 0,
446 origin: wgpu::Origin3d::ZERO,
447 aspect: wgpu::TextureAspect::All,
448 },
449 rgba,
450 wgpu::TexelCopyBufferLayout {
451 offset: 0,
452 bytes_per_row: Some(4 * width),
453 rows_per_image: Some(height),
454 },
455 wgpu::Extent3d {
456 width,
457 height,
458 depth_or_array_layers: 1,
459 },
460 );
461 }
462
463 pub fn create_video_texture(&self, width: u32, height: u32) -> VideoTexture {
464 let plane_texture = |label, size, format| {
465 self.device.create_texture(&wgpu::TextureDescriptor {
466 label: Some(label),
467 size,
468 mip_level_count: 1,
469 sample_count: 1,
470 dimension: wgpu::TextureDimension::D2,
471 format,
472 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
473 view_formats: &[],
474 })
475 };
476 let luma = plane_texture(
477 "Video NV12 Luma",
478 wgpu::Extent3d {
479 width,
480 height,
481 depth_or_array_layers: 1,
482 },
483 wgpu::TextureFormat::R8Unorm,
484 );
485 let chroma = plane_texture(
486 "Video NV12 Chroma",
487 wgpu::Extent3d {
488 width: width.div_ceil(2),
489 height: height.div_ceil(2),
490 depth_or_array_layers: 1,
491 },
492 wgpu::TextureFormat::Rg8Unorm,
493 );
494 let output = self.device.create_texture(&wgpu::TextureDescriptor {
495 label: Some("Video RGB Output"),
496 size: wgpu::Extent3d {
497 width,
498 height,
499 depth_or_array_layers: 1,
500 },
501 mip_level_count: 1,
502 sample_count: 1,
503 dimension: wgpu::TextureDimension::D2,
504 format: wgpu::TextureFormat::Rgba8UnormSrgb,
505 usage: wgpu::TextureUsages::TEXTURE_BINDING
506 | wgpu::TextureUsages::COPY_DST
507 | wgpu::TextureUsages::RENDER_ATTACHMENT,
508 view_formats: &[],
509 });
510 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
511 label: Some("Video Plane Sampler"),
512 address_mode_u: wgpu::AddressMode::ClampToEdge,
513 address_mode_v: wgpu::AddressMode::ClampToEdge,
514 address_mode_w: wgpu::AddressMode::ClampToEdge,
515 mag_filter: wgpu::FilterMode::Linear,
516 min_filter: wgpu::FilterMode::Linear,
517 mipmap_filter: wgpu::FilterMode::Nearest,
518 ..Default::default()
519 });
520 let output_sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
521 label: Some("Video Output Sampler"),
522 address_mode_u: wgpu::AddressMode::ClampToEdge,
523 address_mode_v: wgpu::AddressMode::ClampToEdge,
524 address_mode_w: wgpu::AddressMode::ClampToEdge,
525 mag_filter: wgpu::FilterMode::Linear,
526 min_filter: wgpu::FilterMode::Nearest,
527 mipmap_filter: wgpu::FilterMode::Nearest,
528 ..Default::default()
529 });
530 let conversion = YuvConversion::new(YuvColorInfo {
531 matrix: YuvMatrix::Bt709,
532 range: YuvRange::Limited,
533 });
534 let conversion_buffer = self
535 .device
536 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
537 label: Some("Video YUV Conversion Uniform"),
538 contents: bytemuck::bytes_of(&conversion),
539 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
540 });
541 let luma_view = luma.create_view(&wgpu::TextureViewDescriptor::default());
542 let chroma_view = chroma.create_view(&wgpu::TextureViewDescriptor::default());
543 let output_view = output.create_view(&wgpu::TextureViewDescriptor::default());
544 let conversion_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
545 label: Some("Video NV12 Conversion Bind Group"),
546 layout: &self.nv12_bind_group_layout,
547 entries: &[
548 wgpu::BindGroupEntry {
549 binding: 0,
550 resource: wgpu::BindingResource::TextureView(&luma_view),
551 },
552 wgpu::BindGroupEntry {
553 binding: 1,
554 resource: wgpu::BindingResource::TextureView(&chroma_view),
555 },
556 wgpu::BindGroupEntry {
557 binding: 2,
558 resource: wgpu::BindingResource::Sampler(&sampler),
559 },
560 wgpu::BindGroupEntry {
561 binding: 3,
562 resource: conversion_buffer.as_entire_binding(),
563 },
564 ],
565 });
566 let effects_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
567 label: Some("Video Effects Bind Group"),
568 layout: &self.bind_group_layout_tex,
569 entries: &[
570 wgpu::BindGroupEntry {
571 binding: 0,
572 resource: wgpu::BindingResource::TextureView(&output_view),
573 },
574 wgpu::BindGroupEntry {
575 binding: 1,
576 resource: wgpu::BindingResource::Sampler(&output_sampler),
577 },
578 ],
579 });
580
581 VideoTexture {
582 output,
583 output_view,
584 effects_bind_group,
585 luma,
586 chroma,
587 conversion_buffer,
588 conversion_bind_group,
589 width,
590 height,
591 }
592 }
593
594 pub fn update_video_texture(
595 &self,
596 texture: &VideoTexture,
597 frame: &VideoFrameData,
598 ) -> anyhow::Result<()> {
599 match frame {
600 VideoFrameData::Rgba(rgba) => {
601 anyhow::ensure!(
602 rgba.len() == texture.width as usize * texture.height as usize * 4,
603 "invalid RGBA video frame size"
604 );
605 self.update_texture(&texture.output, rgba, texture.width, texture.height);
606 }
607 VideoFrameData::Nv12 {
608 y_plane,
609 uv_plane,
610 color,
611 } => {
612 let chroma_width = texture.width.div_ceil(2);
613 let chroma_height = texture.height.div_ceil(2);
614 anyhow::ensure!(
615 y_plane.len() == texture.width as usize * texture.height as usize,
616 "invalid NV12 luma plane size"
617 );
618 anyhow::ensure!(
619 uv_plane.len() == (chroma_width * chroma_height * 2) as usize,
620 "invalid NV12 chroma plane size"
621 );
622 self.queue.write_texture(
623 texture.luma.as_image_copy(),
624 y_plane,
625 wgpu::TexelCopyBufferLayout {
626 offset: 0,
627 bytes_per_row: Some(texture.width),
628 rows_per_image: Some(texture.height),
629 },
630 wgpu::Extent3d {
631 width: texture.width,
632 height: texture.height,
633 depth_or_array_layers: 1,
634 },
635 );
636 self.queue.write_texture(
637 texture.chroma.as_image_copy(),
638 uv_plane,
639 wgpu::TexelCopyBufferLayout {
640 offset: 0,
641 bytes_per_row: Some(chroma_width * 2),
642 rows_per_image: Some(chroma_height),
643 },
644 wgpu::Extent3d {
645 width: chroma_width,
646 height: chroma_height,
647 depth_or_array_layers: 1,
648 },
649 );
650 self.queue.write_buffer(
651 &texture.conversion_buffer,
652 0,
653 bytemuck::bytes_of(&YuvConversion::new(*color)),
654 );
655
656 let mut encoder =
657 self.device
658 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
659 label: Some("NV12 Conversion Encoder"),
660 });
661 {
662 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
663 label: Some("NV12 Conversion Pass"),
664 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
665 view: &texture.output_view,
666 resolve_target: None,
667 ops: wgpu::Operations {
668 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
669 store: wgpu::StoreOp::Store,
670 },
671 })],
672 depth_stencil_attachment: None,
673 occlusion_query_set: None,
674 timestamp_writes: None,
675 });
676 pass.set_pipeline(&self.nv12_pipeline);
677 pass.set_bind_group(0, &texture.conversion_bind_group, &[]);
678 pass.draw(0..3, 0..1);
679 }
680 self.queue.submit([encoder.finish()]);
681 }
682 }
683 Ok(())
684 }
685
686 pub fn load_texture(
687 &self,
688 image: &image::DynamicImage,
689 ) -> anyhow::Result<(wgpu::Texture, wgpu::BindGroup)> {
690 let rgba = image.to_rgba8();
691 let (width, height) = image.dimensions();
692 let (texture, bind_group) = self.create_texture(width, height);
693 self.update_texture(&texture, &rgba, width, height);
694 Ok((texture, bind_group))
695 }
696
697 pub fn update_uniforms(&self, buffer: &wgpu::Buffer, uniforms: Uniforms) {
698 self.queue
699 .write_buffer(buffer, 0, bytemuck::cast_slice(&[uniforms]));
700 }
701
702 pub fn render_frame(
703 &self,
704 request: FrameRequest,
705 per_output: &PerOutputUniforms,
706 ) -> anyhow::Result<FrameStatus> {
707 let FrameRequest {
708 surface,
709 format,
710 bg_bind,
711 new_bind,
712 effect,
713 width,
714 height,
715 img_width,
716 img_height,
717 old_img_width,
718 old_img_height,
719 scaling_mode,
720 } = request;
721 let mut uniforms = Uniforms::from_effect(effect);
722 uniforms.resolution = [width as f32, height as f32];
723 uniforms.image_resolution = [img_width as f32, img_height as f32];
724 uniforms.old_image_resolution = [old_img_width as f32, old_img_height as f32];
725 uniforms.scaling_mode = scaling_mode;
726 self.update_uniforms(&per_output.buffer, uniforms);
727
728 let pipeline = self.get_pipeline(format);
729 let output = match surface.get_current_texture() {
734 Ok(texture) => texture,
735 Err(wgpu::SurfaceError::Timeout) => return Ok(FrameStatus::TimedOut),
736 Err(wgpu::SurfaceError::Outdated) => return Ok(FrameStatus::Outdated),
737 Err(wgpu::SurfaceError::Lost) => return Ok(FrameStatus::Lost),
738 Err(err) => {
739 return Err(anyhow::anyhow!(
740 "failed to acquire swapchain texture: {err:?}"
741 ));
742 }
743 };
744 let view = output
745 .texture
746 .create_view(&wgpu::TextureViewDescriptor::default());
747
748 let mut encoder = self
749 .device
750 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
751 label: Some("Render Encoder"),
752 });
753
754 {
755 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
756 label: Some("Wallpaper Render Pass"),
757 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
758 view: &view,
759 resolve_target: None,
760 ops: wgpu::Operations {
761 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
762 store: wgpu::StoreOp::Store,
763 },
764 })],
765 depth_stencil_attachment: None,
766 occlusion_query_set: None,
767 timestamp_writes: None,
768 });
769
770 render_pass.set_pipeline(&pipeline);
771 render_pass.set_bind_group(0, bg_bind, &[]);
772 render_pass.set_bind_group(1, new_bind, &[]);
773 render_pass.set_bind_group(2, &per_output.bind_group, &[]);
774 render_pass.draw(0..3, 0..1);
776 }
777
778 self.queue.submit(std::iter::once(encoder.finish()));
779 output.present();
780
781 Ok(FrameStatus::Presented)
782 }
783}
784
785#[derive(Debug, Clone, Copy, PartialEq, Eq)]
787pub enum FrameStatus {
788 Presented,
789 TimedOut,
790 Outdated,
791 Lost,
792}
793
794pub struct FrameRequest<'a> {
796 pub surface: &'a wgpu::Surface<'a>,
797 pub format: wgpu::TextureFormat,
798 pub bg_bind: &'a wgpu::BindGroup,
800 pub new_bind: &'a wgpu::BindGroup,
802 pub effect: &'a crate::animation::EffectUniforms,
804 pub width: u32,
805 pub height: u32,
806 pub img_width: u32,
807 pub img_height: u32,
808 pub old_img_width: u32,
809 pub old_img_height: u32,
810 pub scaling_mode: u32,
812}
813
814#[cfg(test)]
815mod tests {
816 use super::*;
817
818 #[test]
819 fn selects_yuv_conversion_coefficients_and_range() {
820 let limited_709 = YuvConversion::new(YuvColorInfo {
821 matrix: YuvMatrix::Bt709,
822 range: YuvRange::Limited,
823 });
824 assert_eq!(limited_709.red, [1.0, 0.0, 1.5748, 0.0]);
825 assert_eq!(limited_709.green, [1.0, -0.187_324, -0.468_124, 0.0]);
826 assert_eq!(limited_709.range[0], 16.0 / 255.0);
827 assert_eq!(limited_709.range[1], 255.0 / 219.0);
828
829 let full_2020 = YuvConversion::new(YuvColorInfo {
830 matrix: YuvMatrix::Bt2020,
831 range: YuvRange::Full,
832 });
833 assert_eq!(full_2020.red, [1.0, 0.0, 1.4746, 0.0]);
834 assert_eq!(full_2020.blue, [1.0, 1.8814, 0.0, 0.0]);
835 assert_eq!(full_2020.range, [0.0, 1.0, 128.0 / 255.0, 1.0]);
836 }
837}