use std::sync::Arc;
use darkly::brush::compile_graph;
use darkly::brush::eval::BrushGraphRunner;
use darkly::brush::gpu_context::{BrushGpuContext, BrushPerfCounters, DabBatch, StrokeResources};
use darkly::brush::pipeline::BrushPipelines;
use darkly::brush::stroke_buffer::StrokeBuffer;
use darkly::brush::wire::BrushWireType;
use darkly::gpu::paint_target::GpuPaintTarget;
use darkly::gpu::test_utils::{readback_texture, test_device};
use darkly::nodegraph::Graph;
const W: u32 = 32;
const H: u32 = 32;
const SENTINEL_RGBA: [u8; 4] = [255, 0, 255, 255];
fn paint_solid(
device: &wgpu::Device,
queue: &wgpu::Queue,
view: &wgpu::TextureView,
color: wgpu::Color,
) {
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("paint-solid"),
});
let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("paint-solid-pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(color),
store: wgpu::StoreOp::Store,
},
})],
..Default::default()
});
queue.submit([encoder.finish()]);
}
fn make_pre_stroke(device: &wgpu::Device) -> (wgpu::Texture, wgpu::TextureView) {
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("pre-stroke-tex"),
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::Rgba8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
(tex, view)
}
enum Setup {
ScratchPrefilled(wgpu::Color),
PreStrokeWithSentinel,
}
fn run_begin_stroke(graph: &Graph<BrushWireType>, setup: Setup) -> Vec<u8> {
let (device, queue) = test_device();
let device = Arc::new(device);
let queue = Arc::new(queue);
let pipelines = BrushPipelines::new(
&device,
&queue,
&darkly::gpu::selection::selection_mask_bgl(&device),
);
let mut stroke_buffer = StrokeBuffer::new(&device, W, H, &pipelines);
let (paint_target_tex, paint_target_view) = make_pre_stroke(&device);
match &setup {
Setup::PreStrokeWithSentinel => {
paint_solid(
&device,
&queue,
stroke_buffer.pre_stroke_view(),
wgpu::Color {
r: SENTINEL_RGBA[0] as f64 / 255.0,
g: SENTINEL_RGBA[1] as f64 / 255.0,
b: SENTINEL_RGBA[2] as f64 / 255.0,
a: SENTINEL_RGBA[3] as f64 / 255.0,
},
);
}
Setup::ScratchPrefilled(color) => {
paint_solid(
&device,
&queue,
stroke_buffer.scratch_mut().write_view(),
*color,
);
}
};
let mut runner: BrushGraphRunner = compile_graph(graph).expect("brush compiles");
let encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("begin-stroke"),
});
let (scratch, pre_stroke_tex, pre_stroke_bg) = stroke_buffer.parts_for_brush_ctx();
let mut ctx = BrushGpuContext {
encoder,
device: &device,
queue: &queue,
pipelines: &pipelines,
selection_bind_group: pipelines.default_selection_bind_group(),
canvas_width: W,
canvas_height: H,
canvas_origin: [0, 0],
blend_mode: 0,
view_rotation: 0.0,
perf: BrushPerfCounters::default(),
stroke: Some(StrokeResources {
scratch,
paint_target: GpuPaintTarget::from_canvas_texture(
&paint_target_tex,
&paint_target_view,
wgpu::TextureFormat::Rgba8Unorm,
darkly::coord::CanvasRect::from_xywh(0, 0, W, H),
),
pre_stroke_texture: pre_stroke_tex,
pre_stroke_bind_group: pre_stroke_bg,
}),
preview: None,
dab_batch: DabBatch::default(),
};
runner.begin_stroke(&mut ctx);
queue.submit([ctx.encoder.finish()]);
readback_texture(
&device,
&queue,
stroke_buffer.scratch().write_texture(),
wgpu::TextureFormat::Rgba8Unorm,
W,
H,
)
}
fn builtin_graph(name: &str) -> Graph<BrushWireType> {
darkly::brush::builtin_brushes::all()
.into_iter()
.find(|b| b.metadata.name == name)
.unwrap_or_else(|| panic!("builtin brush `{name}` not registered"))
.metadata
.graph
}
fn assert_all(rgba: &[u8], expected: [u8; 4]) {
let mut mismatches = 0usize;
let mut first_bad = None;
for (i, px) in rgba.chunks_exact(4).enumerate() {
if px != expected {
if first_bad.is_none() {
first_bad = Some((i, [px[0], px[1], px[2], px[3]]));
}
mismatches += 1;
}
}
assert_eq!(
mismatches, 0,
"{} pixels differ from expected {:?}; first at idx {:?}",
mismatches, expected, first_bad
);
}
#[test]
fn paint_terminal_clears_scratch_to_transparent() {
let rgba = run_begin_stroke(
&builtin_graph("Round"),
Setup::ScratchPrefilled(wgpu::Color {
r: 1.0,
g: 0.0,
b: 1.0,
a: 1.0,
}),
);
assert_all(&rgba, [0, 0, 0, 0]);
}
#[test]
fn watercolor_terminal_clears_scratch_to_transparent() {
let rgba = run_begin_stroke(
&builtin_graph("Smooth Watercolor"),
Setup::ScratchPrefilled(wgpu::Color {
r: 1.0,
g: 0.0,
b: 1.0,
a: 1.0,
}),
);
assert_all(&rgba, [0, 0, 0, 0]);
}
#[test]
fn smudge_terminal_seeds_scratch_from_pre_stroke() {
let rgba = run_begin_stroke(&builtin_graph("Smudge"), Setup::PreStrokeWithSentinel);
assert_all(&rgba, SENTINEL_RGBA);
}
#[test]
fn liquify_terminal_seeds_scratch_from_pre_stroke() {
let rgba = run_begin_stroke(&builtin_graph("Liquify"), Setup::PreStrokeWithSentinel);
assert_all(&rgba, SENTINEL_RGBA);
}