Compare commits

...

2 Commits

Author SHA1 Message Date
Skyler Lehmkuhl 63a8080e60 improve painting performance 2026-03-04 15:20:39 -05:00
Skyler Lehmkuhl 4e79abdc35 bundle amp models more sensibly 2026-03-04 15:20:20 -05:00
7 changed files with 40 additions and 64 deletions

View File

@ -10,22 +10,22 @@ const BUNDLED_MODELS: &[BundledModel] = &[
BundledModel { BundledModel {
name: "BossSD1", name: "BossSD1",
filename: "BossSD1-WaveNet.nam", filename: "BossSD1-WaveNet.nam",
data: include_bytes!("../../../../../vendor/NeuralAudio/Utils/Models/BossSD1-WaveNet.nam"), data: include_bytes!("../../../../../src/assets/nam_models/BossSD1-WaveNet.nam"),
}, },
BundledModel { BundledModel {
name: "DeluxeReverb", name: "DeluxeReverb",
filename: "DeluxeReverb.nam", filename: "DeluxeReverb.nam",
data: include_bytes!("../../../../../vendor/NeuralAudio/Utils/Models/DeluxeReverb.nam"), data: include_bytes!("../../../../../src/assets/nam_models/DeluxeReverb.nam"),
}, },
BundledModel { BundledModel {
name: "DingwallBass", name: "DingwallBass",
filename: "DingwallBass.nam", filename: "DingwallBass.nam",
data: include_bytes!("../../../../../vendor/NeuralAudio/Utils/Models/DingwallBass.nam"), data: include_bytes!("../../../../../src/assets/nam_models/DingwallBass.nam"),
}, },
BundledModel { BundledModel {
name: "Rhythm", name: "Rhythm",
filename: "Rhythm.nam", filename: "Rhythm.nam",
data: include_bytes!("../../../../../vendor/NeuralAudio/Utils/Models/Rhythm.nam"), data: include_bytes!("../../../../../src/assets/nam_models/Rhythm.nam"),
}, },
]; ];

View File

@ -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 (1raw)^(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;

View File

@ -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();
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long