ntsc.rsannotatedntsc.rssource531 lines · 22.3 KB · raw
1mod constants;
2
3use crate::config::NtscShaderConfig;
4use crate::renderer::{PipelineShader, Shaders};
5use jgenesis_common::frontend::{CompositeParams, RenderFrameOptions, SamplesPerColorCycle};
6use std::sync::Arc;
7use wgpu::util::DeviceExt;
8
9const BACKDROP_PIXELS: u32 = 6;
10
11const FIR_BUFFER_LEN: usize = 84;
12
13struct NtscFilters {
14    luma_bsf: &'static [f32],
15    chroma_bpf: &'static [f32],
16    y_encode_lpf: &'static [f32],
17    y_decode_lpf: &'static [f32],
18    uv_lpf: &'static [f32],
19}
20
21impl NtscFilters {
22    fn from(samples_per_color_cycle: SamplesPerColorCycle) -> Self {
23        match samples_per_color_cycle {
24            SamplesPerColorCycle::Fifteen => Self {
25                luma_bsf: constants::LUMA_BSF_15_COEFFICIENTS,
26                chroma_bpf: constants::CHROMA_BPF_15_COEFFICIENTS,
27                y_encode_lpf: constants::Y_ENCODE_LPF_15_COEFFICIENTS,
28                y_decode_lpf: constants::Y_DECODE_LPF_15_COEFFICIENTS,
29                uv_lpf: constants::UV_LPF_15_COEFFICIENTS,
30            },
31            SamplesPerColorCycle::Twelve => Self {
32                luma_bsf: constants::LUMA_BSF_12_COEFFICIENTS,
33                chroma_bpf: constants::CHROMA_BPF_12_COEFFICIENTS,
34                y_encode_lpf: constants::Y_ENCODE_LPF_12_COEFFICIENTS,
35                y_decode_lpf: constants::Y_DECODE_LPF_12_COEFFICIENTS,
36                uv_lpf: constants::UV_LPF_12_COEFFICIENTS,
37            },
38        }
39    }
40}
41
42#[repr(C)]
43#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
44struct ImmediateParams {
45    frame_phase_offset: i32,
46    per_line_phase_offset: i32,
47}
48
49impl ImmediateParams {
50    const ZERO: Self = Self { frame_phase_offset: 0, per_line_phase_offset: 0 };
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum NtscShaderVariant {
55    // Input frame buffer contains RGB888 colors
56    // Encode from RGB to NTSC then decode back to RGB
57    Rgb,
58    // Input frame buffer contains 9-bit NES colors (2-bit luma, 4-bit hue, 3-bit color emphasis)
59    // Emulate the NES PPU's NTSC output, then decode to RGB
60    NesPpu,
61}
62
63pub struct NtscShader {
64    output: Arc<wgpu::Texture>,
65    ntsc_frame_size: wgpu::Extent3d,
66    samples_per_color_cycle: SamplesPerColorCycle,
67    immediates_bind_group_layout: wgpu::BindGroupLayout,
68    immediates_bind_group: wgpu::BindGroup,
69    rgb_to_ntsc_bind_group: wgpu::BindGroup,
70    rgb_to_ntsc_pipeline: wgpu::ComputePipeline,
71    separate_luma_chroma_bind_group: wgpu::BindGroup,
72    separate_luma_chroma_pipeline: wgpu::ComputePipeline,
73    luma_chroma_to_rgb_bind_group: wgpu::BindGroup,
74    luma_chroma_to_rgb_pipeline: wgpu::ComputePipeline,
75}
76
77impl NtscShader {
78    pub fn create(
79        device: &wgpu::Device,
80        shaders: &Shaders,
81        input: &wgpu::Texture,
82        params: CompositeParams,
83        config: NtscShaderConfig,
84        variant: NtscShaderVariant,
85    ) -> Self {
86        let ntsc_texture_descriptor = wgpu::TextureDescriptor {
87            label: None,
88            size: wgpu::Extent3d {
89                width: params.upscale_factor * (input.width() + 2 * BACKDROP_PIXELS),
90                height: input.height(),
91                depth_or_array_layers: 1,
92            },
93            mip_level_count: 1,
94            sample_count: 1,
95            dimension: wgpu::TextureDimension::D2,
96            format: wgpu::TextureFormat::R32Float,
97            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::STORAGE_BINDING,
98            view_formats: &[],
99        };
100
101        let ntsc_frame = device.create_texture(&ntsc_texture_descriptor);
102        let ntsc_pass = device.create_texture(&ntsc_texture_descriptor);
103        let ntsc_stop = device.create_texture(&ntsc_texture_descriptor);
104
105        let output_frame = device.create_texture(&wgpu::TextureDescriptor {
106            label: "ntsc_output_texture".into(),
107            size: wgpu::Extent3d {
108                width: params.upscale_factor * input.width(),
109                height: input.height(),
110                depth_or_array_layers: 1,
111            },
112            mip_level_count: 1,
113            sample_count: 1,
114            dimension: wgpu::TextureDimension::D2,
115            format: wgpu::TextureFormat::Rgba8Unorm,
116            usage: wgpu::TextureUsages::TEXTURE_BINDING
117                | wgpu::TextureUsages::STORAGE_BINDING
118                | wgpu::TextureUsages::COPY_SRC,
119            view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
120        });
121
122        let input_view = input.create_view(&wgpu::TextureViewDescriptor::default());
123        let ntsc_view = ntsc_frame.create_view(&wgpu::TextureViewDescriptor::default());
124        let ntsc_pass_view = ntsc_pass.create_view(&wgpu::TextureViewDescriptor::default());
125        let ntsc_stop_view = ntsc_stop.create_view(&wgpu::TextureViewDescriptor::default());
126        let output_view = output_frame.create_view(&wgpu::TextureViewDescriptor::default());
127
128        let filters = NtscFilters::from(params.samples_per_color_cycle);
129
130        let luma_bsf_fir_buffer = create_fir_buffer(device, filters.luma_bsf);
131        let chroma_bpf_fir_buffer = create_fir_buffer(device, filters.chroma_bpf);
132        let y_encode_lpf_fir_buffer = create_fir_buffer(device, filters.y_encode_lpf);
133        let y_decode_lpf_fir_buffer = create_fir_buffer(device, filters.y_decode_lpf);
134        let uv_lpf_fir_buffer = create_fir_buffer(device, filters.uv_lpf);
135
136        let fir_len = match params.samples_per_color_cycle {
137            SamplesPerColorCycle::Fifteen => constants::FIR_LEN_15,
138            SamplesPerColorCycle::Twelve => constants::FIR_LEN_12,
139        };
140        let decode_hue_offset = match variant {
141            NtscShaderVariant::Rgb => 0.0,
142            NtscShaderVariant::NesPpu => 2.9 / 12.0 * 2.0 * std::f64::consts::PI,
143        };
144        let pipeline_compilation_options = wgpu::PipelineCompilationOptions {
145            constants: &[
146                ("samples_per_color_cycle", u32::from(params.samples_per_color_cycle).into()),
147                ("fir_len", fir_len.into()),
148                ("upscale_factor", params.upscale_factor.into()),
149                ("decode_hue_offset", decode_hue_offset),
150                ("decode_brightness", config.brightness),
151                ("decode_saturation", config.saturation),
152                ("decode_gamma", config.gamma),
153            ],
154            ..wgpu::PipelineCompilationOptions::default()
155        };
156
157        let initial_immediates_buffer = device.create_buffer(&wgpu::BufferDescriptor {
158            label: "ntsc_immediates_buffer".into(),
159            size: size_of::<ImmediateParams>() as u64,
160            usage: wgpu::BufferUsages::UNIFORM,
161            mapped_at_creation: false,
162        });
163
164        let immediates_bind_group_layout =
165            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
166                label: "ntsc_immediates_bind_group_layout".into(),
167                entries: &[wgpu::BindGroupLayoutEntry {
168                    binding: 0,
169                    visibility: wgpu::ShaderStages::COMPUTE,
170                    ty: wgpu::BindingType::Buffer {
171                        ty: wgpu::BufferBindingType::Uniform,
172                        has_dynamic_offset: false,
173                        min_binding_size: None,
174                    },
175                    count: None,
176                }],
177            });
178
179        let initial_immediates_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
180            label: "ntsc_immediates_bind_group".into(),
181            layout: &immediates_bind_group_layout,
182            entries: &[wgpu::BindGroupEntry {
183                binding: 0,
184                resource: wgpu::BindingResource::Buffer(
185                    initial_immediates_buffer.as_entire_buffer_binding(),
186                ),
187            }],
188        });
189
190        let rgb_to_ntsc_bind_group_layout =
191            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
192                label: "rgb_to_ntsc_bind_group_layout".into(),
193                entries: &[
194                    wgpu::BindGroupLayoutEntry {
195                        binding: 0,
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                    wgpu::BindGroupLayoutEntry {
205                        binding: 1,
206                        visibility: wgpu::ShaderStages::COMPUTE,
207                        ty: wgpu::BindingType::Buffer {
208                            ty: wgpu::BufferBindingType::Uniform,
209                            has_dynamic_offset: false,
210                            min_binding_size: None,
211                        },
212                        count: None,
213                    },
214                    wgpu::BindGroupLayoutEntry {
215                        binding: 2,
216                        visibility: wgpu::ShaderStages::COMPUTE,
217                        ty: wgpu::BindingType::Texture {
218                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
219                            view_dimension: wgpu::TextureViewDimension::D2,
220                            multisampled: false,
221                        },
222                        count: None,
223                    },
224                    wgpu::BindGroupLayoutEntry {
225                        binding: 3,
226                        visibility: wgpu::ShaderStages::COMPUTE,
227                        ty: wgpu::BindingType::StorageTexture {
228                            access: wgpu::StorageTextureAccess::WriteOnly,
229                            format: wgpu::TextureFormat::R32Float,
230                            view_dimension: wgpu::TextureViewDimension::D2,
231                        },
232                        count: None,
233                    },
234                ],
235            });
236
237        let rgb_to_ntsc_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
238            label: "rgb_to_ntsc_bind_group".into(),
239            layout: &rgb_to_ntsc_bind_group_layout,
240            entries: &[
241                wgpu::BindGroupEntry {
242                    binding: 0,
243                    resource: wgpu::BindingResource::Buffer(
244                        y_encode_lpf_fir_buffer.as_entire_buffer_binding(),
245                    ),
246                },
247                wgpu::BindGroupEntry {
248                    binding: 1,
249                    resource: wgpu::BindingResource::Buffer(
250                        uv_lpf_fir_buffer.as_entire_buffer_binding(),
251                    ),
252                },
253                wgpu::BindGroupEntry {
254                    binding: 2,
255                    resource: wgpu::BindingResource::TextureView(&input_view),
256                },
257                wgpu::BindGroupEntry {
258                    binding: 3,
259                    resource: wgpu::BindingResource::TextureView(&ntsc_view),
260                },
261            ],
262        });
263
264        let rgb_to_ntsc_pipeline_layout =
265            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
266                label: "rgb_to_ntsc_pipeline_layout".into(),
267                bind_group_layouts: &[
268                    Some(&rgb_to_ntsc_bind_group_layout),
269                    Some(&immediates_bind_group_layout),
270                ],
271                immediate_size: 0,
272            });
273
274        let rgb_to_ntsc_shader = match variant {
275            NtscShaderVariant::Rgb => "rgb_to_ntsc",
276            NtscShaderVariant::NesPpu => "nes_to_ntsc",
277        };
278        let rgb_to_ntsc_pipeline =
279            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
280                label: "rgb_to_ntsc_pipeline".into(),
281                layout: Some(&rgb_to_ntsc_pipeline_layout),
282                module: &shaders.ntsc,
283                entry_point: Some(rgb_to_ntsc_shader),
284                compilation_options: pipeline_compilation_options.clone(),
285                cache: None,
286            });
287
288        let separate_luma_chroma_pipeline =
289            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
290                label: "separate_luma_chroma_pipeline".into(),
291                layout: None,
292                module: &shaders.ntsc,
293                entry_point: Some("separate_luma_chroma"),
294                compilation_options: pipeline_compilation_options.clone(),
295                cache: None,
296            });
297
298        let separate_luma_chroma_bind_group =
299            device.create_bind_group(&wgpu::BindGroupDescriptor {
300                label: "separate_luma_chroma_bind_group".into(),
301                layout: &separate_luma_chroma_pipeline.get_bind_group_layout(0),
302                entries: &[
303                    wgpu::BindGroupEntry {
304                        binding: 4,
305                        resource: wgpu::BindingResource::Buffer(
306                            luma_bsf_fir_buffer.as_entire_buffer_binding(),
307                        ),
308                    },
309                    wgpu::BindGroupEntry {
310                        binding: 5,
311                        resource: wgpu::BindingResource::Buffer(
312                            chroma_bpf_fir_buffer.as_entire_buffer_binding(),
313                        ),
314                    },
315                    wgpu::BindGroupEntry {
316                        binding: 6,
317                        resource: wgpu::BindingResource::TextureView(&ntsc_view),
318                    },
319                    wgpu::BindGroupEntry {
320                        binding: 7,
321                        resource: wgpu::BindingResource::TextureView(&ntsc_pass_view),
322                    },
323                    wgpu::BindGroupEntry {
324                        binding: 8,
325                        resource: wgpu::BindingResource::TextureView(&ntsc_stop_view),
326                    },
327                ],
328            });
329
330        let luma_chroma_to_rgb_bind_group_layout =
331            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
332                label: "luma_chroma_to_rgb_bind_group_layout".into(),
333                entries: &[
334                    wgpu::BindGroupLayoutEntry {
335                        binding: 9,
336                        visibility: wgpu::ShaderStages::COMPUTE,
337                        ty: wgpu::BindingType::Buffer {
338                            ty: wgpu::BufferBindingType::Uniform,
339                            has_dynamic_offset: false,
340                            min_binding_size: None,
341                        },
342                        count: None,
343                    },
344                    wgpu::BindGroupLayoutEntry {
345                        binding: 10,
346                        visibility: wgpu::ShaderStages::COMPUTE,
347                        ty: wgpu::BindingType::Buffer {
348                            ty: wgpu::BufferBindingType::Uniform,
349                            has_dynamic_offset: false,
350                            min_binding_size: None,
351                        },
352                        count: None,
353                    },
354                    wgpu::BindGroupLayoutEntry {
355                        binding: 11,
356                        visibility: wgpu::ShaderStages::COMPUTE,
357                        ty: wgpu::BindingType::Texture {
358                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
359                            view_dimension: wgpu::TextureViewDimension::D2,
360                            multisampled: false,
361                        },
362                        count: None,
363                    },
364                    wgpu::BindGroupLayoutEntry {
365                        binding: 12,
366                        visibility: wgpu::ShaderStages::COMPUTE,
367                        ty: wgpu::BindingType::Texture {
368                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
369                            view_dimension: wgpu::TextureViewDimension::D2,
370                            multisampled: false,
371                        },
372                        count: None,
373                    },
374                    wgpu::BindGroupLayoutEntry {
375                        binding: 13,
376                        visibility: wgpu::ShaderStages::COMPUTE,
377                        ty: wgpu::BindingType::StorageTexture {
378                            access: wgpu::StorageTextureAccess::WriteOnly,
379                            format: wgpu::TextureFormat::Rgba8Unorm,
380                            view_dimension: wgpu::TextureViewDimension::D2,
381                        },
382                        count: None,
383                    },
384                ],
385            });
386
387        let luma_chroma_to_rgb_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
388            label: "luma_chroma_to_rgb_bind_group".into(),
389            layout: &luma_chroma_to_rgb_bind_group_layout,
390            entries: &[
391                wgpu::BindGroupEntry {
392                    binding: 9,
393                    resource: wgpu::BindingResource::Buffer(
394                        y_decode_lpf_fir_buffer.as_entire_buffer_binding(),
395                    ),
396                },
397                wgpu::BindGroupEntry {
398                    binding: 10,
399                    resource: wgpu::BindingResource::Buffer(
400                        uv_lpf_fir_buffer.as_entire_buffer_binding(),
401                    ),
402                },
403                wgpu::BindGroupEntry {
404                    binding: 11,
405                    resource: wgpu::BindingResource::TextureView(&ntsc_pass_view),
406                },
407                wgpu::BindGroupEntry {
408                    binding: 12,
409                    resource: wgpu::BindingResource::TextureView(&ntsc_stop_view),
410                },
411                wgpu::BindGroupEntry {
412                    binding: 13,
413                    resource: wgpu::BindingResource::TextureView(&output_view),
414                },
415            ],
416        });
417
418        let luma_chroma_to_rgb_pipeline_layout =
419            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
420                label: "luma_chroma_to_rgb_pipeline_layout".into(),
421                bind_group_layouts: &[
422                    Some(&luma_chroma_to_rgb_bind_group_layout),
423                    Some(&immediates_bind_group_layout),
424                ],
425                immediate_size: 0,
426            });
427
428        let luma_chroma_to_rgb_pipeline =
429            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
430                label: "luma_chroma_to_rgb_pipeline".into(),
431                layout: Some(&luma_chroma_to_rgb_pipeline_layout),
432                module: &shaders.ntsc,
433                entry_point: Some("luma_chroma_to_rgb"),
434                compilation_options: pipeline_compilation_options.clone(),
435                cache: None,
436            });
437
438        Self {
439            output: Arc::new(output_frame),
440            ntsc_frame_size: ntsc_texture_descriptor.size,
441            samples_per_color_cycle: params.samples_per_color_cycle,
442            immediates_bind_group_layout,
443            immediates_bind_group: initial_immediates_bind_group,
444            rgb_to_ntsc_bind_group,
445            rgb_to_ntsc_pipeline,
446            separate_luma_chroma_bind_group,
447            separate_luma_chroma_pipeline,
448            luma_chroma_to_rgb_bind_group,
449            luma_chroma_to_rgb_pipeline,
450        }
451    }
452}
453
454impl PipelineShader for NtscShader {
455    fn prepare(&mut self, device: &wgpu::Device, options: RenderFrameOptions) {
456        let immediate_params =
457            options.ntsc_per_frame_params.map_or(ImmediateParams::ZERO, |per_frame_params| {
458                let samples_per_color_cycle: u64 = self.samples_per_color_cycle.into();
459
460                ImmediateParams {
461                    frame_phase_offset: (per_frame_params.frame_phase_offset
462                        % samples_per_color_cycle) as i32,
463                    per_line_phase_offset: (per_frame_params.per_line_phase_offset
464                        % samples_per_color_cycle)
465                        as i32,
466                }
467            });
468
469        let immediates_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
470            label: "ntsc_immediates_buffer".into(),
471            contents: bytemuck::cast_slice(&[immediate_params]),
472            usage: wgpu::BufferUsages::UNIFORM,
473        });
474
475        self.immediates_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
476            label: "ntsc_immediates_bind_group".into(),
477            layout: &self.immediates_bind_group_layout,
478            entries: &[wgpu::BindGroupEntry {
479                binding: 0,
480                resource: wgpu::BindingResource::Buffer(
481                    immediates_buffer.as_entire_buffer_binding(),
482                ),
483            }],
484        });
485    }
486
487    fn draw(&mut self, encoder: &mut wgpu::CommandEncoder) {
488        let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
489            label: "ntsc_compute_pass".into(),
490            ..wgpu::ComputePassDescriptor::default()
491        });
492
493        let ntsc_x_workgroups = self.ntsc_frame_size.width / 16
494            + u32::from(!self.ntsc_frame_size.width.is_multiple_of(16));
495        let output_x_workgroups =
496            self.output.width() / 16 + u32::from(!self.output.width().is_multiple_of(16));
497        let y_workgroups =
498            self.output.height() / 16 + u32::from(!self.output.height().is_multiple_of(16));
499
500        compute_pass.set_bind_group(1, &self.immediates_bind_group, &[]);
501
502        compute_pass.set_bind_group(0, &self.rgb_to_ntsc_bind_group, &[]);
503        compute_pass.set_pipeline(&self.rgb_to_ntsc_pipeline);
504        compute_pass.dispatch_workgroups(ntsc_x_workgroups, y_workgroups, 1);
505
506        compute_pass.set_bind_group(0, &self.separate_luma_chroma_bind_group, &[]);
507        compute_pass.set_pipeline(&self.separate_luma_chroma_pipeline);
508        compute_pass.dispatch_workgroups(ntsc_x_workgroups, y_workgroups, 1);
509
510        compute_pass.set_bind_group(0, &self.luma_chroma_to_rgb_bind_group, &[]);
511        compute_pass.set_pipeline(&self.luma_chroma_to_rgb_pipeline);
512        compute_pass.dispatch_workgroups(output_x_workgroups, y_workgroups, 1);
513    }
514
515    fn output_texture(&self) -> &Arc<wgpu::Texture> {
516        &self.output
517    }
518}
519
520fn create_fir_buffer(device: &wgpu::Device, coefficients: &[f32]) -> wgpu::Buffer {
521    let mut fir: Vec<f32> = vec![0.0; FIR_BUFFER_LEN];
522
523    let slice_len = coefficients.len().min(FIR_BUFFER_LEN);
524    fir[..slice_len].copy_from_slice(&coefficients[..slice_len]);
525
526    device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
527        label: None,
528        contents: bytemuck::cast_slice(&fir),
529        usage: wgpu::BufferUsages::UNIFORM,
530    })
531}