improve painting performance
This commit is contained in:
parent
4e79abdc35
commit
63a8080e60
|
|
@ -139,7 +139,8 @@ impl BrushEngine {
|
||||||
if stroke.points.len() < 2 {
|
if stroke.points.len() < 2 {
|
||||||
if let Some(pt) = stroke.points.first() {
|
if let Some(pt) = stroke.points.first() {
|
||||||
let r = stroke.brush_settings.radius_at_pressure(pt.pressure);
|
let r = stroke.brush_settings.radius_at_pressure(pt.pressure);
|
||||||
let o = stroke.brush_settings.opacity_at_pressure(pt.pressure);
|
let raw_o = stroke.brush_settings.opacity_at_pressure(pt.pressure);
|
||||||
|
let o = 1.0 - (1.0 - raw_o).powf(stroke.brush_settings.dabs_per_radius * 0.5);
|
||||||
// Single-tap smudge has no direction — skip (same as CPU engine)
|
// Single-tap smudge has no direction — skip (same as CPU engine)
|
||||||
if !matches!(stroke.blend_mode, RasterBlendMode::Smudge) {
|
if !matches!(stroke.blend_mode, RasterBlendMode::Smudge) {
|
||||||
push_dab(&mut dabs, &mut bbox, pt.x, pt.y, r, o, 0.0, 0.0, 0.0);
|
push_dab(&mut dabs, &mut bbox, pt.x, pt.y, r, o, 0.0, 0.0, 0.0);
|
||||||
|
|
@ -177,7 +178,12 @@ impl BrushEngine {
|
||||||
let y2 = p0.y + t * dy;
|
let y2 = p0.y + t * dy;
|
||||||
let pressure2 = p0.pressure + t * (p1.pressure - p0.pressure);
|
let pressure2 = p0.pressure + t * (p1.pressure - p0.pressure);
|
||||||
let radius2 = stroke.brush_settings.radius_at_pressure(pressure2);
|
let radius2 = stroke.brush_settings.radius_at_pressure(pressure2);
|
||||||
let opacity2 = stroke.brush_settings.opacity_at_pressure(pressure2);
|
let raw_opacity = stroke.brush_settings.opacity_at_pressure(pressure2);
|
||||||
|
// Normalize per-dab opacity so dense dabs don't saturate faster than sparse ones.
|
||||||
|
// Formula: per_dab = 1 − (1 − raw)^(dabs_per_radius / 2)
|
||||||
|
// Derivation: N = 2/dabs_per_radius dabs cover one full diameter at the centre;
|
||||||
|
// accumulated = 1 − (1 − per_dab)^N = raw → per_dab = 1 − (1−raw)^(dabs_per_radius/2)
|
||||||
|
let opacity2 = 1.0 - (1.0 - raw_opacity).powf(stroke.brush_settings.dabs_per_radius * 0.5);
|
||||||
|
|
||||||
if matches!(stroke.blend_mode, RasterBlendMode::Smudge) {
|
if matches!(stroke.blend_mode, RasterBlendMode::Smudge) {
|
||||||
let ndx = dx / seg_len;
|
let ndx = dx / seg_len;
|
||||||
|
|
|
||||||
|
|
@ -276,12 +276,10 @@ impl GpuBrushEngine {
|
||||||
|
|
||||||
/// Dispatch the brush compute shader for `dabs` onto the canvas of `keyframe_id`.
|
/// Dispatch the brush compute shader for `dabs` onto the canvas of `keyframe_id`.
|
||||||
///
|
///
|
||||||
/// Each dab is dispatched as a separate copy+compute+swap so that every dab
|
/// Each dab is dispatched serially: copy the dab's bounding box from src→dst,
|
||||||
/// reads the result of the previous one. This is required for the smudge tool:
|
/// dispatch the compute shader, then swap. The bbox-only copy is safe because
|
||||||
/// if all dabs were batched into one dispatch they would all read the pre-batch
|
/// neither normal/erase nor smudge reads outside the current dab's radius.
|
||||||
/// canvas state, breaking the carry-forward that makes smudge drag pixels along.
|
|
||||||
///
|
///
|
||||||
/// `dab_bbox` is the union bounding box (unused here; kept for API compat).
|
|
||||||
/// If `dabs` is empty, does nothing.
|
/// If `dabs` is empty, does nothing.
|
||||||
pub fn render_dabs(
|
pub fn render_dabs(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -294,56 +292,41 @@ impl GpuBrushEngine {
|
||||||
canvas_h: u32,
|
canvas_h: u32,
|
||||||
) {
|
) {
|
||||||
if dabs.is_empty() { return; }
|
if dabs.is_empty() { return; }
|
||||||
|
|
||||||
if !self.canvases.contains_key(&keyframe_id) { return; }
|
if !self.canvases.contains_key(&keyframe_id) { return; }
|
||||||
|
|
||||||
let full_extent = wgpu::Extent3d {
|
|
||||||
width: self.canvases[&keyframe_id].width,
|
|
||||||
height: self.canvases[&keyframe_id].height,
|
|
||||||
depth_or_array_layers: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
for dab in dabs {
|
for dab in dabs {
|
||||||
// Per-dab bounding box
|
|
||||||
let r_fringe = dab.radius + 1.0;
|
let r_fringe = dab.radius + 1.0;
|
||||||
let dx0 = (dab.x - r_fringe).floor() as i32;
|
let x0 = ((dab.x - r_fringe).floor() as i32).max(0) as u32;
|
||||||
let dy0 = (dab.y - r_fringe).floor() as i32;
|
let y0 = ((dab.y - r_fringe).floor() as i32).max(0) as u32;
|
||||||
let dx1 = (dab.x + r_fringe).ceil() as i32;
|
let x1 = ((dab.x + r_fringe).ceil() as i32).min(canvas_w as i32) as u32;
|
||||||
let dy1 = (dab.y + r_fringe).ceil() as i32;
|
let y1 = ((dab.y + r_fringe).ceil() as i32).min(canvas_h as i32) as u32;
|
||||||
|
if x1 <= x0 || y1 <= y0 { continue; }
|
||||||
|
|
||||||
let x0 = dx0.max(0) as u32;
|
let bbox_w = x1 - x0;
|
||||||
let y0 = dy0.max(0) as u32;
|
let bbox_h = y1 - y0;
|
||||||
let x1 = (dx1.min(canvas_w as i32 - 1)).max(0) as u32;
|
|
||||||
let y1 = (dy1.min(canvas_h as i32 - 1)).max(0) as u32;
|
|
||||||
if x1 < x0 || y1 < y0 { continue; }
|
|
||||||
|
|
||||||
let bbox_w = x1 - x0 + 1;
|
|
||||||
let bbox_h = y1 - y0 + 1;
|
|
||||||
|
|
||||||
let canvas = self.canvases.get_mut(&keyframe_id).unwrap();
|
let canvas = self.canvases.get_mut(&keyframe_id).unwrap();
|
||||||
|
|
||||||
// Pre-fill dst from src so pixels outside this dab's bbox are preserved.
|
|
||||||
let mut copy_enc = device.create_command_encoder(
|
let mut copy_enc = device.create_command_encoder(
|
||||||
&wgpu::CommandEncoderDescriptor { label: Some("canvas_copy_encoder") },
|
&wgpu::CommandEncoderDescriptor { label: Some("canvas_copy_encoder") },
|
||||||
);
|
);
|
||||||
copy_enc.copy_texture_to_texture(
|
copy_enc.copy_texture_to_texture(
|
||||||
wgpu::TexelCopyTextureInfo {
|
wgpu::TexelCopyTextureInfo {
|
||||||
texture: canvas.src(),
|
texture: canvas.src(),
|
||||||
mip_level: 0,
|
mip_level: 0,
|
||||||
origin: wgpu::Origin3d::ZERO,
|
origin: wgpu::Origin3d { x: x0, y: y0, z: 0 },
|
||||||
aspect: wgpu::TextureAspect::All,
|
aspect: wgpu::TextureAspect::All,
|
||||||
},
|
},
|
||||||
wgpu::TexelCopyTextureInfo {
|
wgpu::TexelCopyTextureInfo {
|
||||||
texture: canvas.dst(),
|
texture: canvas.dst(),
|
||||||
mip_level: 0,
|
mip_level: 0,
|
||||||
origin: wgpu::Origin3d::ZERO,
|
origin: wgpu::Origin3d { x: x0, y: y0, z: 0 },
|
||||||
aspect: wgpu::TextureAspect::All,
|
aspect: wgpu::TextureAspect::All,
|
||||||
},
|
},
|
||||||
full_extent,
|
wgpu::Extent3d { width: bbox_w, height: bbox_h, depth_or_array_layers: 1 },
|
||||||
);
|
);
|
||||||
queue.submit(Some(copy_enc.finish()));
|
queue.submit(Some(copy_enc.finish()));
|
||||||
|
|
||||||
// Upload single-dab buffer and params
|
|
||||||
let dab_bytes = bytemuck::bytes_of(dab);
|
let dab_bytes = bytemuck::bytes_of(dab);
|
||||||
let dab_buf = device.create_buffer(&wgpu::BufferDescriptor {
|
let dab_buf = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("dab_storage_buf"),
|
label: Some("dab_storage_buf"),
|
||||||
|
|
@ -354,8 +337,8 @@ impl GpuBrushEngine {
|
||||||
queue.write_buffer(&dab_buf, 0, dab_bytes);
|
queue.write_buffer(&dab_buf, 0, dab_bytes);
|
||||||
|
|
||||||
let params = DabParams {
|
let params = DabParams {
|
||||||
bbox_x0: x0 as i32,
|
bbox_x0: x0 as i32,
|
||||||
bbox_y0: y0 as i32,
|
bbox_y0: y0 as i32,
|
||||||
bbox_w,
|
bbox_w,
|
||||||
bbox_h,
|
bbox_h,
|
||||||
num_dabs: 1,
|
num_dabs: 1,
|
||||||
|
|
@ -375,22 +358,10 @@ impl GpuBrushEngine {
|
||||||
label: Some("brush_dab_bg"),
|
label: Some("brush_dab_bg"),
|
||||||
layout: &self.compute_bg_layout,
|
layout: &self.compute_bg_layout,
|
||||||
entries: &[
|
entries: &[
|
||||||
wgpu::BindGroupEntry {
|
wgpu::BindGroupEntry { binding: 0, resource: dab_buf.as_entire_binding() },
|
||||||
binding: 0,
|
wgpu::BindGroupEntry { binding: 1, resource: params_buf.as_entire_binding() },
|
||||||
resource: dab_buf.as_entire_binding(),
|
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(canvas.src_view()) },
|
||||||
},
|
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(canvas.dst_view()) },
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 1,
|
|
||||||
resource: params_buf.as_entire_binding(),
|
|
||||||
},
|
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 2,
|
|
||||||
resource: wgpu::BindingResource::TextureView(canvas.src_view()),
|
|
||||||
},
|
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 3,
|
|
||||||
resource: wgpu::BindingResource::TextureView(canvas.dst_view()),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -399,18 +370,13 @@ impl GpuBrushEngine {
|
||||||
);
|
);
|
||||||
{
|
{
|
||||||
let mut pass = compute_enc.begin_compute_pass(
|
let mut pass = compute_enc.begin_compute_pass(
|
||||||
&wgpu::ComputePassDescriptor {
|
&wgpu::ComputePassDescriptor { label: Some("brush_dab_pass"), timestamp_writes: None },
|
||||||
label: Some("brush_dab_pass"),
|
|
||||||
timestamp_writes: None,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
pass.set_pipeline(&self.compute_pipeline);
|
pass.set_pipeline(&self.compute_pipeline);
|
||||||
pass.set_bind_group(0, &bg, &[]);
|
pass.set_bind_group(0, &bg, &[]);
|
||||||
pass.dispatch_workgroups(bbox_w.div_ceil(8), bbox_h.div_ceil(8), 1);
|
pass.dispatch_workgroups(bbox_w.div_ceil(8), bbox_h.div_ceil(8), 1);
|
||||||
}
|
}
|
||||||
queue.submit(Some(compute_enc.finish()));
|
queue.submit(Some(compute_enc.finish()));
|
||||||
|
|
||||||
// Swap: the just-written dst becomes src for the next dab.
|
|
||||||
canvas.swap();
|
canvas.swap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue