1use crate::buffer::GpuBuffer;
7use crate::context::GpuContext;
8use crate::error::{GpuError, GpuResult};
9use crate::shaders::{
10 ComputePipelineBuilder, WgslShader, create_compute_bind_group_layout, storage_buffer_layout,
11 uniform_buffer_layout,
12};
13use bytemuck::{Pod, Zeroable};
14use tracing::debug;
15use wgpu::{
16 BindGroupDescriptor, BindGroupEntry, BufferUsages, CommandEncoderDescriptor,
17 ComputePassDescriptor, ComputePipeline,
18};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ResamplingMethod {
23 NearestNeighbor,
25 Bilinear,
27 Bicubic,
29 Lanczos {
37 a: u32,
39 },
40}
41
42impl ResamplingMethod {
43 pub fn entry_point(&self) -> &'static str {
45 match self {
46 Self::NearestNeighbor => "nearest_neighbor",
47 Self::Bilinear => "bilinear",
48 Self::Bicubic => "bicubic",
49 Self::Lanczos { .. } => "lanczos",
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, Pod, Zeroable)]
56#[repr(C)]
57pub struct ResamplingParams {
58 pub src_width: u32,
60 pub src_height: u32,
62 pub dst_width: u32,
64 pub dst_height: u32,
66}
67
68impl ResamplingParams {
69 pub fn new(src_width: u32, src_height: u32, dst_width: u32, dst_height: u32) -> Self {
71 Self {
72 src_width,
73 src_height,
74 dst_width,
75 dst_height,
76 }
77 }
78
79 pub fn scale_factors(&self) -> (f32, f32) {
81 let scale_x = self.src_width as f32 / self.dst_width as f32;
82 let scale_y = self.src_height as f32 / self.dst_height as f32;
83 (scale_x, scale_y)
84 }
85}
86
87pub struct ResamplingKernel {
89 context: GpuContext,
90 pipeline: ComputePipeline,
91 bind_group_layout: wgpu::BindGroupLayout,
92 workgroup_size: (u32, u32),
93 method: ResamplingMethod,
94}
95
96impl ResamplingKernel {
97 pub fn new(context: &GpuContext, method: ResamplingMethod) -> GpuResult<Self> {
107 let workgroup_size = context.tuner.raster_2d;
108 Self::new_with_workgroup_size(context, method, workgroup_size)
109 }
110
111 pub fn new_with_workgroup_size(
121 context: &GpuContext,
122 method: ResamplingMethod,
123 workgroup_size: (u32, u32),
124 ) -> GpuResult<Self> {
125 debug!(
126 "Creating resampling kernel: {:?} (workgroup {}x{})",
127 method, workgroup_size.0, workgroup_size.1
128 );
129
130 if let ResamplingMethod::Lanczos { a } = method {
132 if a == 0 || a > 8 {
133 return Err(GpuError::invalid_kernel_params(format!(
134 "Lanczos window parameter a={a} out of valid range 1..=8"
135 )));
136 }
137 }
138
139 let shader_source = Self::resampling_shader(method, workgroup_size);
140 let mut shader = WgslShader::new(shader_source, method.entry_point());
141 let shader_module = shader.compile(context.device())?;
142
143 let bind_group_layout = create_compute_bind_group_layout(
144 context.device(),
145 &[
146 storage_buffer_layout(0, true), uniform_buffer_layout(1), storage_buffer_layout(2, false), ],
150 Some("ResamplingKernel BindGroupLayout"),
151 )?;
152
153 let pipeline =
154 ComputePipelineBuilder::new(context.device(), shader_module, method.entry_point())
155 .bind_group_layout(&bind_group_layout)
156 .label(format!("ResamplingKernel Pipeline: {:?}", method))
157 .build()?;
158
159 Ok(Self {
160 context: context.clone(),
161 pipeline,
162 bind_group_layout,
163 workgroup_size,
164 method,
165 })
166 }
167
168 fn resampling_shader(method: ResamplingMethod, workgroup_size: (u32, u32)) -> String {
177 let (wg_x, wg_y) = workgroup_size;
178
179 let common = r#"
180struct ResamplingParams {
181 src_width: u32,
182 src_height: u32,
183 dst_width: u32,
184 dst_height: u32,
185}
186
187@group(0) @binding(0) var<storage, read> input: array<f32>;
188@group(0) @binding(1) var<uniform> params: ResamplingParams;
189@group(0) @binding(2) var<storage, read_write> output: array<f32>;
190
191fn get_pixel(x: u32, y: u32) -> f32 {
192 if (x >= params.src_width || y >= params.src_height) {
193 return 0.0;
194 }
195 return input[y * params.src_width + x];
196}
197
198fn lerp(a: f32, b: f32, t: f32) -> f32 {
199 return a + (b - a) * t;
200}
201"#;
202
203 match method {
204 ResamplingMethod::NearestNeighbor => format!(
205 r#"
206{}
207
208@compute @workgroup_size({wg_x}, {wg_y})
209fn nearest_neighbor(@builtin(global_invocation_id) global_id: vec3<u32>) {{
210 let dst_x = global_id.x;
211 let dst_y = global_id.y;
212
213 if (dst_x >= params.dst_width || dst_y >= params.dst_height) {{
214 return;
215 }}
216
217 let scale_x = f32(params.src_width) / f32(params.dst_width);
218 let scale_y = f32(params.src_height) / f32(params.dst_height);
219
220 let src_x = u32(f32(dst_x) * scale_x);
221 let src_y = u32(f32(dst_y) * scale_y);
222
223 let value = get_pixel(src_x, src_y);
224 output[dst_y * params.dst_width + dst_x] = value;
225}}
226"#,
227 common,
228 wg_x = wg_x,
229 wg_y = wg_y,
230 ),
231
232 ResamplingMethod::Bilinear => format!(
233 r#"
234{}
235
236@compute @workgroup_size({wg_x}, {wg_y})
237fn bilinear(@builtin(global_invocation_id) global_id: vec3<u32>) {{
238 let dst_x = global_id.x;
239 let dst_y = global_id.y;
240
241 if (dst_x >= params.dst_width || dst_y >= params.dst_height) {{
242 return;
243 }}
244
245 let scale_x = f32(params.src_width) / f32(params.dst_width);
246 let scale_y = f32(params.src_height) / f32(params.dst_height);
247
248 let src_x = f32(dst_x) * scale_x;
249 let src_y = f32(dst_y) * scale_y;
250
251 let x0 = u32(floor(src_x));
252 let y0 = u32(floor(src_y));
253 let x1 = min(x0 + 1u, params.src_width - 1u);
254 let y1 = min(y0 + 1u, params.src_height - 1u);
255
256 let tx = fract(src_x);
257 let ty = fract(src_y);
258
259 let v00 = get_pixel(x0, y0);
260 let v10 = get_pixel(x1, y0);
261 let v01 = get_pixel(x0, y1);
262 let v11 = get_pixel(x1, y1);
263
264 let v0 = lerp(v00, v10, tx);
265 let v1 = lerp(v01, v11, tx);
266 let value = lerp(v0, v1, ty);
267
268 output[dst_y * params.dst_width + dst_x] = value;
269}}
270"#,
271 common,
272 wg_x = wg_x,
273 wg_y = wg_y,
274 ),
275
276 ResamplingMethod::Bicubic => format!(
277 r#"
278{}
279
280fn cubic_interpolate(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 {{
281 let a = -0.5 * p0 + 1.5 * p1 - 1.5 * p2 + 0.5 * p3;
282 let b = p0 - 2.5 * p1 + 2.0 * p2 - 0.5 * p3;
283 let c = -0.5 * p0 + 0.5 * p2;
284 let d = p1;
285 return a * t * t * t + b * t * t + c * t + d;
286}}
287
288@compute @workgroup_size({wg_x}, {wg_y})
289fn bicubic(@builtin(global_invocation_id) global_id: vec3<u32>) {{
290 let dst_x = global_id.x;
291 let dst_y = global_id.y;
292
293 if (dst_x >= params.dst_width || dst_y >= params.dst_height) {{
294 return;
295 }}
296
297 let scale_x = f32(params.src_width) / f32(params.dst_width);
298 let scale_y = f32(params.src_height) / f32(params.dst_height);
299
300 let src_x = f32(dst_x) * scale_x;
301 let src_y = f32(dst_y) * scale_y;
302
303 let x_floor = floor(src_x);
304 let y_floor = floor(src_y);
305 let tx = fract(src_x);
306 let ty = fract(src_y);
307
308 // Sample 4x4 neighborhood
309 var cols: array<f32, 4>;
310 for (var j = 0; j < 4; j++) {{
311 let y = i32(y_floor) + j - 1;
312 var row: array<f32, 4>;
313 for (var i = 0; i < 4; i++) {{
314 let x = i32(x_floor) + i - 1;
315 if (x >= 0 && x < i32(params.src_width) && y >= 0 && y < i32(params.src_height)) {{
316 row[i] = get_pixel(u32(x), u32(y));
317 }} else {{
318 row[i] = 0.0;
319 }}
320 }}
321 cols[j] = cubic_interpolate(row[0], row[1], row[2], row[3], tx);
322 }}
323
324 let value = cubic_interpolate(cols[0], cols[1], cols[2], cols[3], ty);
325 output[dst_y * params.dst_width + dst_x] = value;
326}}
327"#,
328 common,
329 wg_x = wg_x,
330 wg_y = wg_y,
331 ),
332
333 ResamplingMethod::Lanczos { a } => {
334 let two_a = 2 * a;
336 format!(
337 r#"
338{common}
339
340// sinc(x) = sin(pi*x) / (pi*x), with sinc(0) = 1.
341fn sinc(x: f32) -> f32 {{
342 if (abs(x) < 1e-6) {{
343 return 1.0;
344 }}
345 let pi_x: f32 = 3.14159265358979323846 * x;
346 return sin(pi_x) / pi_x;
347}}
348
349// Lanczos weighting function with window radius a.
350// L(x) = sinc(x) * sinc(x/a) for |x| < a, else 0.
351fn lanczos_weight(x: f32, a: f32) -> f32 {{
352 if (abs(x) >= a) {{
353 return 0.0;
354 }}
355 return sinc(x) * sinc(x / a);
356}}
357
358@compute @workgroup_size({wg_x}, {wg_y})
359fn lanczos(@builtin(global_invocation_id) global_id: vec3<u32>) {{
360 let dst_x = global_id.x;
361 let dst_y = global_id.y;
362
363 if (dst_x >= params.dst_width || dst_y >= params.dst_height) {{
364 return;
365 }}
366
367 let scale_x = f32(params.src_width) / f32(params.dst_width);
368 let scale_y = f32(params.src_height) / f32(params.dst_height);
369
370 // Map destination pixel centre to source coordinate space.
371 // The +0.5 / -0.5 shift aligns pixel centres correctly.
372 let src_cx = (f32(dst_x) + 0.5) * scale_x - 0.5;
373 let src_cy = (f32(dst_y) + 0.5) * scale_y - 0.5;
374
375 // Window radius injected at shader-build time.
376 let lanczos_a: f32 = {a}f;
377
378 var weight_sum: f32 = 0.0;
379 var value_sum: f32 = 0.0;
380
381 // Sample 2*a rows and 2*a columns centred on src_c(x,y).
382 // Row/col index starts at floor(src_c) - a + 1 and runs for 2*a steps.
383 let x_start: i32 = i32(floor(src_cx)) - i32({a}) + 1;
384 let y_start: i32 = i32(floor(src_cy)) - i32({a}) + 1;
385
386 for (var j: i32 = 0; j < {two_a}; j++) {{
387 let sy: i32 = y_start + j;
388 let wy: f32 = lanczos_weight(src_cy - f32(sy), lanczos_a);
389 if (wy == 0.0) {{ continue; }}
390
391 // Clamp-to-edge: keep source coordinate inside the image.
392 let clamped_y: u32 = u32(clamp(sy, 0, i32(params.src_height) - 1));
393
394 for (var i: i32 = 0; i < {two_a}; i++) {{
395 let sx: i32 = x_start + i;
396 let wx: f32 = lanczos_weight(src_cx - f32(sx), lanczos_a);
397 if (wx == 0.0) {{ continue; }}
398
399 let clamped_x: u32 = u32(clamp(sx, 0, i32(params.src_width) - 1));
400
401 let w: f32 = wx * wy;
402 weight_sum += w;
403 value_sum += w * get_pixel(clamped_x, clamped_y);
404 }}
405 }}
406
407 // Normalise to preserve overall brightness; guard against zero weight sum.
408 let result: f32 = select(0.0, value_sum / weight_sum, weight_sum > 1e-10);
409 output[dst_y * params.dst_width + dst_x] = result;
410}}
411"#,
412 a = a,
413 two_a = two_a,
414 wg_x = wg_x,
415 wg_y = wg_y,
416 )
417 }
418 }
419 }
420
421 pub fn execute<T: Pod>(
427 &self,
428 input: &GpuBuffer<T>,
429 params: ResamplingParams,
430 ) -> GpuResult<GpuBuffer<T>> {
431 let expected_input_size = (params.src_width as usize) * (params.src_height as usize);
433 if input.len() != expected_input_size {
434 return Err(GpuError::invalid_kernel_params(format!(
435 "Input buffer size mismatch: expected {}, got {}",
436 expected_input_size,
437 input.len()
438 )));
439 }
440
441 let output_size = (params.dst_width as usize) * (params.dst_height as usize);
443 let output = GpuBuffer::new(
444 &self.context,
445 output_size,
446 BufferUsages::STORAGE | BufferUsages::COPY_SRC,
447 )?;
448
449 let params_buffer = GpuBuffer::from_data(
451 &self.context,
452 &[params],
453 BufferUsages::UNIFORM | BufferUsages::COPY_DST,
454 )?;
455
456 let bind_group = self
458 .context
459 .device()
460 .create_bind_group(&BindGroupDescriptor {
461 label: Some("ResamplingKernel BindGroup"),
462 layout: &self.bind_group_layout,
463 entries: &[
464 BindGroupEntry {
465 binding: 0,
466 resource: input.buffer().as_entire_binding(),
467 },
468 BindGroupEntry {
469 binding: 1,
470 resource: params_buffer.buffer().as_entire_binding(),
471 },
472 BindGroupEntry {
473 binding: 2,
474 resource: output.buffer().as_entire_binding(),
475 },
476 ],
477 });
478
479 let mut encoder = self
481 .context
482 .device()
483 .create_command_encoder(&CommandEncoderDescriptor {
484 label: Some("ResamplingKernel Encoder"),
485 });
486
487 {
488 let mut compute_pass = encoder.begin_compute_pass(&ComputePassDescriptor {
489 label: Some("ResamplingKernel Pass"),
490 timestamp_writes: None,
491 });
492
493 compute_pass.set_pipeline(&self.pipeline);
494 compute_pass.set_bind_group(0, &bind_group, &[]);
495
496 let workgroups_x =
497 (params.dst_width + self.workgroup_size.0 - 1) / self.workgroup_size.0;
498 let workgroups_y =
499 (params.dst_height + self.workgroup_size.1 - 1) / self.workgroup_size.1;
500
501 compute_pass.dispatch_workgroups(workgroups_x, workgroups_y, 1);
502 }
503
504 self.context.queue().submit(Some(encoder.finish()));
505
506 debug!(
507 "Resampled {}x{} to {}x{} using {:?}",
508 params.src_width, params.src_height, params.dst_width, params.dst_height, self.method
509 );
510
511 Ok(output)
512 }
513}
514
515pub fn resize<T: Pod>(
521 context: &GpuContext,
522 input: &GpuBuffer<T>,
523 src_width: u32,
524 src_height: u32,
525 dst_width: u32,
526 dst_height: u32,
527 method: ResamplingMethod,
528) -> GpuResult<GpuBuffer<T>> {
529 let kernel = ResamplingKernel::new(context, method)?;
530 let params = ResamplingParams::new(src_width, src_height, dst_width, dst_height);
531 kernel.execute(input, params)
532}
533
534pub fn downscale_2x<T: Pod>(
540 context: &GpuContext,
541 input: &GpuBuffer<T>,
542 width: u32,
543 height: u32,
544) -> GpuResult<GpuBuffer<T>> {
545 resize(
546 context,
547 input,
548 width,
549 height,
550 width / 2,
551 height / 2,
552 ResamplingMethod::Bilinear,
553 )
554}
555
556pub fn upscale_2x<T: Pod>(
562 context: &GpuContext,
563 input: &GpuBuffer<T>,
564 width: u32,
565 height: u32,
566 method: ResamplingMethod,
567) -> GpuResult<GpuBuffer<T>> {
568 resize(context, input, width, height, width * 2, height * 2, method)
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574
575 #[test]
576 fn test_resampling_params() {
577 let params = ResamplingParams::new(1024, 768, 512, 384);
578 let (scale_x, scale_y) = params.scale_factors();
579 assert!((scale_x - 2.0).abs() < 1e-5);
580 assert!((scale_y - 2.0).abs() < 1e-5);
581 }
582
583 #[test]
584 fn test_resampling_shader() {
585 let shader = ResamplingKernel::resampling_shader(ResamplingMethod::Bilinear, (16, 16));
586 assert!(shader.contains("@compute"));
587 assert!(shader.contains("bilinear"));
588 assert!(shader.contains("@workgroup_size(16, 16)"));
589 }
590
591 #[test]
592 fn test_resampling_shader_custom_workgroup() {
593 let shader = ResamplingKernel::resampling_shader(ResamplingMethod::NearestNeighbor, (8, 8));
594 assert!(shader.contains("@workgroup_size(8, 8)"));
595 assert!(shader.contains("nearest_neighbor"));
596 }
597
598 #[test]
599 fn test_resampling_shader_4x4_fallback() {
600 let shader = ResamplingKernel::resampling_shader(ResamplingMethod::Bilinear, (4, 4));
601 assert!(shader.contains("@workgroup_size(4, 4)"));
602 }
603
604 #[test]
605 fn test_lanczos_shader_a2() {
606 let shader =
607 ResamplingKernel::resampling_shader(ResamplingMethod::Lanczos { a: 2 }, (16, 16));
608 assert!(shader.contains("@compute"));
609 assert!(shader.contains("fn lanczos("));
610 assert!(shader.contains("fn sinc("));
611 assert!(shader.contains("fn lanczos_weight("));
612 assert!(shader.contains("4"));
614 assert!(shader.contains("2f"));
616 assert!(shader.contains("@workgroup_size(16, 16)"));
617 }
618
619 #[test]
620 fn test_lanczos_shader_a3() {
621 let shader =
622 ResamplingKernel::resampling_shader(ResamplingMethod::Lanczos { a: 3 }, (16, 16));
623 assert!(shader.contains("fn lanczos("));
624 assert!(shader.contains("6"));
626 assert!(shader.contains("3f"));
627 }
628
629 #[test]
630 fn test_lanczos_shader_a2_custom_workgroup() {
631 let shader =
632 ResamplingKernel::resampling_shader(ResamplingMethod::Lanczos { a: 2 }, (8, 8));
633 assert!(shader.contains("@workgroup_size(8, 8)"));
634 assert!(shader.contains("fn lanczos("));
635 }
636
637 #[test]
638 fn test_lanczos_entry_point() {
639 assert_eq!(ResamplingMethod::Lanczos { a: 2 }.entry_point(), "lanczos");
640 assert_eq!(ResamplingMethod::Lanczos { a: 3 }.entry_point(), "lanczos");
641 }
642
643 #[test]
644 fn test_lanczos_equality() {
645 assert_eq!(
646 ResamplingMethod::Lanczos { a: 2 },
647 ResamplingMethod::Lanczos { a: 2 }
648 );
649 assert_ne!(
650 ResamplingMethod::Lanczos { a: 2 },
651 ResamplingMethod::Lanczos { a: 3 }
652 );
653 assert_ne!(
654 ResamplingMethod::Lanczos { a: 2 },
655 ResamplingMethod::Bilinear
656 );
657 }
658
659 #[tokio::test]
660 async fn test_resampling_kernel() {
661 if let Ok(context) = GpuContext::new().await {
662 if let Ok(_kernel) = ResamplingKernel::new(&context, ResamplingMethod::NearestNeighbor)
663 {
664 }
666 }
667 }
668
669 #[tokio::test]
670 async fn test_resize_operation() {
671 if let Ok(context) = GpuContext::new().await {
672 let input_data: Vec<f32> = (0..16).map(|i| i as f32).collect();
674
675 if let Ok(input) = GpuBuffer::from_data(
676 &context,
677 &input_data,
678 BufferUsages::STORAGE | BufferUsages::COPY_SRC,
679 ) {
680 if let Ok(_output) = resize(
681 &context,
682 &input,
683 4,
684 4,
685 2,
686 2,
687 ResamplingMethod::NearestNeighbor,
688 ) {
689 }
691 }
692 }
693 }
694}