1use wgpu::util::DeviceExt;
2
3use super::gpu::GpuContext;
4
5const MAX_EFFECT_PARAMS: usize = 4;
7const PARAM_FLOATS: usize = 20;
9
10#[derive(Clone, Copy, Debug)]
11pub enum EffectType {
12 Bloom,
13 Blur,
14 Vignette,
15 Crt,
16}
17
18impl EffectType {
19 pub fn from_str(s: &str) -> Option<Self> {
20 match s {
21 "bloom" => Some(EffectType::Bloom),
22 "blur" => Some(EffectType::Blur),
23 "vignette" => Some(EffectType::Vignette),
24 "crt" => Some(EffectType::Crt),
25 _ => None,
26 }
27 }
28
29 fn fragment_source(&self) -> &'static str {
30 match self {
31 EffectType::Bloom => BLOOM_FRAGMENT,
32 EffectType::Blur => BLUR_FRAGMENT,
33 EffectType::Vignette => VIGNETTE_FRAGMENT,
34 EffectType::Crt => CRT_FRAGMENT,
35 }
36 }
37
38 fn defaults(&self) -> [f32; PARAM_FLOATS] {
40 let mut d = [0.0f32; PARAM_FLOATS];
41 match self {
42 EffectType::Bloom => {
43 d[4] = 0.7;
45 d[5] = 0.5;
46 d[6] = 3.0;
47 }
48 EffectType::Blur => {
49 d[4] = 1.0;
51 }
52 EffectType::Vignette => {
53 d[4] = 0.5;
55 d[5] = 0.8;
56 }
57 EffectType::Crt => {
58 d[4] = 800.0;
60 d[5] = 0.1;
61 d[6] = 1.1;
62 }
63 }
64 d
65 }
66}
67
68struct EffectEntry {
69 #[allow(dead_code)]
70 effect_type: EffectType,
71 pipeline: wgpu::RenderPipeline,
72 param_buffer: wgpu::Buffer,
73 param_bind_group: wgpu::BindGroup,
74 param_data: [f32; PARAM_FLOATS],
75}
76
77struct OffscreenTarget {
78 #[allow(dead_code)]
79 texture: wgpu::Texture,
80 view: wgpu::TextureView,
81 bind_group: wgpu::BindGroup,
82 width: u32,
83 height: u32,
84}
85
86pub struct PostProcessPipeline {
89 effects: Vec<(u32, EffectEntry)>,
91 target_a: Option<OffscreenTarget>,
93 target_b: Option<OffscreenTarget>,
94 texture_bind_group_layout: wgpu::BindGroupLayout,
96 params_bind_group_layout: wgpu::BindGroupLayout,
97 pipeline_layout: wgpu::PipelineLayout,
98 sampler: wgpu::Sampler,
99 surface_format: wgpu::TextureFormat,
100}
101
102impl PostProcessPipeline {
103 pub fn new(gpu: &GpuContext) -> Self {
104 let texture_bind_group_layout =
106 gpu.device
107 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
108 label: Some("postprocess_texture_layout"),
109 entries: &[
110 wgpu::BindGroupLayoutEntry {
111 binding: 0,
112 visibility: wgpu::ShaderStages::FRAGMENT,
113 ty: wgpu::BindingType::Texture {
114 multisampled: false,
115 view_dimension: wgpu::TextureViewDimension::D2,
116 sample_type: wgpu::TextureSampleType::Float {
117 filterable: true,
118 },
119 },
120 count: None,
121 },
122 wgpu::BindGroupLayoutEntry {
123 binding: 1,
124 visibility: wgpu::ShaderStages::FRAGMENT,
125 ty: wgpu::BindingType::Sampler(
126 wgpu::SamplerBindingType::Filtering,
127 ),
128 count: None,
129 },
130 ],
131 });
132
133 let params_bind_group_layout =
135 gpu.device
136 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
137 label: Some("postprocess_params_layout"),
138 entries: &[wgpu::BindGroupLayoutEntry {
139 binding: 0,
140 visibility: wgpu::ShaderStages::FRAGMENT,
141 ty: wgpu::BindingType::Buffer {
142 ty: wgpu::BufferBindingType::Uniform,
143 has_dynamic_offset: false,
144 min_binding_size: None,
145 },
146 count: None,
147 }],
148 });
149
150 let pipeline_layout =
151 gpu.device
152 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
153 label: Some("postprocess_pipeline_layout"),
154 bind_group_layouts: &[
155 &texture_bind_group_layout,
156 ¶ms_bind_group_layout,
157 ],
158 push_constant_ranges: &[],
159 });
160
161 let sampler = gpu.device.create_sampler(&wgpu::SamplerDescriptor {
162 label: Some("postprocess_sampler"),
163 address_mode_u: wgpu::AddressMode::ClampToEdge,
164 address_mode_v: wgpu::AddressMode::ClampToEdge,
165 mag_filter: wgpu::FilterMode::Linear,
166 min_filter: wgpu::FilterMode::Linear,
167 ..Default::default()
168 });
169
170 Self {
171 effects: Vec::new(),
172 target_a: None,
173 target_b: None,
174 texture_bind_group_layout,
175 params_bind_group_layout,
176 pipeline_layout,
177 sampler,
178 surface_format: gpu.config.format,
179 }
180 }
181
182 pub fn has_effects(&self) -> bool {
184 !self.effects.is_empty()
185 }
186
187 pub fn add(&mut self, gpu: &GpuContext, id: u32, effect_type: EffectType) {
189 let wgsl = build_effect_wgsl(effect_type.fragment_source());
190
191 let shader_module =
192 gpu.device
193 .create_shader_module(wgpu::ShaderModuleDescriptor {
194 label: Some("postprocess_shader"),
195 source: wgpu::ShaderSource::Wgsl(wgsl.into()),
196 });
197
198 let pipeline =
199 gpu.device
200 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
201 label: Some("postprocess_pipeline"),
202 layout: Some(&self.pipeline_layout),
203 vertex: wgpu::VertexState {
204 module: &shader_module,
205 entry_point: Some("vs_main"),
206 buffers: &[], compilation_options: Default::default(),
208 },
209 fragment: Some(wgpu::FragmentState {
210 module: &shader_module,
211 entry_point: Some("fs_main"),
212 targets: &[Some(wgpu::ColorTargetState {
213 format: self.surface_format,
214 blend: None,
215 write_mask: wgpu::ColorWrites::ALL,
216 })],
217 compilation_options: Default::default(),
218 }),
219 primitive: wgpu::PrimitiveState {
220 topology: wgpu::PrimitiveTopology::TriangleList,
221 ..Default::default()
222 },
223 depth_stencil: None,
224 multisample: wgpu::MultisampleState::default(),
225 multiview: None,
226 cache: None,
227 });
228
229 let param_data = effect_type.defaults();
230
231 let param_buffer =
232 gpu.device
233 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
234 label: Some("postprocess_param_buffer"),
235 contents: bytemuck::cast_slice(¶m_data),
236 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
237 });
238
239 let param_bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
240 label: Some("postprocess_param_bind_group"),
241 layout: &self.params_bind_group_layout,
242 entries: &[wgpu::BindGroupEntry {
243 binding: 0,
244 resource: param_buffer.as_entire_binding(),
245 }],
246 });
247
248 self.effects.push((
249 id,
250 EffectEntry {
251 effect_type,
252 pipeline,
253 param_buffer,
254 param_bind_group,
255 param_data,
256 },
257 ));
258 }
259
260 pub fn set_param(&mut self, id: u32, index: u32, x: f32, y: f32, z: f32, w: f32) {
262 if let Some((_, entry)) = self.effects.iter_mut().find(|(eid, _)| *eid == id) {
263 let base = 4 + (index as usize).min(MAX_EFFECT_PARAMS - 1) * 4;
264 entry.param_data[base] = x;
265 entry.param_data[base + 1] = y;
266 entry.param_data[base + 2] = z;
267 entry.param_data[base + 3] = w;
268 }
269 }
270
271 pub fn remove(&mut self, id: u32) {
273 self.effects.retain(|(eid, _)| *eid != id);
274 }
275
276 pub fn clear(&mut self) {
278 self.effects.clear();
279 }
280
281 fn ensure_targets(&mut self, gpu: &GpuContext) {
283 let w = gpu.config.width;
284 let h = gpu.config.height;
285
286 let needs_recreate = self
287 .target_a
288 .as_ref()
289 .map(|t| t.width != w || t.height != h)
290 .unwrap_or(true);
291
292 if needs_recreate {
293 self.target_a = Some(self.create_target(gpu, w, h, "postprocess_a"));
294 self.target_b = Some(self.create_target(gpu, w, h, "postprocess_b"));
295 }
296 }
297
298 fn create_target(
299 &self,
300 gpu: &GpuContext,
301 width: u32,
302 height: u32,
303 label: &str,
304 ) -> OffscreenTarget {
305 let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
306 label: Some(label),
307 size: wgpu::Extent3d {
308 width,
309 height,
310 depth_or_array_layers: 1,
311 },
312 mip_level_count: 1,
313 sample_count: 1,
314 dimension: wgpu::TextureDimension::D2,
315 format: self.surface_format,
316 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
317 | wgpu::TextureUsages::TEXTURE_BINDING,
318 view_formats: &[],
319 });
320
321 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
322
323 let bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
324 label: Some(&format!("{label}_bind_group")),
325 layout: &self.texture_bind_group_layout,
326 entries: &[
327 wgpu::BindGroupEntry {
328 binding: 0,
329 resource: wgpu::BindingResource::TextureView(&view),
330 },
331 wgpu::BindGroupEntry {
332 binding: 1,
333 resource: wgpu::BindingResource::Sampler(&self.sampler),
334 },
335 ],
336 });
337
338 OffscreenTarget {
339 texture,
340 view,
341 bind_group,
342 width,
343 height,
344 }
345 }
346
347 pub fn sprite_target(&mut self, gpu: &GpuContext) -> &wgpu::TextureView {
350 self.ensure_targets(gpu);
351 &self.target_a.as_ref().unwrap().view
352 }
353
354 pub fn apply(
357 &mut self,
358 gpu: &GpuContext,
359 encoder: &mut wgpu::CommandEncoder,
360 surface_view: &wgpu::TextureView,
361 ) {
362 let n = self.effects.len();
363 if n == 0 {
364 return;
365 }
366
367 let resolution = [gpu.config.width as f32, gpu.config.height as f32];
368
369 for (_, entry) in self.effects.iter_mut() {
371 entry.param_data[0] = resolution[0];
372 entry.param_data[1] = resolution[1];
373 gpu.queue.write_buffer(
374 &entry.param_buffer,
375 0,
376 bytemuck::cast_slice(&entry.param_data),
377 );
378 }
379
380 for i in 0..n {
385 let is_last = i == n - 1;
386
387 let source_bg = if i % 2 == 0 {
389 &self.target_a.as_ref().unwrap().bind_group
390 } else {
391 &self.target_b.as_ref().unwrap().bind_group
392 };
393
394 let dest_view = if is_last {
396 surface_view
397 } else if i % 2 == 0 {
398 &self.target_b.as_ref().unwrap().view
399 } else {
400 &self.target_a.as_ref().unwrap().view
401 };
402
403 let (_, entry) = &self.effects[i];
404
405 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
406 label: Some("postprocess_pass"),
407 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
408 view: dest_view,
409 resolve_target: None,
410 ops: wgpu::Operations {
411 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
412 store: wgpu::StoreOp::Store,
413 },
414 })],
415 depth_stencil_attachment: None,
416 timestamp_writes: None,
417 occlusion_query_set: None,
418 });
419
420 pass.set_pipeline(&entry.pipeline);
421 pass.set_bind_group(0, source_bg, &[]);
422 pass.set_bind_group(1, &entry.param_bind_group, &[]);
423 pass.draw(0..3, 0..1); }
425 }
426}
427
428fn build_effect_wgsl(fragment_source: &str) -> String {
430 format!("{}\n{}\n", EFFECT_PREAMBLE, fragment_source)
431}
432
433const EFFECT_PREAMBLE: &str = r#"
435@group(0) @binding(0)
436var t_input: texture_2d<f32>;
437
438@group(0) @binding(1)
439var s_input: sampler;
440
441struct EffectParams {
442 resolution: vec4<f32>,
443 values: array<vec4<f32>, 4>,
444};
445
446@group(1) @binding(0)
447var<uniform> params: EffectParams;
448
449struct VertexOutput {
450 @builtin(position) position: vec4<f32>,
451 @location(0) uv: vec2<f32>,
452};
453
454@vertex
455fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput {
456 // Fullscreen triangle: 3 vertices cover clip space [-1,1]
457 var out: VertexOutput;
458 let uv = vec2<f32>(f32((idx << 1u) & 2u), f32(idx & 2u));
459 out.position = vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
460 out.uv = vec2<f32>(uv.x, 1.0 - uv.y);
461 return out;
462}
463"#;
464
465const BLOOM_FRAGMENT: &str = r#"
468@fragment
469fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
470 let resolution = params.resolution.xy;
471 let threshold = params.values[0].x;
472 let intensity = params.values[0].y;
473 let radius = params.values[0].z;
474
475 let texel = 1.0 / resolution;
476 let original = textureSample(t_input, s_input, in.uv);
477
478 // 3x3 Gaussian-weighted bright-pass blur
479 var bloom = vec3<f32>(0.0);
480
481 let s00 = textureSample(t_input, s_input, in.uv + vec2<f32>(-1.0, -1.0) * texel * radius).rgb;
482 let s10 = textureSample(t_input, s_input, in.uv + vec2<f32>( 0.0, -1.0) * texel * radius).rgb;
483 let s20 = textureSample(t_input, s_input, in.uv + vec2<f32>( 1.0, -1.0) * texel * radius).rgb;
484 let s01 = textureSample(t_input, s_input, in.uv + vec2<f32>(-1.0, 0.0) * texel * radius).rgb;
485 let s11 = textureSample(t_input, s_input, in.uv).rgb;
486 let s21 = textureSample(t_input, s_input, in.uv + vec2<f32>( 1.0, 0.0) * texel * radius).rgb;
487 let s02 = textureSample(t_input, s_input, in.uv + vec2<f32>(-1.0, 1.0) * texel * radius).rgb;
488 let s12 = textureSample(t_input, s_input, in.uv + vec2<f32>( 0.0, 1.0) * texel * radius).rgb;
489 let s22 = textureSample(t_input, s_input, in.uv + vec2<f32>( 1.0, 1.0) * texel * radius).rgb;
490
491 let lum = vec3<f32>(0.2126, 0.7152, 0.0722);
492 bloom += max(dot(s00, lum) - threshold, 0.0) * s00 * 0.0625;
493 bloom += max(dot(s10, lum) - threshold, 0.0) * s10 * 0.125;
494 bloom += max(dot(s20, lum) - threshold, 0.0) * s20 * 0.0625;
495 bloom += max(dot(s01, lum) - threshold, 0.0) * s01 * 0.125;
496 bloom += max(dot(s11, lum) - threshold, 0.0) * s11 * 0.25;
497 bloom += max(dot(s21, lum) - threshold, 0.0) * s21 * 0.125;
498 bloom += max(dot(s02, lum) - threshold, 0.0) * s02 * 0.0625;
499 bloom += max(dot(s12, lum) - threshold, 0.0) * s12 * 0.125;
500 bloom += max(dot(s22, lum) - threshold, 0.0) * s22 * 0.0625;
501
502 return vec4<f32>(original.rgb + bloom * intensity, original.a);
503}
504"#;
505
506const BLUR_FRAGMENT: &str = r#"
509@fragment
510fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
511 let resolution = params.resolution.xy;
512 let strength = params.values[0].x;
513
514 let texel = 1.0 / resolution * strength;
515
516 var color = vec4<f32>(0.0);
517 color += textureSample(t_input, s_input, in.uv + vec2<f32>(-1.0, -1.0) * texel) * 0.0625;
518 color += textureSample(t_input, s_input, in.uv + vec2<f32>( 0.0, -1.0) * texel) * 0.125;
519 color += textureSample(t_input, s_input, in.uv + vec2<f32>( 1.0, -1.0) * texel) * 0.0625;
520 color += textureSample(t_input, s_input, in.uv + vec2<f32>(-1.0, 0.0) * texel) * 0.125;
521 color += textureSample(t_input, s_input, in.uv) * 0.25;
522 color += textureSample(t_input, s_input, in.uv + vec2<f32>( 1.0, 0.0) * texel) * 0.125;
523 color += textureSample(t_input, s_input, in.uv + vec2<f32>(-1.0, 1.0) * texel) * 0.0625;
524 color += textureSample(t_input, s_input, in.uv + vec2<f32>( 0.0, 1.0) * texel) * 0.125;
525 color += textureSample(t_input, s_input, in.uv + vec2<f32>( 1.0, 1.0) * texel) * 0.0625;
526
527 return color;
528}
529"#;
530
531const VIGNETTE_FRAGMENT: &str = r#"
534@fragment
535fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
536 let intensity = params.values[0].x;
537 let radius = params.values[0].y;
538
539 let original = textureSample(t_input, s_input, in.uv);
540
541 let center = in.uv - vec2<f32>(0.5);
542 let dist = length(center) * 1.414;
543 let vignette = smoothstep(radius, radius - 0.3, dist);
544 let factor = mix(1.0, vignette, intensity);
545
546 return vec4<f32>(original.rgb * factor, original.a);
547}
548"#;
549
550const CRT_FRAGMENT: &str = r#"
553@fragment
554fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
555 let scanline_freq = params.values[0].x;
556 let distortion = params.values[0].y;
557 let brightness = params.values[0].z;
558
559 // Barrel distortion
560 let center = in.uv - vec2<f32>(0.5);
561 let dist2 = dot(center, center);
562 let distorted_uv = in.uv + center * dist2 * distortion;
563
564 // Black outside screen bounds
565 if distorted_uv.x < 0.0 || distorted_uv.x > 1.0 || distorted_uv.y < 0.0 || distorted_uv.y > 1.0 {
566 return vec4<f32>(0.0, 0.0, 0.0, 1.0);
567 }
568
569 let original = textureSample(t_input, s_input, distorted_uv);
570
571 // Scanlines
572 let scanline = sin(distorted_uv.y * scanline_freq) * 0.5 + 0.5;
573 let scanline_effect = mix(0.8, 1.0, scanline);
574
575 // Chromatic aberration (subtle RGB offset at edges)
576 let ca_offset = center * dist2 * 0.003;
577 let r = textureSample(t_input, s_input, distorted_uv + ca_offset).r;
578 let g = original.g;
579 let b = textureSample(t_input, s_input, distorted_uv - ca_offset).b;
580
581 return vec4<f32>(vec3<f32>(r, g, b) * scanline_effect * brightness, original.a);
582}
583"#;