use super::buffer::GpuBuffer;
use super::context::GpuContext;
use super::shaders;
use crate::RangaError;
use crate::blend::BlendMode;
use crate::pixel::{PixelBuffer, PixelFormat};
use crate::transform::ScaleFilter;
pub fn gpu_blend(
ctx: &mut GpuContext,
src: &PixelBuffer,
dst: &mut PixelBuffer,
mode: BlendMode,
opacity: f32,
) -> Result<(), RangaError> {
if src.format != PixelFormat::Rgba8 || dst.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"gpu_blend: expected Rgba8, got {:?}",
if src.format != PixelFormat::Rgba8 {
src.format
} else {
dst.format
}
)));
}
if src.width != dst.width || src.height != dst.height {
return Err(RangaError::DimensionMismatch {
expected: src.pixel_count(),
actual: dst.pixel_count(),
});
}
let pixel_count = u32::try_from(src.pixel_count())
.map_err(|_| RangaError::Other("gpu_blend: pixel count exceeds u32::MAX".into()))?;
let mode_id: u32 = blend_mode_id(mode);
let src_gpu = GpuBuffer::upload(ctx, src);
let dst_gpu = GpuBuffer::upload(ctx, dst);
let params = [pixel_count, mode_id, opacity.to_bits(), 0u32];
let params_bytes = params_to_bytes(¶ms);
dispatch_3buf_shader(
ctx,
"blend_all",
&shaders::build_shader(shaders::BLEND_ALL),
src_gpu.wgpu_buffer(),
dst_gpu.wgpu_buffer(),
params_bytes,
pixel_count.div_ceil(256),
)?;
let result = dst_gpu.download(ctx)?;
dst.data = result.data;
Ok(())
}
pub fn gpu_invert(ctx: &mut GpuContext, buf: &mut PixelBuffer) -> Result<(), RangaError> {
if buf.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"gpu_invert: expected Rgba8, got {:?}",
buf.format
)));
}
let pixel_count = u32::try_from(buf.pixel_count())
.map_err(|_| RangaError::Other("gpu_invert: pixel count exceeds u32::MAX".into()))?;
let gpu_buf = GpuBuffer::upload(ctx, buf);
let params = [pixel_count];
let params_bytes = params_to_bytes(¶ms);
dispatch_1buf_shader(
ctx,
"invert",
&shaders::build_shader(shaders::INVERT),
gpu_buf.wgpu_buffer(),
params_bytes,
pixel_count.div_ceil(256),
)?;
let result = gpu_buf.download(ctx)?;
buf.data = result.data;
Ok(())
}
pub fn gpu_grayscale(ctx: &mut GpuContext, buf: &mut PixelBuffer) -> Result<(), RangaError> {
if buf.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"gpu_grayscale: expected Rgba8, got {:?}",
buf.format
)));
}
let pixel_count = u32::try_from(buf.pixel_count())
.map_err(|_| RangaError::Other("gpu_grayscale: pixel count exceeds u32::MAX".into()))?;
let gpu_buf = GpuBuffer::upload(ctx, buf);
let params = [pixel_count];
let params_bytes = params_to_bytes(¶ms);
dispatch_1buf_shader(
ctx,
"grayscale",
&shaders::build_shader(shaders::GRAYSCALE),
gpu_buf.wgpu_buffer(),
params_bytes,
pixel_count.div_ceil(256),
)?;
let result = gpu_buf.download(ctx)?;
buf.data = result.data;
Ok(())
}
pub fn gpu_brightness_contrast(
ctx: &mut GpuContext,
buf: &mut PixelBuffer,
brightness: f32,
contrast: f32,
) -> Result<(), RangaError> {
if buf.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"gpu_brightness_contrast: expected Rgba8, got {:?}",
buf.format
)));
}
let pixel_count = u32::try_from(buf.pixel_count()).map_err(|_| {
RangaError::Other("gpu_brightness_contrast: pixel count exceeds u32::MAX".into())
})?;
let gpu_buf = GpuBuffer::upload(ctx, buf);
let params = [pixel_count, brightness.to_bits(), contrast.to_bits(), 0u32];
let params_bytes = params_to_bytes(¶ms);
dispatch_1buf_shader(
ctx,
"brightness_contrast",
&shaders::build_shader(shaders::BRIGHTNESS_CONTRAST),
gpu_buf.wgpu_buffer(),
params_bytes,
pixel_count.div_ceil(256),
)?;
let result = gpu_buf.download(ctx)?;
buf.data = result.data;
Ok(())
}
pub fn gpu_saturation(
ctx: &mut GpuContext,
buf: &mut PixelBuffer,
factor: f32,
) -> Result<(), RangaError> {
if buf.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"gpu_saturation: expected Rgba8, got {:?}",
buf.format
)));
}
let pixel_count = u32::try_from(buf.pixel_count())
.map_err(|_| RangaError::Other("gpu_saturation: pixel count exceeds u32::MAX".into()))?;
let gpu_buf = GpuBuffer::upload(ctx, buf);
let params = [pixel_count, factor.to_bits(), 0u32, 0u32];
let params_bytes = params_to_bytes(¶ms);
dispatch_1buf_shader(
ctx,
"saturation",
&shaders::build_shader(shaders::SATURATION),
gpu_buf.wgpu_buffer(),
params_bytes,
pixel_count.div_ceil(256),
)?;
let result = gpu_buf.download(ctx)?;
buf.data = result.data;
Ok(())
}
pub fn gpu_noise_gaussian(
ctx: &mut GpuContext,
buf: &mut PixelBuffer,
strength: f32,
seed: u32,
) -> Result<(), RangaError> {
if buf.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"gpu_noise_gaussian: expected Rgba8, got {:?}",
buf.format
)));
}
let pixel_count = u32::try_from(buf.pixel_count()).map_err(|_| {
RangaError::Other("gpu_noise_gaussian: pixel count exceeds u32::MAX".into())
})?;
let gpu_buf = GpuBuffer::upload(ctx, buf);
let params = [pixel_count, seed, strength.to_bits(), 0u32];
let params_bytes = params_to_bytes(¶ms);
dispatch_1buf_shader(
ctx,
"noise_gaussian",
&shaders::build_shader(shaders::NOISE_GAUSSIAN),
gpu_buf.wgpu_buffer(),
params_bytes,
pixel_count.div_ceil(256),
)?;
let result = gpu_buf.download(ctx)?;
buf.data = result.data;
Ok(())
}
pub fn gpu_dissolve(
ctx: &mut GpuContext,
src: &PixelBuffer,
dst: &mut PixelBuffer,
factor: f32,
) -> Result<(), RangaError> {
if src.format != PixelFormat::Rgba8 || dst.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"gpu_dissolve: expected Rgba8, got {:?}",
if src.format != PixelFormat::Rgba8 {
src.format
} else {
dst.format
}
)));
}
if src.width != dst.width || src.height != dst.height {
return Err(RangaError::DimensionMismatch {
expected: src.pixel_count(),
actual: dst.pixel_count(),
});
}
let pixel_count = u32::try_from(src.pixel_count())
.map_err(|_| RangaError::Other("gpu_dissolve: pixel count exceeds u32::MAX".into()))?;
let src_gpu = GpuBuffer::upload(ctx, src);
let dst_gpu = GpuBuffer::upload(ctx, dst);
let params = [pixel_count, factor.to_bits(), 0u32, 0u32];
let params_bytes = params_to_bytes(¶ms);
dispatch_3buf_shader(
ctx,
"dissolve",
&shaders::build_shader(shaders::DISSOLVE),
src_gpu.wgpu_buffer(),
dst_gpu.wgpu_buffer(),
params_bytes,
pixel_count.div_ceil(256),
)?;
let result = dst_gpu.download(ctx)?;
dst.data = result.data;
Ok(())
}
pub fn gpu_fade(
ctx: &mut GpuContext,
buf: &mut PixelBuffer,
factor: f32,
) -> Result<(), RangaError> {
if buf.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"gpu_fade: expected Rgba8, got {:?}",
buf.format
)));
}
let pixel_count = u32::try_from(buf.pixel_count())
.map_err(|_| RangaError::Other("gpu_fade: pixel count exceeds u32::MAX".into()))?;
let gpu_buf = GpuBuffer::upload(ctx, buf);
let params = [pixel_count, factor.to_bits(), 0u32, 0u32];
let params_bytes = params_to_bytes(¶ms);
dispatch_1buf_shader(
ctx,
"fade",
&shaders::build_shader(shaders::FADE),
gpu_buf.wgpu_buffer(),
params_bytes,
pixel_count.div_ceil(256),
)?;
let result = gpu_buf.download(ctx)?;
buf.data = result.data;
Ok(())
}
pub fn gpu_wipe(
ctx: &mut GpuContext,
src: &PixelBuffer,
dst: &mut PixelBuffer,
progress: f32,
) -> Result<(), RangaError> {
if src.format != PixelFormat::Rgba8 || dst.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"gpu_wipe: expected Rgba8, got {:?}",
if src.format != PixelFormat::Rgba8 {
src.format
} else {
dst.format
}
)));
}
if src.width != dst.width || src.height != dst.height {
return Err(RangaError::DimensionMismatch {
expected: src.pixel_count(),
actual: dst.pixel_count(),
});
}
let pixel_count = u32::try_from(src.pixel_count())
.map_err(|_| RangaError::Other("gpu_wipe: pixel count exceeds u32::MAX".into()))?;
let src_gpu = GpuBuffer::upload(ctx, src);
let dst_gpu = GpuBuffer::upload(ctx, dst);
let params = [pixel_count, src.width, src.height, progress.to_bits()];
let params_bytes = params_to_bytes(¶ms);
dispatch_3buf_shader(
ctx,
"wipe",
&shaders::build_shader(shaders::WIPE),
src_gpu.wgpu_buffer(),
dst_gpu.wgpu_buffer(),
params_bytes,
pixel_count.div_ceil(256),
)?;
let result = dst_gpu.download(ctx)?;
dst.data = result.data;
Ok(())
}
pub struct GpuChain<'a> {
ctx: &'a mut GpuContext,
buf_a: GpuBuffer,
buf_b: GpuBuffer,
current_is_a: bool,
width: u32,
height: u32,
pixel_count: u32,
}
impl<'a> GpuChain<'a> {
#[must_use = "returns a new GPU processing chain"]
pub fn new(ctx: &'a mut GpuContext, buf: &PixelBuffer) -> Result<Self, RangaError> {
if buf.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"GpuChain::new: expected Rgba8, got {:?}",
buf.format
)));
}
let pixel_count = u32::try_from(buf.pixel_count())
.map_err(|_| RangaError::Other("GpuChain::new: pixel count exceeds u32::MAX".into()))?;
let buf_a = GpuBuffer::upload(ctx, buf);
let zeroed = PixelBuffer::zeroed(buf.width, buf.height, PixelFormat::Rgba8);
let buf_b = GpuBuffer::upload(ctx, &zeroed);
Ok(Self {
ctx,
buf_a,
buf_b,
current_is_a: true,
width: buf.width,
height: buf.height,
pixel_count,
})
}
pub fn invert(self) -> Result<Self, RangaError> {
let params = [self.pixel_count];
let params_bytes = params_to_bytes(¶ms);
let current = if self.current_is_a {
&self.buf_a
} else {
&self.buf_b
};
dispatch_1buf_shader(
self.ctx,
"invert",
&shaders::build_shader(shaders::INVERT),
current.wgpu_buffer(),
params_bytes,
self.pixel_count.div_ceil(256),
)?;
Ok(self)
}
pub fn grayscale(self) -> Result<Self, RangaError> {
let params = [self.pixel_count];
let params_bytes = params_to_bytes(¶ms);
dispatch_1buf_shader(
self.ctx,
"grayscale",
&shaders::build_shader(shaders::GRAYSCALE),
(if self.current_is_a {
&self.buf_a
} else {
&self.buf_b
})
.wgpu_buffer(),
params_bytes,
self.pixel_count.div_ceil(256),
)?;
Ok(self)
}
pub fn brightness_contrast(self, brightness: f32, contrast: f32) -> Result<Self, RangaError> {
let params = [
self.pixel_count,
brightness.to_bits(),
contrast.to_bits(),
0u32,
];
let params_bytes = params_to_bytes(¶ms);
dispatch_1buf_shader(
self.ctx,
"brightness_contrast",
&shaders::build_shader(shaders::BRIGHTNESS_CONTRAST),
(if self.current_is_a {
&self.buf_a
} else {
&self.buf_b
})
.wgpu_buffer(),
params_bytes,
self.pixel_count.div_ceil(256),
)?;
Ok(self)
}
pub fn saturation(self, factor: f32) -> Result<Self, RangaError> {
let params = [self.pixel_count, factor.to_bits(), 0u32, 0u32];
let params_bytes = params_to_bytes(¶ms);
dispatch_1buf_shader(
self.ctx,
"saturation",
&shaders::build_shader(shaders::SATURATION),
(if self.current_is_a {
&self.buf_a
} else {
&self.buf_b
})
.wgpu_buffer(),
params_bytes,
self.pixel_count.div_ceil(256),
)?;
Ok(self)
}
pub fn noise_gaussian(self, strength: f32, seed: u32) -> Result<Self, RangaError> {
let params = [self.pixel_count, seed, strength.to_bits(), 0u32];
let params_bytes = params_to_bytes(¶ms);
dispatch_1buf_shader(
self.ctx,
"noise_gaussian",
&shaders::build_shader(shaders::NOISE_GAUSSIAN),
(if self.current_is_a {
&self.buf_a
} else {
&self.buf_b
})
.wgpu_buffer(),
params_bytes,
self.pixel_count.div_ceil(256),
)?;
Ok(self)
}
pub fn dissolve(self, other: &PixelBuffer, factor: f32) -> Result<Self, RangaError> {
if other.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"GpuChain::dissolve: expected Rgba8, got {:?}",
other.format
)));
}
if other.width != self.width || other.height != self.height {
return Err(RangaError::DimensionMismatch {
expected: (self.width as usize) * (self.height as usize),
actual: other.pixel_count(),
});
}
let src_gpu = GpuBuffer::upload(self.ctx, other);
let params = [self.pixel_count, factor.to_bits(), 0u32, 0u32];
let params_bytes = params_to_bytes(¶ms);
dispatch_3buf_shader(
self.ctx,
"dissolve",
&shaders::build_shader(shaders::DISSOLVE),
src_gpu.wgpu_buffer(),
(if self.current_is_a {
&self.buf_a
} else {
&self.buf_b
})
.wgpu_buffer(),
params_bytes,
self.pixel_count.div_ceil(256),
)?;
Ok(self)
}
pub fn fade(self, factor: f32) -> Result<Self, RangaError> {
let params = [self.pixel_count, factor.to_bits(), 0u32, 0u32];
let params_bytes = params_to_bytes(¶ms);
dispatch_1buf_shader(
self.ctx,
"fade",
&shaders::build_shader(shaders::FADE),
(if self.current_is_a {
&self.buf_a
} else {
&self.buf_b
})
.wgpu_buffer(),
params_bytes,
self.pixel_count.div_ceil(256),
)?;
Ok(self)
}
pub fn wipe(self, other: &PixelBuffer, progress: f32) -> Result<Self, RangaError> {
if other.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"GpuChain::wipe: expected Rgba8, got {:?}",
other.format
)));
}
if other.width != self.width || other.height != self.height {
return Err(RangaError::DimensionMismatch {
expected: (self.width as usize) * (self.height as usize),
actual: other.pixel_count(),
});
}
let src_gpu = GpuBuffer::upload(self.ctx, other);
let params = [
self.pixel_count,
self.width,
self.height,
progress.to_bits(),
];
let params_bytes = params_to_bytes(¶ms);
dispatch_3buf_shader(
self.ctx,
"wipe",
&shaders::build_shader(shaders::WIPE),
src_gpu.wgpu_buffer(),
(if self.current_is_a {
&self.buf_a
} else {
&self.buf_b
})
.wgpu_buffer(),
params_bytes,
self.pixel_count.div_ceil(256),
)?;
Ok(self)
}
pub fn gaussian_blur(self, radius: u32) -> Result<Self, RangaError> {
if radius == 0 {
return Ok(self);
}
let kernel = build_gaussian_kernel(radius);
let kernel_bytes: &[u8] = unsafe {
std::slice::from_raw_parts(
kernel.as_ptr().cast::<u8>(),
kernel.len() * std::mem::size_of::<f32>(),
)
};
let kernel_gpu = mabda::buffer::create_storage_buffer(
self.ctx.device(),
kernel_bytes,
"blur_kernel",
true,
);
let params = [self.width, self.height, radius, 0u32];
let params_bytes: &[u8] = unsafe {
std::slice::from_raw_parts(params.as_ptr().cast::<u8>(), std::mem::size_of_val(¶ms))
};
let workgroups_x = self.width.div_ceil(16);
let workgroups_y = self.height.div_ceil(16);
dispatch_blur_shader(
self.ctx,
"blur_horizontal",
&shaders::build_shader(shaders::BLUR_HORIZONTAL),
(if self.current_is_a {
&self.buf_a
} else {
&self.buf_b
})
.wgpu_buffer(),
(if self.current_is_a {
&self.buf_b
} else {
&self.buf_a
})
.wgpu_buffer(),
&kernel_gpu,
params_bytes,
workgroups_x,
workgroups_y,
)?;
dispatch_blur_shader(
self.ctx,
"blur_vertical",
&shaders::build_shader(shaders::BLUR_VERTICAL),
(if self.current_is_a {
&self.buf_b
} else {
&self.buf_a
})
.wgpu_buffer(),
(if self.current_is_a {
&self.buf_a
} else {
&self.buf_b
})
.wgpu_buffer(),
&kernel_gpu,
params_bytes,
workgroups_x,
workgroups_y,
)?;
Ok(self)
}
pub fn blend(
self,
other: &PixelBuffer,
mode: BlendMode,
opacity: f32,
) -> Result<Self, RangaError> {
if other.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"GpuChain::blend: expected Rgba8, got {:?}",
other.format
)));
}
if other.width != self.width || other.height != self.height {
return Err(RangaError::DimensionMismatch {
expected: (self.width as usize) * (self.height as usize),
actual: other.pixel_count(),
});
}
let mode_id: u32 = blend_mode_id(mode);
let src_gpu = GpuBuffer::upload(self.ctx, other);
let params = [self.pixel_count, mode_id, opacity.to_bits(), 0u32];
let params_bytes = params_to_bytes(¶ms);
dispatch_3buf_shader(
self.ctx,
"blend_all",
&shaders::build_shader(shaders::BLEND_ALL),
src_gpu.wgpu_buffer(),
(if self.current_is_a {
&self.buf_a
} else {
&self.buf_b
})
.wgpu_buffer(),
params_bytes,
self.pixel_count.div_ceil(256),
)?;
Ok(self)
}
#[must_use = "returns the final processed buffer"]
pub fn finish(self) -> Result<PixelBuffer, RangaError> {
let current = if self.current_is_a {
&self.buf_a
} else {
&self.buf_b
};
current.download(self.ctx).map_err(Into::into)
}
}
#[inline]
fn params_to_bytes(params: &[u32]) -> &[u8] {
unsafe {
std::slice::from_raw_parts(params.as_ptr().cast::<u8>(), std::mem::size_of_val(params))
}
}
#[inline]
fn blend_mode_id(mode: BlendMode) -> u32 {
match mode {
BlendMode::Normal => 0,
BlendMode::Multiply => 1,
BlendMode::Screen => 2,
BlendMode::Overlay => 3,
BlendMode::Darken => 4,
BlendMode::Lighten => 5,
BlendMode::ColorDodge => 6,
BlendMode::ColorBurn => 7,
BlendMode::SoftLight => 8,
BlendMode::HardLight => 9,
BlendMode::Difference => 10,
BlendMode::Exclusion => 11,
}
}
fn ensure_pipeline_1buf(
ctx: &mut GpuContext,
name: &'static str,
shader_src: &str,
) -> Result<*const mabda::compute::ComputePipeline, RangaError> {
let pipeline = ctx.get_or_create_pipeline_1buf(name, shader_src)?;
Ok(pipeline as *const _)
}
fn ensure_pipeline_3buf(
ctx: &mut GpuContext,
name: &'static str,
shader_src: &str,
) -> Result<*const mabda::compute::ComputePipeline, RangaError> {
let pipeline = ctx.get_or_create_pipeline_3buf(name, shader_src)?;
Ok(pipeline as *const _)
}
fn ensure_pipeline_4buf(
ctx: &mut GpuContext,
name: &'static str,
shader_src: &str,
) -> Result<*const mabda::compute::ComputePipeline, RangaError> {
let pipeline = ctx.get_or_create_pipeline_4buf(name, shader_src)?;
Ok(pipeline as *const _)
}
fn dispatch_1buf_shader(
ctx: &mut GpuContext,
name: &'static str,
shader_src: &str,
storage_buf: &wgpu::Buffer,
params_data: &[u8],
workgroups: u32,
) -> Result<(), RangaError> {
let pipeline_ptr = ensure_pipeline_1buf(ctx, name, shader_src)?;
let pipeline = unsafe { &*pipeline_ptr };
let aligned_size = params_data.len().div_ceil(16) * 16;
let mut aligned_data = vec![0u8; aligned_size];
aligned_data[..params_data.len()].copy_from_slice(params_data);
let device = ctx.device();
let queue = ctx.queue();
let params_buf = mabda::buffer::create_uniform_buffer(device, &aligned_data, "params");
let bgl = pipeline
.bind_group_layout(0)
.ok_or_else(|| RangaError::Other("bind group layout not found".into()))?;
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: storage_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: params_buf.as_entire_binding(),
},
],
});
pipeline.dispatch(device, queue, &bg, workgroups, 1, 1);
Ok(())
}
fn dispatch_3buf_shader(
ctx: &mut GpuContext,
name: &'static str,
shader_src: &str,
src_buf: &wgpu::Buffer,
dst_buf: &wgpu::Buffer,
params_data: &[u8],
workgroups: u32,
) -> Result<(), RangaError> {
let pipeline_ptr = ensure_pipeline_3buf(ctx, name, shader_src)?;
let pipeline = unsafe { &*pipeline_ptr };
let aligned_size = params_data.len().div_ceil(16) * 16;
let mut aligned_data = vec![0u8; aligned_size];
aligned_data[..params_data.len()].copy_from_slice(params_data);
let device = ctx.device();
let queue = ctx.queue();
let params_buf = mabda::buffer::create_uniform_buffer(device, &aligned_data, "params");
let bgl = pipeline
.bind_group_layout(0)
.ok_or_else(|| RangaError::Other("bind group layout not found".into()))?;
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: src_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: dst_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: params_buf.as_entire_binding(),
},
],
});
pipeline.dispatch(device, queue, &bg, workgroups, 1, 1);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn dispatch_2d_3buf_shader(
ctx: &mut GpuContext,
name: &'static str,
shader_src: &str,
input_buf: &wgpu::Buffer,
output_buf: &wgpu::Buffer,
params_data: &[u8],
workgroups_x: u32,
workgroups_y: u32,
) -> Result<(), RangaError> {
let pipeline_ptr = ensure_pipeline_3buf(ctx, name, shader_src)?;
let pipeline = unsafe { &*pipeline_ptr };
let aligned_size = params_data.len().div_ceil(16) * 16;
let mut aligned_data = vec![0u8; aligned_size];
aligned_data[..params_data.len()].copy_from_slice(params_data);
let device = ctx.device();
let queue = ctx.queue();
let params_buf =
mabda::buffer::create_uniform_buffer(device, &aligned_data, "transform_params");
let bgl = pipeline
.bind_group_layout(0)
.ok_or_else(|| RangaError::Other("bind group layout not found".into()))?;
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: input_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: output_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: params_buf.as_entire_binding(),
},
],
});
pipeline.dispatch(device, queue, &bg, workgroups_x, workgroups_y, 1);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn dispatch_blur_shader(
ctx: &mut GpuContext,
name: &'static str,
shader_src: &str,
input_buf: &wgpu::Buffer,
output_buf: &wgpu::Buffer,
kernel_buf: &wgpu::Buffer,
params_data: &[u8],
workgroups_x: u32,
workgroups_y: u32,
) -> Result<(), RangaError> {
let pipeline_ptr = ensure_pipeline_4buf(ctx, name, shader_src)?;
let pipeline = unsafe { &*pipeline_ptr };
let aligned_size = params_data.len().div_ceil(16) * 16;
let mut aligned_data = vec![0u8; aligned_size];
aligned_data[..params_data.len()].copy_from_slice(params_data);
let device = ctx.device();
let queue = ctx.queue();
let params_buf = mabda::buffer::create_uniform_buffer(device, &aligned_data, "blur_params");
let bgl = pipeline
.bind_group_layout(0)
.ok_or_else(|| RangaError::Other("bind group layout not found".into()))?;
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: input_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: output_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: kernel_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: params_buf.as_entire_binding(),
},
],
});
pipeline.dispatch(device, queue, &bg, workgroups_x, workgroups_y, 1);
Ok(())
}
#[must_use = "returns a new blurred buffer"]
pub fn gpu_gaussian_blur(
ctx: &mut GpuContext,
buf: &PixelBuffer,
radius: u32,
) -> Result<PixelBuffer, RangaError> {
if buf.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"gpu_gaussian_blur: expected Rgba8, got {:?}",
buf.format
)));
}
if radius == 0 {
return Ok(buf.clone());
}
let w = buf.width;
let h = buf.height;
let kernel = build_gaussian_kernel(radius);
let input_gpu = GpuBuffer::upload(ctx, buf);
let temp_buf = PixelBuffer::zeroed(w, h, PixelFormat::Rgba8);
let temp_gpu = GpuBuffer::upload(ctx, &temp_buf);
let output_buf = PixelBuffer::zeroed(w, h, PixelFormat::Rgba8);
let output_gpu = GpuBuffer::upload(ctx, &output_buf);
let kernel_bytes: &[u8] = unsafe {
std::slice::from_raw_parts(
kernel.as_ptr().cast::<u8>(),
kernel.len() * std::mem::size_of::<f32>(),
)
};
let kernel_gpu =
mabda::buffer::create_storage_buffer(ctx.device(), kernel_bytes, "blur_kernel", true);
let params = [w, h, radius, 0u32];
let params_bytes: &[u8] = unsafe {
std::slice::from_raw_parts(params.as_ptr().cast::<u8>(), std::mem::size_of_val(¶ms))
};
let workgroups_x = w.div_ceil(16);
let workgroups_y = h.div_ceil(16);
dispatch_blur_shader(
ctx,
"blur_horizontal",
&shaders::build_shader(shaders::BLUR_HORIZONTAL),
input_gpu.wgpu_buffer(),
temp_gpu.wgpu_buffer(),
&kernel_gpu,
params_bytes,
workgroups_x,
workgroups_y,
)?;
dispatch_blur_shader(
ctx,
"blur_vertical",
&shaders::build_shader(shaders::BLUR_VERTICAL),
temp_gpu.wgpu_buffer(),
output_gpu.wgpu_buffer(),
&kernel_gpu,
params_bytes,
workgroups_x,
workgroups_y,
)?;
output_gpu.download(ctx).map_err(Into::into)
}
fn build_gaussian_kernel(radius: u32) -> Vec<f32> {
let r = radius as i32;
let sigma = (radius as f32 / 3.0).max(0.5);
let len = (2 * r + 1) as usize;
let mut kernel = vec![0.0f32; len];
let mut sum = 0.0;
for i in 0..len as i32 {
let x = (i - r) as f32;
let v = (-x * x / (2.0 * sigma * sigma)).exp();
kernel[i as usize] = v;
sum += v;
}
for v in &mut kernel {
*v /= sum;
}
kernel
}
#[must_use = "returns a new cropped buffer"]
pub fn gpu_crop(
ctx: &mut GpuContext,
buf: &PixelBuffer,
left: u32,
top: u32,
right: u32,
bottom: u32,
) -> Result<PixelBuffer, RangaError> {
if buf.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"gpu_crop: expected Rgba8, got {:?}",
buf.format
)));
}
let l = left.min(buf.width);
let t = top.min(buf.height);
let r = right.min(buf.width).max(l);
let b = bottom.min(buf.height).max(t);
let dst_w = r - l;
let dst_h = b - t;
if dst_w == 0 || dst_h == 0 {
return Ok(PixelBuffer::zeroed(0, 0, PixelFormat::Rgba8));
}
let input_gpu = GpuBuffer::upload(ctx, buf);
let output_pb = PixelBuffer::zeroed(dst_w, dst_h, PixelFormat::Rgba8);
let output_gpu = GpuBuffer::upload(ctx, &output_pb);
let params = [buf.width, dst_w, dst_h, l, t, 0u32, 0u32, 0u32];
let params_bytes = params_to_bytes(¶ms);
let workgroups_x = dst_w.div_ceil(16);
let workgroups_y = dst_h.div_ceil(16);
dispatch_2d_3buf_shader(
ctx,
"crop",
&shaders::build_shader(shaders::CROP),
input_gpu.wgpu_buffer(),
output_gpu.wgpu_buffer(),
params_bytes,
workgroups_x,
workgroups_y,
)?;
output_gpu.download(ctx).map_err(Into::into)
}
#[must_use = "returns a new resized buffer"]
pub fn gpu_resize(
ctx: &mut GpuContext,
buf: &PixelBuffer,
new_w: u32,
new_h: u32,
filter: ScaleFilter,
) -> Result<PixelBuffer, RangaError> {
if buf.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"gpu_resize: expected Rgba8, got {:?}",
buf.format
)));
}
if new_w == 0 || new_h == 0 {
return Ok(PixelBuffer::zeroed(0, 0, PixelFormat::Rgba8));
}
let input_gpu = GpuBuffer::upload(ctx, buf);
let output_pb = PixelBuffer::zeroed(new_w, new_h, PixelFormat::Rgba8);
let output_gpu = GpuBuffer::upload(ctx, &output_pb);
let params = [buf.width, buf.height, new_w, new_h];
let params_bytes = params_to_bytes(¶ms);
let (shader_name, shader_src) = match filter {
ScaleFilter::Nearest => (
"resize_nearest",
shaders::build_shader(shaders::RESIZE_NEAREST),
),
ScaleFilter::Bilinear | ScaleFilter::Bicubic => (
"resize_bilinear",
shaders::build_shader(shaders::RESIZE_BILINEAR),
),
};
let workgroups_x = new_w.div_ceil(16);
let workgroups_y = new_h.div_ceil(16);
dispatch_2d_3buf_shader(
ctx,
shader_name,
&shader_src,
input_gpu.wgpu_buffer(),
output_gpu.wgpu_buffer(),
params_bytes,
workgroups_x,
workgroups_y,
)?;
output_gpu.download(ctx).map_err(Into::into)
}
#[must_use = "returns a new flipped buffer"]
pub fn gpu_flip_horizontal(
ctx: &mut GpuContext,
buf: &PixelBuffer,
) -> Result<PixelBuffer, RangaError> {
if buf.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"gpu_flip_horizontal: expected Rgba8, got {:?}",
buf.format
)));
}
let input_gpu = GpuBuffer::upload(ctx, buf);
let output_pb = PixelBuffer::zeroed(buf.width, buf.height, PixelFormat::Rgba8);
let output_gpu = GpuBuffer::upload(ctx, &output_pb);
let params = [buf.width, buf.height, 0u32, 0u32];
let params_bytes = params_to_bytes(¶ms);
let workgroups_x = buf.width.div_ceil(16);
let workgroups_y = buf.height.div_ceil(16);
dispatch_2d_3buf_shader(
ctx,
"flip_horizontal",
&shaders::build_shader(shaders::FLIP_HORIZONTAL),
input_gpu.wgpu_buffer(),
output_gpu.wgpu_buffer(),
params_bytes,
workgroups_x,
workgroups_y,
)?;
output_gpu.download(ctx).map_err(Into::into)
}
#[must_use = "returns a new flipped buffer"]
pub fn gpu_flip_vertical(
ctx: &mut GpuContext,
buf: &PixelBuffer,
) -> Result<PixelBuffer, RangaError> {
if buf.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"gpu_flip_vertical: expected Rgba8, got {:?}",
buf.format
)));
}
let input_gpu = GpuBuffer::upload(ctx, buf);
let output_pb = PixelBuffer::zeroed(buf.width, buf.height, PixelFormat::Rgba8);
let output_gpu = GpuBuffer::upload(ctx, &output_pb);
let params = [buf.width, buf.height, 0u32, 0u32];
let params_bytes = params_to_bytes(¶ms);
let workgroups_x = buf.width.div_ceil(16);
let workgroups_y = buf.height.div_ceil(16);
dispatch_2d_3buf_shader(
ctx,
"flip_vertical",
&shaders::build_shader(shaders::FLIP_VERTICAL),
input_gpu.wgpu_buffer(),
output_gpu.wgpu_buffer(),
params_bytes,
workgroups_x,
workgroups_y,
)?;
output_gpu.download(ctx).map_err(Into::into)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gpu::GpuContext;
fn try_gpu() -> Option<GpuContext> {
GpuContext::new().ok()
}
#[test]
fn gpu_invert_matches_cpu() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return, };
let data: Vec<u8> = (0..64u8).flat_map(|i| [i * 4, i * 3, i * 2, 255]).collect();
let mut gpu_buf = PixelBuffer::new(data.clone(), 8, 8, PixelFormat::Rgba8).unwrap();
let mut cpu_buf = PixelBuffer::new(data, 8, 8, PixelFormat::Rgba8).unwrap();
gpu_invert(&mut ctx, &mut gpu_buf).unwrap();
crate::filter::invert(&mut cpu_buf).unwrap();
for (g, c) in gpu_buf.data.iter().zip(cpu_buf.data.iter()) {
assert!(
(*g as i16 - *c as i16).unsigned_abs() <= 1,
"GPU={g} CPU={c}"
);
}
}
#[test]
fn gpu_grayscale_produces_uniform_channels() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let data: Vec<u8> = (0..16u8)
.flat_map(|i| {
[
200u8.wrapping_add(i * 3),
100u8.wrapping_add(i * 7),
50u8.wrapping_add(i * 11),
255,
]
})
.collect();
let mut buf = PixelBuffer::new(data, 4, 4, PixelFormat::Rgba8).unwrap();
gpu_grayscale(&mut ctx, &mut buf).unwrap();
for pixel in buf.data.chunks_exact(4) {
assert_eq!(pixel[0], pixel[1], "R != G: {} != {}", pixel[0], pixel[1]);
assert_eq!(pixel[1], pixel[2], "G != B: {} != {}", pixel[1], pixel[2]);
assert_eq!(pixel[3], 255, "alpha changed");
}
}
#[test]
fn gpu_brightness_contrast_identity() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let data: Vec<u8> = (0..16u8)
.flat_map(|i| [i * 16, i * 8, i * 4, 255])
.collect();
let original = data.clone();
let mut buf = PixelBuffer::new(data, 4, 4, PixelFormat::Rgba8).unwrap();
gpu_brightness_contrast(&mut ctx, &mut buf, 0.0, 1.0).unwrap();
for (g, o) in buf.data.iter().zip(original.iter()) {
assert!(
(*g as i16 - *o as i16).unsigned_abs() <= 1,
"GPU={g} original={o}"
);
}
}
#[test]
fn gpu_blend_normal_opaque() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let src_data = vec![255, 0, 0, 255, 0, 255, 0, 255];
let dst_data = vec![0, 0, 255, 255, 0, 0, 255, 255];
let src = PixelBuffer::new(src_data, 2, 1, PixelFormat::Rgba8).unwrap();
let mut dst = PixelBuffer::new(dst_data, 2, 1, PixelFormat::Rgba8).unwrap();
gpu_blend(&mut ctx, &src, &mut dst, BlendMode::Normal, 1.0).unwrap();
assert!(dst.data[0] > 250, "red channel: {}", dst.data[0]);
assert!(dst.data[1] < 5, "green channel: {}", dst.data[1]);
assert!(dst.data[2] < 5, "blue channel: {}", dst.data[2]);
}
#[test]
fn gpu_blend_dimension_mismatch() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let src = PixelBuffer::zeroed(4, 4, PixelFormat::Rgba8);
let mut dst = PixelBuffer::zeroed(8, 8, PixelFormat::Rgba8);
let result = gpu_blend(&mut ctx, &src, &mut dst, BlendMode::Normal, 1.0);
assert!(result.is_err());
}
#[test]
fn gpu_invert_rejects_non_rgba8() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let mut buf = PixelBuffer::zeroed(4, 4, PixelFormat::Rgb8);
let result = gpu_invert(&mut ctx, &mut buf);
assert!(result.is_err());
}
#[test]
fn gpu_gaussian_blur_uniform_unchanged() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let buf = PixelBuffer::new(vec![128; 8 * 8 * 4], 8, 8, PixelFormat::Rgba8).unwrap();
let blurred = gpu_gaussian_blur(&mut ctx, &buf, 2).unwrap();
for (i, &v) in blurred.data.iter().enumerate() {
assert!(
(v as i16 - 128).unsigned_abs() <= 1,
"pixel byte {i}: expected ~128, got {v}"
);
}
}
#[test]
fn gpu_gaussian_blur_radius_zero_is_identity() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let data: Vec<u8> = (0..16u8)
.flat_map(|i| [i * 16, i * 8, i * 4, 255])
.collect();
let buf = PixelBuffer::new(data.clone(), 4, 4, PixelFormat::Rgba8).unwrap();
let blurred = gpu_gaussian_blur(&mut ctx, &buf, 0).unwrap();
assert_eq!(blurred.data, data);
}
#[test]
fn gpu_gaussian_blur_rejects_non_rgba8() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let buf = PixelBuffer::zeroed(4, 4, PixelFormat::Rgb8);
let result = gpu_gaussian_blur(&mut ctx, &buf, 2);
assert!(result.is_err());
}
#[test]
fn build_gaussian_kernel_sums_to_one() {
let kernel = super::build_gaussian_kernel(5);
let sum: f32 = kernel.iter().sum();
assert!((sum - 1.0).abs() < 1e-5, "kernel sum={sum}");
assert_eq!(kernel.len(), 11); }
#[test]
fn pipeline_cache_reuses_pipelines() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let mut buf1 = PixelBuffer::zeroed(4, 4, PixelFormat::Rgba8);
gpu_invert(&mut ctx, &mut buf1).unwrap();
let mut buf2 = PixelBuffer::zeroed(4, 4, PixelFormat::Rgba8);
gpu_invert(&mut ctx, &mut buf2).unwrap();
}
#[test]
fn gpu_chain_invert_matches_single() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let data: Vec<u8> = (0..64u8).flat_map(|i| [i * 4, i * 3, i * 2, 255]).collect();
let buf = PixelBuffer::new(data.clone(), 8, 8, PixelFormat::Rgba8).unwrap();
let chain_result = GpuChain::new(&mut ctx, &buf)
.unwrap()
.invert()
.unwrap()
.finish()
.unwrap();
let mut single_buf = PixelBuffer::new(data, 8, 8, PixelFormat::Rgba8).unwrap();
gpu_invert(&mut ctx, &mut single_buf).unwrap();
for (c, s) in chain_result.data.iter().zip(single_buf.data.iter()) {
assert!(
(*c as i16 - *s as i16).unsigned_abs() <= 1,
"chain={c} single={s}"
);
}
}
#[test]
fn gpu_chain_multiple_ops() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let data: Vec<u8> = (0..64u8).flat_map(|i| [i * 4, i * 3, i * 2, 255]).collect();
let buf = PixelBuffer::new(data.clone(), 8, 8, PixelFormat::Rgba8).unwrap();
let result = GpuChain::new(&mut ctx, &buf)
.unwrap()
.invert()
.unwrap()
.brightness_contrast(0.1, 1.2)
.unwrap()
.finish()
.unwrap();
assert_ne!(result.data, data, "chain should produce a different result");
assert_eq!(result.width, 8);
assert_eq!(result.height, 8);
assert_eq!(result.data.len(), 8 * 8 * 4);
}
#[test]
fn gpu_chain_with_blur() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let data: Vec<u8> = (0..64u8).flat_map(|i| [i * 4, i * 3, i * 2, 255]).collect();
let buf = PixelBuffer::new(data, 8, 8, PixelFormat::Rgba8).unwrap();
let result = GpuChain::new(&mut ctx, &buf)
.unwrap()
.invert()
.unwrap()
.gaussian_blur(2)
.unwrap()
.finish()
.unwrap();
assert_eq!(result.width, 8);
assert_eq!(result.height, 8);
assert_eq!(result.data.len(), 8 * 8 * 4);
}
#[test]
fn gpu_chain_rejects_non_rgba8() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let buf = PixelBuffer::zeroed(4, 4, PixelFormat::Rgb8);
let result = GpuChain::new(&mut ctx, &buf);
assert!(result.is_err());
}
#[test]
fn gpu_noise_gaussian_modifies_buffer() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let data = vec![128u8; 8 * 8 * 4];
let original = data.clone();
let mut buf = PixelBuffer::new(data, 8, 8, PixelFormat::Rgba8).unwrap();
gpu_noise_gaussian(&mut ctx, &mut buf, 0.3, 42).unwrap();
assert_ne!(buf.data, original, "noise should modify the buffer");
}
#[test]
fn gpu_noise_gaussian_deterministic() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let data = vec![128u8; 8 * 8 * 4];
let mut buf1 = PixelBuffer::new(data.clone(), 8, 8, PixelFormat::Rgba8).unwrap();
let mut buf2 = PixelBuffer::new(data, 8, 8, PixelFormat::Rgba8).unwrap();
gpu_noise_gaussian(&mut ctx, &mut buf1, 0.2, 123).unwrap();
gpu_noise_gaussian(&mut ctx, &mut buf2, 0.2, 123).unwrap();
assert_eq!(
buf1.data, buf2.data,
"same seed+strength should produce same output"
);
}
#[test]
fn gpu_dissolve_at_zero_is_src() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let src_data = vec![200u8; 4 * 4 * 4];
let dst_data = vec![50u8; 4 * 4 * 4];
let src = PixelBuffer::new(src_data.clone(), 4, 4, PixelFormat::Rgba8).unwrap();
let mut dst = PixelBuffer::new(dst_data, 4, 4, PixelFormat::Rgba8).unwrap();
gpu_dissolve(&mut ctx, &src, &mut dst, 0.0).unwrap();
for (g, s) in dst.data.iter().zip(src_data.iter()) {
assert!(
(*g as i16 - *s as i16).unsigned_abs() <= 1,
"GPU={g} src={s}"
);
}
}
#[test]
fn gpu_dissolve_at_one_is_dst() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let src_data = vec![200u8; 4 * 4 * 4];
let dst_data = vec![50u8; 4 * 4 * 4];
let original_dst = dst_data.clone();
let src = PixelBuffer::new(src_data, 4, 4, PixelFormat::Rgba8).unwrap();
let mut dst = PixelBuffer::new(dst_data, 4, 4, PixelFormat::Rgba8).unwrap();
gpu_dissolve(&mut ctx, &src, &mut dst, 1.0).unwrap();
for (g, d) in dst.data.iter().zip(original_dst.iter()) {
assert!(
(*g as i16 - *d as i16).unsigned_abs() <= 1,
"GPU={g} dst={d}"
);
}
}
#[test]
fn gpu_fade_zero_is_black() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let data = vec![200u8; 4 * 4 * 4];
let mut buf = PixelBuffer::new(data, 4, 4, PixelFormat::Rgba8).unwrap();
gpu_fade(&mut ctx, &mut buf, 0.0).unwrap();
for pixel in buf.data.chunks_exact(4) {
assert!(pixel[0] <= 1, "R should be ~0, got {}", pixel[0]);
assert!(pixel[1] <= 1, "G should be ~0, got {}", pixel[1]);
assert!(pixel[2] <= 1, "B should be ~0, got {}", pixel[2]);
assert_eq!(pixel[3], 200, "alpha should be preserved");
}
}
#[test]
fn gpu_fade_one_is_identity() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let data: Vec<u8> = (0..16u8)
.flat_map(|i| [i * 16, i * 8, i * 4, 255])
.collect();
let original = data.clone();
let mut buf = PixelBuffer::new(data, 4, 4, PixelFormat::Rgba8).unwrap();
gpu_fade(&mut ctx, &mut buf, 1.0).unwrap();
for (g, o) in buf.data.iter().zip(original.iter()) {
assert!(
(*g as i16 - *o as i16).unsigned_abs() <= 1,
"GPU={g} original={o}"
);
}
}
#[test]
fn gpu_wipe_zero_is_src() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let src_data = vec![200u8; 4 * 4 * 4];
let dst_data = vec![50u8; 4 * 4 * 4];
let src = PixelBuffer::new(src_data.clone(), 4, 4, PixelFormat::Rgba8).unwrap();
let mut dst = PixelBuffer::new(dst_data, 4, 4, PixelFormat::Rgba8).unwrap();
gpu_wipe(&mut ctx, &src, &mut dst, 0.0).unwrap();
for (g, s) in dst.data.iter().zip(src_data.iter()) {
assert!(
(*g as i16 - *s as i16).unsigned_abs() <= 1,
"GPU={g} src={s}"
);
}
}
#[test]
fn gpu_wipe_one_is_dst() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let src_data = vec![200u8; 4 * 4 * 4];
let dst_data = vec![50u8; 4 * 4 * 4];
let original_dst = dst_data.clone();
let src = PixelBuffer::new(src_data, 4, 4, PixelFormat::Rgba8).unwrap();
let mut dst = PixelBuffer::new(dst_data, 4, 4, PixelFormat::Rgba8).unwrap();
gpu_wipe(&mut ctx, &src, &mut dst, 1.0).unwrap();
for (g, d) in dst.data.iter().zip(original_dst.iter()) {
assert!(
(*g as i16 - *d as i16).unsigned_abs() <= 1,
"GPU={g} dst={d}"
);
}
}
#[test]
fn gpu_crop_basic() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let buf = PixelBuffer::zeroed(8, 8, PixelFormat::Rgba8);
let cropped = gpu_crop(&mut ctx, &buf, 2, 2, 6, 6).unwrap();
assert_eq!(cropped.width, 4);
assert_eq!(cropped.height, 4);
assert_eq!(cropped.data.len(), 4 * 4 * 4);
}
#[test]
fn gpu_crop_rejects_non_rgba8() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let buf = PixelBuffer::zeroed(8, 8, PixelFormat::Rgb8);
let result = gpu_crop(&mut ctx, &buf, 0, 0, 4, 4);
assert!(result.is_err());
}
#[test]
fn gpu_resize_nearest_doubles() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let buf = PixelBuffer::zeroed(4, 4, PixelFormat::Rgba8);
let resized = gpu_resize(&mut ctx, &buf, 8, 8, ScaleFilter::Nearest).unwrap();
assert_eq!(resized.width, 8);
assert_eq!(resized.height, 8);
assert_eq!(resized.data.len(), 8 * 8 * 4);
}
#[test]
fn gpu_resize_bilinear_halves() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let buf = PixelBuffer::new(vec![128; 8 * 8 * 4], 8, 8, PixelFormat::Rgba8).unwrap();
let resized = gpu_resize(&mut ctx, &buf, 4, 4, ScaleFilter::Bilinear).unwrap();
assert_eq!(resized.width, 4);
assert_eq!(resized.height, 4);
assert_eq!(resized.data.len(), 4 * 4 * 4);
}
#[test]
fn gpu_flip_horizontal_roundtrip() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let data: Vec<u8> = (0..16u8)
.flat_map(|i| [i * 16, i * 8, i * 4, 255])
.collect();
let buf = PixelBuffer::new(data.clone(), 4, 4, PixelFormat::Rgba8).unwrap();
let f1 = gpu_flip_horizontal(&mut ctx, &buf).unwrap();
let f2 = gpu_flip_horizontal(&mut ctx, &f1).unwrap();
assert_eq!(f2.data, data);
}
#[test]
fn gpu_flip_vertical_roundtrip() {
let mut ctx = match try_gpu() {
Some(ctx) => ctx,
None => return,
};
let data: Vec<u8> = (0..16u8)
.flat_map(|i| [i * 16, i * 8, i * 4, 255])
.collect();
let buf = PixelBuffer::new(data.clone(), 4, 4, PixelFormat::Rgba8).unwrap();
let f1 = gpu_flip_vertical(&mut ctx, &buf).unwrap();
let f2 = gpu_flip_vertical(&mut ctx, &f1).unwrap();
assert_eq!(f2.data, data);
}
}