use darkly::coord::CanvasRect;
use darkly::gpu::atlas::CanvasFrame;
use darkly::gpu::paint_target::{GpuPaintTarget, PaintPipelines};
use darkly::gpu::region_store::RegionScratch;
use darkly::gpu::test_utils::*;
fn cr(x: i32, y: i32, w: u32, h: u32) -> CanvasRect {
CanvasRect::from_xywh(x, y, w, h)
}
fn frame<'a>(tex: &'a wgpu::Texture, w: u32, h: u32) -> CanvasFrame<'a> {
CanvasFrame {
texture: tex,
canvas_extent: cr(0, 0, w, h),
}
}
fn encoder(device: &wgpu::Device) -> wgpu::CommandEncoder {
device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("test"),
})
}
fn submit(queue: &wgpu::Queue, encoder: wgpu::CommandEncoder) {
queue.submit([encoder.finish()]);
}
fn pixel_at(pixels: &[u8], w: u32, x: u32, y: u32, bpp: u32) -> &[u8] {
let offset = ((y * w + x) * bpp) as usize;
&pixels[offset..offset + bpp as usize]
}
#[test]
fn gpu_gradient_linear_interpolation() {
let (device, queue) = test_device();
let (w, h) = (128, 128);
let fmt = wgpu::TextureFormat::Rgba8Unorm;
let (tex, view) = create_test_texture(&device, &queue, w, h, &vec![0u8; (w * h * 4) as usize]);
let pipelines = PaintPipelines::new(
&device,
&queue,
&darkly::gpu::selection::selection_mask_bgl(&device),
);
let target = GpuPaintTarget::from_canvas_texture(
&tex,
&view,
fmt,
darkly::coord::CanvasRect::from_xywh(0, 0, w, h),
);
let mut enc = encoder(&device);
target.linear_gradient(
&mut enc,
&pipelines,
&queue,
0.0,
0.0, 128.0,
128.0, [255, 255, 255, 255], [0, 0, 0, 255], None,
);
submit(&queue, enc);
let pixels = readback_texture(&device, &queue, &tex, fmt, w, h);
let tl = pixel_at(&pixels, w, 0, 0, 4);
assert!(tl[0] > 200, "top-left R should be near 255, got {}", tl[0]);
assert_eq!(tl[3], 255, "alpha should be 255");
let br = pixel_at(&pixels, w, 127, 127, 4);
assert!(br[0] < 55, "bottom-right R should be near 0, got {}", br[0]);
assert_eq!(br[3], 255, "alpha should be 255");
let mid = pixel_at(&pixels, w, 64, 64, 4);
assert!(
mid[0] > 80 && mid[0] < 175,
"center R should be roughly 128, got {}",
mid[0]
);
}
#[test]
fn gpu_gradient_undo() {
let (device, queue) = test_device();
let (w, h) = (64, 64);
let fmt = wgpu::TextureFormat::Rgba8Unorm;
let (tex, view) = create_test_texture(&device, &queue, w, h, &vec![0u8; (w * h * 4) as usize]);
let pipelines = PaintPipelines::new(
&device,
&queue,
&darkly::gpu::selection::selection_mask_bgl(&device),
);
let mut store = RegionScratch::new(&device, w, h);
let mut enc = encoder(&device);
let snap = store.save_region(&device, &mut enc, &frame(&tex, w, h), fmt, cr(0, 0, w, h));
submit(&queue, enc);
let target = GpuPaintTarget::from_canvas_texture(
&tex,
&view,
fmt,
darkly::coord::CanvasRect::from_xywh(0, 0, w, h),
);
let mut enc = encoder(&device);
target.linear_gradient(
&mut enc,
&pipelines,
&queue,
0.0,
0.0,
64.0,
0.0,
[255, 0, 0, 255],
[0, 0, 255, 255],
None,
);
submit(&queue, enc);
let mut enc = encoder(&device);
let (entry, _req) = store.commit_region(
&mut enc,
&device,
darkly::layer::LayerId::from_ffi(1),
&frame(&tex, w, h),
&snap,
cr(0, 0, w, h),
);
submit(&queue, enc);
let pixels = readback_texture(&device, &queue, &tex, fmt, w, h);
assert!(pixel_at(&pixels, w, 0, 0, 4)[0] > 200, "left should be red");
assert!(
pixel_at(&pixels, w, 63, 0, 4)[2] > 200,
"right should be blue"
);
let mut enc = encoder(&device);
let (_forward, _req) = store.restore_region(&mut enc, &device, &entry, &frame(&tex, w, h));
submit(&queue, enc);
let pixels = readback_texture(&device, &queue, &tex, fmt, w, h);
assert_eq!(
pixel_at(&pixels, w, 0, 0, 4)[3],
0,
"after undo, should be transparent"
);
assert_eq!(
pixel_at(&pixels, w, 63, 0, 4)[3],
0,
"after undo, should be transparent"
);
}
#[test]
fn gpu_flood_fill_interior() {
let (device, queue) = test_device();
let (w, h) = (64, 64);
let fmt = wgpu::TextureFormat::Rgba8Unorm;
let (tex, view) = create_test_texture(&device, &queue, w, h, &vec![0u8; (w * h * 4) as usize]);
let pipelines = PaintPipelines::new(
&device,
&queue,
&darkly::gpu::selection::selection_mask_bgl(&device),
);
let target = GpuPaintTarget::from_canvas_texture(
&tex,
&view,
fmt,
darkly::coord::CanvasRect::from_xywh(0, 0, w, h),
);
let red = [255u8, 0, 0, 255];
let mut enc = encoder(&device);
use darkly::coord::CanvasRect;
target.fill_rect(
&mut enc,
&pipelines,
&queue,
CanvasRect::from_xywh(10, 10, 40, 2),
red,
); submit(&queue, enc);
let mut enc = encoder(&device);
target.fill_rect(
&mut enc,
&pipelines,
&queue,
CanvasRect::from_xywh(10, 48, 40, 2),
red,
); submit(&queue, enc);
let mut enc = encoder(&device);
target.fill_rect(
&mut enc,
&pipelines,
&queue,
CanvasRect::from_xywh(10, 10, 2, 40),
red,
); submit(&queue, enc);
let mut enc = encoder(&device);
target.fill_rect(
&mut enc,
&pipelines,
&queue,
CanvasRect::from_xywh(48, 10, 2, 40),
red,
); submit(&queue, enc);
let pixels = readback_texture(&device, &queue, &tex, fmt, w, h);
let fill_mask = darkly::gpu::flood_fill::flood_fill_rgba(&pixels, w, h, 30, 30, 0);
assert_eq!(
fill_mask[(30 * w + 30) as usize],
255,
"interior should be filled"
);
assert_eq!(fill_mask[0], 0, "exterior should not be filled");
assert_eq!(
fill_mask[(10 * w + 10) as usize],
0,
"border should not be filled"
);
let mask_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("test-fill-mask"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &mask_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&fill_mask,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(w),
rows_per_image: Some(h),
},
wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
);
let mask_view = mask_tex.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let mask_bg = pipelines.create_selection_bind_group(&device, &mask_view, &sampler);
let mut enc = encoder(&device);
target.fill_rect_with_selection(
&mut enc,
&pipelines,
&queue,
CanvasRect::from_xywh(0, 0, w, h),
[0, 0, 255, 255],
&mask_bg,
);
submit(&queue, enc);
let pixels = readback_texture(&device, &queue, &tex, fmt, w, h);
let interior = pixel_at(&pixels, w, 30, 30, 4);
assert!(
interior[2] > 200,
"interior should be blue, B={}",
interior[2]
);
assert!(
interior[0] < 55,
"interior R should be low, got {}",
interior[0]
);
let border = pixel_at(&pixels, w, 10, 10, 4);
assert_eq!(border[0], 255, "border should be red");
let exterior = pixel_at(&pixels, w, 0, 0, 4);
assert_eq!(exterior[3], 0, "exterior should be transparent");
}
#[test]
fn gpu_flood_fill_undo() {
let (device, queue) = test_device();
let (w, h) = (64, 64);
let fmt = wgpu::TextureFormat::Rgba8Unorm;
let (tex, view) = create_test_texture(&device, &queue, w, h, &vec![0u8; (w * h * 4) as usize]);
let pipelines = PaintPipelines::new(
&device,
&queue,
&darkly::gpu::selection::selection_mask_bgl(&device),
);
let mut store = RegionScratch::new(&device, w, h);
let mut enc = encoder(&device);
let snap = store.save_region(&device, &mut enc, &frame(&tex, w, h), fmt, cr(0, 0, w, h));
submit(&queue, enc);
let pixels_before = readback_texture(&device, &queue, &tex, fmt, w, h);
let fill_mask = darkly::gpu::flood_fill::flood_fill_rgba(&pixels_before, w, h, 0, 0, 0);
let mask_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("test-fill-mask"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &mask_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&fill_mask,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(w),
rows_per_image: Some(h),
},
wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
);
let mask_view = mask_tex.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let mask_bg = pipelines.create_selection_bind_group(&device, &mask_view, &sampler);
let target = GpuPaintTarget::from_canvas_texture(
&tex,
&view,
fmt,
darkly::coord::CanvasRect::from_xywh(0, 0, w, h),
);
let mut enc = encoder(&device);
target.fill_rect_with_selection(
&mut enc,
&pipelines,
&queue,
CanvasRect::from_xywh(0, 0, w, h),
[0, 255, 0, 255],
&mask_bg,
);
submit(&queue, enc);
let mut enc = encoder(&device);
let (entry, _req) = store.commit_region(
&mut enc,
&device,
darkly::layer::LayerId::from_ffi(1),
&frame(&tex, w, h),
&snap,
cr(0, 0, w, h),
);
submit(&queue, enc);
let pixels = readback_texture(&device, &queue, &tex, fmt, w, h);
assert!(
pixel_at(&pixels, w, 32, 32, 4)[1] > 200,
"should be green after fill"
);
let mut enc = encoder(&device);
let (_forward, _req) = store.restore_region(&mut enc, &device, &entry, &frame(&tex, w, h));
submit(&queue, enc);
let pixels = readback_texture(&device, &queue, &tex, fmt, w, h);
assert_eq!(
pixel_at(&pixels, w, 32, 32, 4)[3],
0,
"after undo, should be transparent"
);
}
#[test]
fn gpu_color_pick_readback() {
let (device, queue) = test_device();
let (w, h) = (64, 64);
let fmt = wgpu::TextureFormat::Rgba8Unorm;
let (tex, view) = create_test_texture(&device, &queue, w, h, &vec![0u8; (w * h * 4) as usize]);
let pipelines = PaintPipelines::new(
&device,
&queue,
&darkly::gpu::selection::selection_mask_bgl(&device),
);
let target = GpuPaintTarget::from_canvas_texture(
&tex,
&view,
fmt,
darkly::coord::CanvasRect::from_xywh(0, 0, w, h),
);
let mut enc = encoder(&device);
target.fill_rect(
&mut enc,
&pipelines,
&queue,
CanvasRect::from_xywh(10, 10, 1, 1),
[255, 0, 0, 255],
);
submit(&queue, enc);
let mut enc = encoder(&device);
target.fill_rect(
&mut enc,
&pipelines,
&queue,
CanvasRect::from_xywh(50, 50, 1, 1),
[0, 0, 255, 255],
);
submit(&queue, enc);
let pick = |x: u32, y: u32| -> [u8; 4] {
let mut enc = encoder(&device);
let request = darkly::gpu::readback::request_readback(
&device,
&mut enc,
&tex,
fmt,
darkly::coord::LayerRect::from_xywh(x, y, 1, 1),
);
submit(&queue, enc);
let data = request.blocking_read(&device);
[data[0], data[1], data[2], data[3]]
};
let red = pick(10, 10);
assert_eq!(
red,
[255, 0, 0, 255],
"should pick red at (10,10), got {:?}",
red
);
let blue = pick(50, 50);
assert_eq!(
blue,
[0, 0, 255, 255],
"should pick blue at (50,50), got {:?}",
blue
);
let empty = pick(0, 0);
assert_eq!(
empty,
[0, 0, 0, 0],
"should pick transparent at (0,0), got {:?}",
empty
);
}
#[test]
fn gpu_gradient_on_mask() {
let (device, queue) = test_device();
let (w, h) = (64, 64);
let fmt = wgpu::TextureFormat::R8Unorm;
let white = vec![255u8; (w * h) as usize];
let (tex, view) = create_test_texture_with_format(&device, &queue, w, h, &white, fmt);
let pipelines = PaintPipelines::new(
&device,
&queue,
&darkly::gpu::selection::selection_mask_bgl(&device),
);
let target = GpuPaintTarget::from_canvas_texture(
&tex,
&view,
fmt,
darkly::coord::CanvasRect::from_xywh(0, 0, w, h),
);
let mut enc = encoder(&device);
target.linear_gradient(
&mut enc,
&pipelines,
&queue,
0.0,
0.0,
64.0,
0.0,
[255, 255, 255, 255], [0, 0, 0, 255], None,
);
submit(&queue, enc);
let pixels = readback_texture(&device, &queue, &tex, fmt, w, h);
assert!(
pixels[(32 * w + 2) as usize] > 200,
"left should be bright, got {}",
pixels[(32 * w + 2) as usize]
);
assert!(
pixels[(32 * w + 62) as usize] < 55,
"right should be dark, got {}",
pixels[(32 * w + 62) as usize]
);
}
#[test]
fn gpu_fill_rect_with_mask() {
let (device, queue) = test_device();
let (w, h) = (64, 64);
let fmt = wgpu::TextureFormat::Rgba8Unorm;
let (tex, view) = create_test_texture(&device, &queue, w, h, &vec![0u8; (w * h * 4) as usize]);
let pipelines = PaintPipelines::new(
&device,
&queue,
&darkly::gpu::selection::selection_mask_bgl(&device),
);
let mut mask_data = vec![0u8; (w * h) as usize];
for y in 16..48 {
for x in 16..48 {
mask_data[(y * w + x) as usize] = 255;
}
}
let (mask_tex, _) = create_test_texture_with_format(
&device,
&queue,
w,
h,
&mask_data,
wgpu::TextureFormat::R8Unorm,
);
let mask_view = mask_tex.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let mask_bg = pipelines.create_selection_bind_group(&device, &mask_view, &sampler);
let target = GpuPaintTarget::from_canvas_texture(
&tex,
&view,
fmt,
darkly::coord::CanvasRect::from_xywh(0, 0, w, h),
);
let mut enc = encoder(&device);
target.fill_rect_with_selection(
&mut enc,
&pipelines,
&queue,
CanvasRect::from_xywh(0, 0, w, h),
[0, 255, 0, 255],
&mask_bg,
);
submit(&queue, enc);
let pixels = readback_texture(&device, &queue, &tex, fmt, w, h);
let inside = pixel_at(&pixels, w, 32, 32, 4);
assert_eq!(
inside[1], 255,
"inside mask should be green, G={}",
inside[1]
);
assert_eq!(inside[3], 255, "inside mask alpha should be 255");
let outside = pixel_at(&pixels, w, 0, 0, 4);
assert_eq!(
outside[3], 0,
"outside mask should be transparent, A={}",
outside[3]
);
}
#[test]
fn gpu_gradient_on_offset_layer_uses_canvas_endpoints() {
let (device, queue) = test_device();
let canvas_w: u32 = 256;
let canvas_h: u32 = 256;
let off_x: i32 = -50;
let off_y: i32 = -50;
let (lw, lh) = (200u32, 200u32);
let fmt = wgpu::TextureFormat::Rgba8Unorm;
let (tex, view) =
create_test_texture(&device, &queue, lw, lh, &vec![0u8; (lw * lh * 4) as usize]);
let pipelines = PaintPipelines::new(
&device,
&queue,
&darkly::gpu::selection::selection_mask_bgl(&device),
);
let target = GpuPaintTarget::from_extent(
&tex,
&view,
fmt,
darkly::coord::CanvasRect::from_xywh(off_x, off_y, lw, lh),
darkly::coord::CanvasRect::from_xywh(0, 0, canvas_w, canvas_h),
);
let mut enc = encoder(&device);
target.linear_gradient(
&mut enc,
&pipelines,
&queue,
0.0,
0.0,
100.0,
0.0,
[255, 255, 255, 255],
[0, 0, 0, 255],
None,
);
submit(&queue, enc);
let pixels = readback_texture(&device, &queue, &tex, fmt, lw, lh);
let start = pixel_at(&pixels, lw, 50, 50, 4);
assert!(
start[0] > 200,
"canvas (0,0) should be near-white (gradient start), got R={}",
start[0]
);
let end = pixel_at(&pixels, lw, 150, 50, 4);
assert!(
end[0] < 55,
"canvas (100,0) should be near-black (gradient end), got R={}",
end[0]
);
let mid = pixel_at(&pixels, lw, 100, 50, 4);
assert!(
mid[0] > 80 && mid[0] < 175,
"canvas (50,0) should be midpoint gradient, got R={}",
mid[0]
);
}