use std::sync::{Arc, OnceLock};
use darkly::brush::compile_graph;
use darkly::brush::eval::BrushGraphRunner;
use darkly::brush::gpu_context::{BrushGpuContext, BrushPerfCounters, DabBatch, StrokeResources};
use darkly::brush::paint_info::PaintInformation;
use darkly::brush::pipeline::BrushPipelines;
use darkly::brush::stroke_buffer::StrokeBuffer;
use darkly::gpu::test_utils::{create_test_texture, readback_texture, test_device};
const CANVAS: u32 = 128;
fn shared_device() -> (Arc<wgpu::Device>, Arc<wgpu::Queue>) {
static HANDLES: OnceLock<(Arc<wgpu::Device>, Arc<wgpu::Queue>)> = OnceLock::new();
HANDLES
.get_or_init(|| {
let (d, q) = test_device();
(Arc::new(d), Arc::new(q))
})
.clone()
}
fn two_tone_canvas(red_x_threshold: u32) -> Vec<u8> {
let mut out = vec![0u8; (CANVAS * CANVAS * 4) as usize];
for y in 0..CANVAS {
for x in 0..CANVAS {
let idx = ((y * CANVAS + x) * 4) as usize;
if x < red_x_threshold {
out[idx] = 220;
out[idx + 1] = 20;
out[idx + 2] = 20;
}
out[idx + 3] = 255;
}
}
out
}
fn pixel(rgba: &[u8], x: u32, y: u32) -> [u8; 4] {
let idx = ((y * CANVAS + x) * 4) as usize;
[rgba[idx], rgba[idx + 1], rgba[idx + 2], rgba[idx + 3]]
}
fn render_smudge_dabs(size_override: f32, dabs: &[([f32; 2], [f32; 2])]) -> Vec<u8> {
let brush = darkly::brush::builtin_brushes::all()
.into_iter()
.find(|b| b.metadata.name == "Smudge")
.unwrap();
let mut graph = brush.metadata.graph.clone();
let term_id = darkly::brush::find_terminal(&graph).expect("Smudge brush has a terminal");
graph
.set_port_default(term_id, "size", size_override)
.unwrap();
graph.set_port_default(term_id, "rate", 0.85).unwrap();
let (device, queue) = shared_device();
let (layer_texture, layer_view) =
create_test_texture(&device, &queue, CANVAS, CANVAS, &two_tone_canvas(36));
let pipelines = BrushPipelines::new(
&device,
&queue,
&darkly::gpu::selection::selection_mask_bgl(&device),
);
let mut stroke_buffer = StrokeBuffer::new(&device, CANVAS, CANVAS, &pipelines);
let pre_stroke = darkly::gpu::paint_target::GpuPaintTarget::from_canvas_texture(
&layer_texture,
&layer_view,
wgpu::TextureFormat::Rgba8Unorm,
darkly::coord::CanvasRect::from_xywh(0, 0, CANVAS, CANVAS),
);
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("smudge-test-pre-stroke"),
});
stroke_buffer.save_pre_stroke(&device, &mut enc, &pipelines, &pre_stroke);
queue.submit([enc.finish()]);
let mut runner: BrushGraphRunner = compile_graph(&graph).expect("brush compiles");
macro_rules! make_ctx {
($label:expr) => {{
let (scratch, pre_stroke_tex, pre_stroke_bg) = stroke_buffer.parts_for_brush_ctx();
BrushGpuContext {
encoder: device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some($label),
}),
device: &device,
queue: &queue,
pipelines: &pipelines,
selection_bind_group: pipelines.default_selection_bind_group(),
canvas_width: CANVAS,
canvas_height: CANVAS,
canvas_origin: [0, 0],
blend_mode: 0,
view_rotation: 0.0,
perf: BrushPerfCounters::default(),
stroke: Some(StrokeResources {
scratch,
paint_target: darkly::gpu::paint_target::GpuPaintTarget::from_canvas_texture(
&layer_texture,
&layer_view,
wgpu::TextureFormat::Rgba8Unorm,
darkly::coord::CanvasRect::from_xywh(0, 0, CANVAS, CANVAS),
),
pre_stroke_texture: pre_stroke_tex,
pre_stroke_bind_group: pre_stroke_bg,
}),
preview: None,
dab_batch: DabBatch::default(),
}
}};
}
{
let mut ctx = make_ctx!("smudge-test-begin");
runner.begin_stroke(&mut ctx);
queue.submit([ctx.encoder.finish()]);
}
{
let mut ctx = make_ctx!("smudge-test-flush");
for (i, (pos, motion)) in dabs.iter().enumerate() {
let info = PaintInformation {
pos: *pos,
motion: *motion,
pressure: 1.0,
distance: 10.0,
..Default::default()
};
runner.seed_sensors(&info, [1.0, 1.0, 1.0, 1.0], 0xC0FFEE, i as u32);
runner.execute_cpu();
runner.execute_gpu(&mut ctx);
}
runner.flush_dabs(&mut ctx);
runner.commit(&mut ctx);
queue.submit([ctx.encoder.finish()]);
}
readback_texture(
&device,
&queue,
&layer_texture,
wgpu::TextureFormat::Rgba8Unorm,
CANVAS,
CANVAS,
)
}
#[test]
fn single_smudge_dab_pulls_red_via_motion() {
let rgba = render_smudge_dabs(0.1, &[([60.0, 64.0], [30.0, 0.0])]);
let centre = pixel(&rgba, 60, 64);
assert!(
centre[0] > 80,
"single smudge dab pulling from a red region should lift red at \
the centre; got {centre:?}"
);
assert!(
centre[0] > centre[1] + 30,
"smear pulled from red should leave red > green; got {centre:?}"
);
}
#[test]
fn smudge_dab2_reads_dab1_deposit_not_pre_stroke() {
let rgba = render_smudge_dabs(
0.1,
&[([60.0, 64.0], [30.0, 0.0]), ([90.0, 64.0], [30.0, 0.0])],
);
let centre_2 = pixel(&rgba, 90, 64);
assert!(
centre_2[0] > 50,
"dab 2 must read dab 1's red-tinted writeback through the per-dab \
barrier — got centre {centre_2:?}. Pre-stroke at (60, 64) was \
BLACK; if dab 2 sees this value it means the inter-dab \
`copy_texture_to_texture` (and thus the per-dab serialization) \
is broken."
);
assert!(
centre_2[0] > centre_2[1] + 30,
"dab 2 smear from dab 1's deposit should leave red dominant; \
got {centre_2:?}"
);
}