use std::collections::HashMap;
use std::sync::Arc;
use super::eval::BrushCursorPreviewInfo;
use super::pipeline::BrushPipelines;
use super::scratch::Scratch;
use super::wgsl::{CompiledBrush, IntrinsicUniforms};
use super::wire::ScalarValue;
use crate::gpu::overlay::ToolOverlay;
use crate::gpu::paint_target::GpuPaintTarget;
pub const MAX_PREVIEW_MASK_SIDE: u32 = 512;
pub const MIN_PREVIEW_MASK_SIDE: u32 = 128;
#[derive(Default, Clone, Debug)]
pub struct BrushPerfCounters {
pub dabs_placed: u32,
pub submit_us: u64,
pub submits: u32,
pub dab_flushes: u32,
pub flushed_dabs: u32,
pub dab_union_bbox_area: u64,
pub dabs_per_flush: Vec<u32>,
pub dab_union_bbox_area_per_flush: Vec<u32>,
}
impl BrushPerfCounters {
pub fn record_dab(&mut self) {
self.dabs_placed = self.dabs_placed.saturating_add(1);
}
pub fn record_dab_flush(&mut self, dab_count: u32) {
self.flushed_dabs = self.flushed_dabs.saturating_add(dab_count);
self.dab_flushes = self.dab_flushes.saturating_add(1);
}
pub fn record_dab_flush_workload(&mut self, dab_count: u32, union_w: u32, union_h: u32) {
let area = (union_w as u64).saturating_mul(union_h as u64);
self.dab_union_bbox_area = self.dab_union_bbox_area.saturating_add(area);
self.dabs_per_flush.push(dab_count);
self.dab_union_bbox_area_per_flush
.push(area.min(u32::MAX as u64) as u32);
}
}
impl std::ops::AddAssign for BrushPerfCounters {
fn add_assign(&mut self, mut rhs: Self) {
self.dabs_placed = self.dabs_placed.saturating_add(rhs.dabs_placed);
self.submit_us = self.submit_us.saturating_add(rhs.submit_us);
self.submits = self.submits.saturating_add(rhs.submits);
self.dab_flushes = self.dab_flushes.saturating_add(rhs.dab_flushes);
self.flushed_dabs = self.flushed_dabs.saturating_add(rhs.flushed_dabs);
self.dab_union_bbox_area = self
.dab_union_bbox_area
.saturating_add(rhs.dab_union_bbox_area);
self.dabs_per_flush.append(&mut rhs.dabs_per_flush);
self.dab_union_bbox_area_per_flush
.append(&mut rhs.dab_union_bbox_area_per_flush);
}
}
pub const MAX_DABS_PER_PHASE: u32 = 16384;
pub struct StrokeResources<'a> {
pub scratch: &'a mut Scratch,
pub paint_target: GpuPaintTarget<'a>,
pub pre_stroke_texture: &'a wgpu::Texture,
pub pre_stroke_bind_group: &'a wgpu::BindGroup,
}
pub struct CursorPreviewState<'a> {
pub mask_view: Option<&'a wgpu::TextureView>,
pub mask_size: (u32, u32),
pub mask_overlay: Option<&'a mut ToolOverlay>,
pub info: Option<BrushCursorPreviewInfo>,
}
#[derive(Default)]
pub struct DabBatch {
pub bytes: Vec<u8>,
pub count: u32,
pub bbox: Option<[u32; 4]>,
pub meta_bytes: Vec<u8>,
pub write_canvas_bbox: Option<crate::coord::CanvasRect>,
pub compiled_brush: Option<Arc<CompiledBrush>>,
pub slot_outputs: Option<HashMap<String, ScalarValue>>,
}
impl DabBatch {
pub fn queue_dab(
&mut self,
compiled: &CompiledBrush,
position: [f32; 2],
bbox_radius: f32,
radius: f32,
) {
let record_start = self.bytes.len();
super::wgsl::pack_intrinsic_dab_header(&mut self.bytes, position, bbox_radius, radius);
let outputs = self
.slot_outputs
.as_ref()
.expect("queue_dab requires slot_outputs on DabBatch");
super::wgsl::pack_dab_record(compiled, outputs, &mut self.bytes);
let written = self.bytes.len() - record_start;
if written < compiled.dab_record_size {
self.bytes
.resize(record_start + compiled.dab_record_size, 0);
}
self.count = self.count.saturating_add(1);
debug_assert!(
self.count <= MAX_DABS_PER_PHASE,
"dab queue overflowed MAX_DABS_PER_PHASE ({MAX_DABS_PER_PHASE}); \
bump the constant or flush more often",
);
}
pub fn take(&mut self) -> (Vec<u8>, u32) {
let bytes = std::mem::take(&mut self.bytes);
let count = std::mem::take(&mut self.count);
self.bbox = None;
(bytes, count)
}
pub fn take_meta(&mut self) -> Vec<u8> {
std::mem::take(&mut self.meta_bytes)
}
pub fn clear(&mut self) {
self.bytes.clear();
self.count = 0;
self.bbox = None;
self.meta_bytes.clear();
}
pub fn push_write_bbox(&mut self, bbox: crate::coord::CanvasRect) {
if bbox.is_empty() {
return;
}
self.write_canvas_bbox = Some(match self.write_canvas_bbox {
Some(prev) => prev.union(bbox),
None => bbox,
});
}
}
impl<'a> StrokeResources<'a> {
pub fn reset_per_dab_read_cache(&mut self) {
self.scratch.reset_read_origin_cache();
}
}
pub struct BrushGpuContext<'a> {
pub encoder: wgpu::CommandEncoder,
pub device: &'a wgpu::Device,
pub queue: &'a wgpu::Queue,
pub pipelines: &'a BrushPipelines,
pub selection_bind_group: &'a wgpu::BindGroup,
pub canvas_width: u32,
pub canvas_height: u32,
pub canvas_origin: [i32; 2],
pub blend_mode: u32,
pub view_rotation: f32,
pub perf: BrushPerfCounters,
pub stroke: Option<StrokeResources<'a>>,
pub preview: Option<CursorPreviewState<'a>>,
pub dab_batch: DabBatch,
}
impl<'a> BrushGpuContext<'a> {
pub fn intrinsic_header(
&self,
layer_offset: [i32; 2],
layer_size: [u32; 2],
) -> IntrinsicUniforms {
IntrinsicUniforms {
layer_offset,
layer_size,
canvas_size: [self.canvas_width, self.canvas_height],
canvas_origin: self.canvas_origin,
cursor_preview_centre: [0.0, 0.0],
cursor_preview_size: [0, 0],
view_rotation: self.view_rotation,
_pad: [0, 0, 0],
}
}
pub fn intrinsic_preview_header(
&self,
target_w: u32,
target_h: u32,
centre: [f32; 2],
) -> IntrinsicUniforms {
IntrinsicUniforms {
layer_offset: [0, 0],
layer_size: [target_w, target_h],
canvas_size: [target_w, target_h],
canvas_origin: [0, 0],
cursor_preview_centre: centre,
cursor_preview_size: [target_w, target_h],
view_rotation: self.view_rotation,
_pad: [0, 0, 0],
}
}
pub fn submit_final(mut self) -> BrushPerfCounters {
let t = web_time::Instant::now();
self.queue.submit([self.encoder.finish()]);
let us = t.elapsed().as_micros() as u64;
self.perf.submit_us = self.perf.submit_us.saturating_add(us);
self.perf.submits = self.perf.submits.saturating_add(1);
self.perf
}
pub fn flush_if_needed(&mut self) {
if self.pipelines.rings_nearly_full() {
let t = web_time::Instant::now();
let finished = std::mem::replace(
&mut self.encoder,
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("brush-ring-flush"),
}),
);
self.queue.submit([finished.finish()]);
self.pipelines.reset_uniform_rings();
self.perf.submit_us = self
.perf
.submit_us
.saturating_add(t.elapsed().as_micros() as u64);
self.perf.submits = self.perf.submits.saturating_add(1);
}
}
pub fn ensure_cursor_preview_mask(
&mut self,
bbox_radius: f32,
) -> Option<(wgpu::TextureView, u32, u32)> {
let preview = self.preview.as_mut()?;
let requested = ((bbox_radius * 2.0).ceil() as u32).max(1);
let side = requested
.next_power_of_two()
.clamp(MIN_PREVIEW_MASK_SIDE, MAX_PREVIEW_MASK_SIDE);
if let Some(overlay) = preview.mask_overlay.as_mut() {
let view = overlay
.ensure_cursor_preview_mask(self.device, side, side)
.clone();
return Some((view, side, side));
}
let view = preview.mask_view?;
Some((view.clone(), preview.mask_size.0, preview.mask_size.1))
}
pub fn prepare_dab_canvas_copy(
&mut self,
position: [f32; 2],
write_half_w: f32,
write_half_h: f32,
read_half_w: f32,
read_half_h: f32,
) -> Option<DabFootprint> {
debug_assert!(
read_half_w >= write_half_w && read_half_h >= write_half_h,
"read region must enclose write region",
);
let stroke = self.stroke.as_mut()?;
let pt_canvas = stroke.paint_target.canvas_extent();
let layer_x0 = pt_canvas.x0() as f32;
let layer_y0 = pt_canvas.y0() as f32;
let layer_x1 = layer_x0 + pt_canvas.width as f32;
let layer_y1 = layer_y0 + pt_canvas.height as f32;
let unclipped_write_x0 = position[0] - write_half_w;
let unclipped_write_y0 = position[1] - write_half_h;
let write_x0 = unclipped_write_x0.max(layer_x0);
let write_y0 = unclipped_write_y0.max(layer_y0);
let write_x1 = (position[0] + write_half_w).min(layer_x1);
let write_y1 = (position[1] + write_half_h).min(layer_y1);
let quad_w = write_x1 - write_x0;
let quad_h = write_y1 - write_y0;
if quad_w <= 0.0 || quad_h <= 0.0 {
return None;
}
let copy = pt_canvas.clamp_f32(
position[0] - read_half_w,
position[1] - read_half_h,
position[0] + read_half_w,
position[1] + read_half_h,
)?;
let copy_canvas_x = copy.x0();
let copy_canvas_y = copy.y0();
let copy_w = copy.width;
let copy_h = copy.height;
let write_bbox = pt_canvas
.clamp_f32(
unclipped_write_x0,
unclipped_write_y0,
position[0] + write_half_w,
position[1] + write_half_h,
)
.expect("write region has positive area (quad_w/h > 0), so its snapped bbox overlaps the extent");
self.dab_batch.push_write_bbox(write_bbox);
let stroke = self.stroke.as_mut().expect("stroke just observed Some");
let copy_local_x = (copy_canvas_x - pt_canvas.x0()) as u32;
let copy_local_y = (copy_canvas_y - pt_canvas.y0()) as u32;
stroke.scratch.sync_read_mirror(
self.device,
&mut self.encoder,
copy_local_x,
copy_local_y,
copy_w,
copy_h,
);
Some(DabFootprint {
layer_offset: [pt_canvas.x0(), pt_canvas.y0()],
layer_size: [pt_canvas.width, pt_canvas.height],
unclipped_origin: [unclipped_write_x0, unclipped_write_y0],
origin: [write_x0, write_y0],
size: [quad_w, quad_h],
copy_canvas_origin: [copy_canvas_x, copy_canvas_y],
copy_local_origin: [copy_local_x, copy_local_y],
copy_size: [copy_w, copy_h],
})
}
pub fn sync_scratch_read_mirror(
&mut self,
origin_x: u32,
origin_y: u32,
width: u32,
height: u32,
) {
if let Some(stroke) = self.stroke.as_mut() {
stroke.scratch.sync_read_mirror(
self.device,
&mut self.encoder,
origin_x,
origin_y,
width,
height,
);
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct DabFootprint {
pub layer_offset: [i32; 2],
pub layer_size: [u32; 2],
pub unclipped_origin: [f32; 2],
pub origin: [f32; 2],
pub size: [f32; 2],
pub copy_canvas_origin: [i32; 2],
pub copy_local_origin: [u32; 2],
pub copy_size: [u32; 2],
}