use crate::coord::CanvasRect;
use crate::gpu::atlas::CanvasFrame;
use crate::gpu::readback::ReadbackRequest;
use crate::layer::LayerId;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Copy, Clone, Debug)]
pub struct Snapshot {
pub saved: CanvasRect,
pub format: wgpu::TextureFormat,
}
const COPY_ROW_ALIGNMENT: u32 = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
pub enum EntryPixels {
Pending {
readback: wgpu::Buffer,
staging: wgpu::Buffer,
},
Ready(Vec<u8>),
}
pub struct UndoRegionEntry {
pub layer_id: LayerId,
pub canvas_rect: CanvasRect,
pub format: wgpu::TextureFormat,
pub padded_row_bytes: u32,
pub unpadded_row_bytes: u32,
pub byte_size: u64,
pub pixels: Rc<RefCell<EntryPixels>>,
}
pub struct RegionScratch {
scratch_rgba: wgpu::Texture,
scratch_r8: wgpu::Texture,
scratch_width: u32,
scratch_height: u32,
}
impl RegionScratch {
pub fn new(device: &wgpu::Device, canvas_width: u32, canvas_height: u32) -> Self {
let scratch_rgba = Self::create_scratch(
device,
canvas_width,
canvas_height,
wgpu::TextureFormat::Rgba8Unorm,
"scratch-rgba",
);
let scratch_r8 = Self::create_scratch(
device,
canvas_width,
canvas_height,
wgpu::TextureFormat::R8Unorm,
"scratch-r8",
);
RegionScratch {
scratch_rgba,
scratch_r8,
scratch_width: canvas_width,
scratch_height: canvas_height,
}
}
pub fn ensure_scratch_capacity(&mut self, device: &wgpu::Device, w: u32, h: u32) {
if w <= self.scratch_width && h <= self.scratch_height {
return;
}
let new_w = w.max(self.scratch_width);
let new_h = h.max(self.scratch_height);
self.realloc_scratch_pair(device, new_w, new_h, None);
}
pub fn grow_scratch_preserving(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
new_w: u32,
new_h: u32,
dst_offset_x: u32,
dst_offset_y: u32,
) {
if new_w <= self.scratch_width
&& new_h <= self.scratch_height
&& dst_offset_x == 0
&& dst_offset_y == 0
{
return;
}
let target_w = new_w.max(self.scratch_width);
let target_h = new_h.max(self.scratch_height);
self.realloc_scratch_pair(
device,
target_w,
target_h,
Some((encoder, dst_offset_x, dst_offset_y)),
);
}
fn realloc_scratch_pair(
&mut self,
device: &wgpu::Device,
new_w: u32,
new_h: u32,
copy: Option<(&mut wgpu::CommandEncoder, u32, u32)>,
) {
let pairs = [
(
&mut self.scratch_rgba,
wgpu::TextureFormat::Rgba8Unorm,
"scratch-rgba",
),
(
&mut self.scratch_r8,
wgpu::TextureFormat::R8Unorm,
"scratch-r8",
),
];
let copy_w = self.scratch_width;
let copy_h = self.scratch_height;
let (encoder_opt, dst_offset_x, dst_offset_y) = match copy {
Some((enc, x, y)) => (Some(enc), x, y),
None => (None, 0, 0),
};
let do_copy = encoder_opt.is_some() && copy_w > 0 && copy_h > 0;
let mut encoder_opt = encoder_opt;
for (field, format, label) in pairs {
let new_tex = Self::create_scratch(device, new_w, new_h, format, label);
if do_copy {
let encoder = encoder_opt
.as_deref_mut()
.expect("do_copy implies encoder present");
let new_view = new_tex.create_view(&wgpu::TextureViewDescriptor::default());
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("scratch-grow-clear"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &new_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(Self::scratch_default_clear(format)),
store: wgpu::StoreOp::Store,
},
})],
..Default::default()
});
encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo {
texture: field,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyTextureInfo {
texture: &new_tex,
mip_level: 0,
origin: wgpu::Origin3d {
x: dst_offset_x,
y: dst_offset_y,
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
wgpu::Extent3d {
width: copy_w,
height: copy_h,
depth_or_array_layers: 1,
},
);
}
*field = new_tex;
}
self.scratch_width = new_w;
self.scratch_height = new_h;
}
pub fn save_region(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
source: &CanvasFrame<'_>,
format: wgpu::TextureFormat,
canvas_rect: CanvasRect,
) -> Snapshot {
let layer_rect = source
.canvas_to_layer_rect(canvas_rect)
.expect("save_region rect must overlap the source's canvas extent");
self.ensure_scratch_capacity(device, layer_rect.x1(), layer_rect.y1());
let scratch = self.scratch_for(format);
encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo {
texture: source.texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: layer_rect.x0(),
y: layer_rect.y0(),
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyTextureInfo {
texture: scratch,
mip_level: 0,
origin: wgpu::Origin3d {
x: layer_rect.x0(),
y: layer_rect.y0(),
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
wgpu::Extent3d {
width: layer_rect.width,
height: layer_rect.height,
depth_or_array_layers: 1,
},
);
Snapshot {
saved: canvas_rect,
format,
}
}
pub fn commit_region(
&self,
encoder: &mut wgpu::CommandEncoder,
device: &wgpu::Device,
layer_id: LayerId,
source: &CanvasFrame<'_>,
snapshot: &Snapshot,
canvas_rect: CanvasRect,
) -> (UndoRegionEntry, ReadbackRequest) {
debug_assert!(
snapshot.saved.contains(canvas_rect),
"commit_region rect {:?} not contained in snapshot.saved {:?}",
canvas_rect,
snapshot.saved,
);
let layer_rect = source
.canvas_to_layer_rect(canvas_rect)
.expect("commit_region rect must overlap the source's canvas extent");
let bpp = snapshot.format.block_copy_size(None).unwrap_or(1);
let unpadded_row_bytes = layer_rect.width * bpp;
let padded_row_bytes = padded_row(layer_rect.width, bpp);
let byte_size = padded_row_bytes as u64 * layer_rect.height as u64;
let (readback, staging) = allocate_pending_buffers(device, byte_size);
let scratch = self.scratch_for(snapshot.format);
let texel_layout = wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_row_bytes),
rows_per_image: Some(layer_rect.height),
};
let extent = wgpu::Extent3d {
width: layer_rect.width,
height: layer_rect.height,
depth_or_array_layers: 1,
};
let origin = wgpu::Origin3d {
x: layer_rect.x0(),
y: layer_rect.y0(),
z: 0,
};
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: scratch,
mip_level: 0,
origin,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: texel_layout,
},
extent,
);
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: scratch,
mip_level: 0,
origin,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &staging,
layout: texel_layout,
},
extent,
);
let pixels = Rc::new(RefCell::new(EntryPixels::Pending {
readback: readback.clone(),
staging,
}));
let request = ReadbackRequest::from_buffer(
readback,
layer_rect.height,
padded_row_bytes,
unpadded_row_bytes,
);
let entry = UndoRegionEntry {
layer_id,
canvas_rect,
format: snapshot.format,
padded_row_bytes,
unpadded_row_bytes,
byte_size,
pixels,
};
(entry, request)
}
pub fn restore_region(
&self,
encoder: &mut wgpu::CommandEncoder,
device: &wgpu::Device,
entry: &UndoRegionEntry,
target: &CanvasFrame<'_>,
) -> (UndoRegionEntry, ReadbackRequest) {
let (forward, request) = self.capture_region(
encoder,
device,
entry.layer_id,
entry.format,
target,
entry.canvas_rect,
);
self.upload_region(encoder, device, entry, target);
(forward, request)
}
pub fn capture_region(
&self,
encoder: &mut wgpu::CommandEncoder,
device: &wgpu::Device,
layer_id: LayerId,
format: wgpu::TextureFormat,
target: &CanvasFrame<'_>,
capture_rect: CanvasRect,
) -> (UndoRegionEntry, ReadbackRequest) {
let layer_rect = target
.canvas_to_layer_rect(capture_rect)
.expect("capture_region rect must overlap the target's canvas extent");
let bpp = format.block_copy_size(None).unwrap_or(1);
let unpadded_row_bytes = layer_rect.width * bpp;
let padded_row_bytes = padded_row(layer_rect.width, bpp);
let byte_size = padded_row_bytes as u64 * layer_rect.height as u64;
let origin = wgpu::Origin3d {
x: layer_rect.x0(),
y: layer_rect.y0(),
z: 0,
};
let extent = wgpu::Extent3d {
width: layer_rect.width,
height: layer_rect.height,
depth_or_array_layers: 1,
};
let texel_layout = wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_row_bytes),
rows_per_image: Some(layer_rect.height),
};
let (readback, staging) = allocate_pending_buffers(device, byte_size);
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: target.texture,
mip_level: 0,
origin,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: texel_layout,
},
extent,
);
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: target.texture,
mip_level: 0,
origin,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &staging,
layout: texel_layout,
},
extent,
);
let pixels = Rc::new(RefCell::new(EntryPixels::Pending {
readback: readback.clone(),
staging,
}));
let request = ReadbackRequest::from_buffer(
readback,
layer_rect.height,
padded_row_bytes,
unpadded_row_bytes,
);
let forward = UndoRegionEntry {
layer_id,
canvas_rect: capture_rect,
format,
padded_row_bytes,
unpadded_row_bytes,
byte_size,
pixels,
};
(forward, request)
}
pub fn upload_region(
&self,
encoder: &mut wgpu::CommandEncoder,
device: &wgpu::Device,
entry: &UndoRegionEntry,
target: &CanvasFrame<'_>,
) {
let layer_rect = target
.canvas_to_layer_rect(entry.canvas_rect)
.expect("upload_region entry must overlap the target's canvas extent");
let origin = wgpu::Origin3d {
x: layer_rect.x0(),
y: layer_rect.y0(),
z: 0,
};
let extent = wgpu::Extent3d {
width: layer_rect.width,
height: layer_rect.height,
depth_or_array_layers: 1,
};
let padded_row_bytes = entry.padded_row_bytes;
let unpadded_row_bytes = entry.unpadded_row_bytes;
let byte_size = entry.byte_size;
let height = layer_rect.height;
let texel_layout = wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_row_bytes),
rows_per_image: Some(height),
};
let pixels_borrow = entry.pixels.borrow();
match &*pixels_borrow {
EntryPixels::Pending { staging, .. } => {
encoder.copy_buffer_to_texture(
wgpu::TexelCopyBufferInfo {
buffer: staging,
layout: texel_layout,
},
wgpu::TexelCopyTextureInfo {
texture: target.texture,
mip_level: 0,
origin,
aspect: wgpu::TextureAspect::All,
},
extent,
);
}
EntryPixels::Ready(vec) => {
let upload = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("undo-region-upload"),
size: byte_size,
usage: wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: true,
});
{
let mut mapped = upload.slice(..).get_mapped_range_mut();
let unpadded_row = unpadded_row_bytes as usize;
let padded_row = padded_row_bytes as usize;
if unpadded_row == padded_row {
let n = vec.len().min(mapped.len());
mapped.slice(..n).copy_from_slice(&vec[..n]);
} else {
for row in 0..height as usize {
let src_off = row * unpadded_row;
let dst_off = row * padded_row;
mapped
.slice(dst_off..dst_off + unpadded_row)
.copy_from_slice(&vec[src_off..src_off + unpadded_row]);
}
}
}
upload.unmap();
encoder.copy_buffer_to_texture(
wgpu::TexelCopyBufferInfo {
buffer: &upload,
layout: texel_layout,
},
wgpu::TexelCopyTextureInfo {
texture: target.texture,
mip_level: 0,
origin,
aspect: wgpu::TextureAspect::All,
},
extent,
);
}
}
}
pub fn restore_from_scratch(
&self,
encoder: &mut wgpu::CommandEncoder,
snapshot: &Snapshot,
target: &CanvasFrame<'_>,
canvas_rect: CanvasRect,
) {
debug_assert!(
snapshot.saved.contains(canvas_rect),
"restore_from_scratch rect {:?} not contained in snapshot.saved {:?}",
canvas_rect,
snapshot.saved,
);
let layer_rect = target
.canvas_to_layer_rect(canvas_rect)
.expect("restore_from_scratch rect must overlap the target's canvas extent");
let origin = wgpu::Origin3d {
x: layer_rect.x0(),
y: layer_rect.y0(),
z: 0,
};
encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo {
texture: self.scratch_for(snapshot.format),
mip_level: 0,
origin,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyTextureInfo {
texture: target.texture,
mip_level: 0,
origin,
aspect: wgpu::TextureAspect::All,
},
wgpu::Extent3d {
width: layer_rect.width,
height: layer_rect.height,
depth_or_array_layers: 1,
},
);
}
pub fn resize_scratch(&mut self, device: &wgpu::Device, width: u32, height: u32) {
if width == self.scratch_width && height == self.scratch_height {
return;
}
self.scratch_rgba = Self::create_scratch(
device,
width,
height,
wgpu::TextureFormat::Rgba8Unorm,
"scratch-rgba",
);
self.scratch_r8 = Self::create_scratch(
device,
width,
height,
wgpu::TextureFormat::R8Unorm,
"scratch-r8",
);
self.scratch_width = width;
self.scratch_height = height;
}
pub fn scratch_view(&self, format: wgpu::TextureFormat) -> wgpu::TextureView {
self.scratch_for(format)
.create_view(&wgpu::TextureViewDescriptor::default())
}
pub fn scratch_dimensions(&self) -> (u32, u32) {
(self.scratch_width, self.scratch_height)
}
fn scratch_for(&self, format: wgpu::TextureFormat) -> &wgpu::Texture {
match format {
wgpu::TextureFormat::R8Unorm => &self.scratch_r8,
_ => &self.scratch_rgba,
}
}
fn create_scratch(
device: &wgpu::Device,
width: u32,
height: u32,
format: wgpu::TextureFormat,
label: &str,
) -> wgpu::Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
})
}
fn scratch_default_clear(format: wgpu::TextureFormat) -> wgpu::Color {
match format {
wgpu::TextureFormat::R8Unorm => wgpu::Color::WHITE,
_ => wgpu::Color::TRANSPARENT,
}
}
}
fn padded_row(width: u32, bytes_per_pixel: u32) -> u32 {
let unpadded = width * bytes_per_pixel;
unpadded.div_ceil(COPY_ROW_ALIGNMENT) * COPY_ROW_ALIGNMENT
}
fn allocate_pending_buffers(device: &wgpu::Device, byte_size: u64) -> (wgpu::Buffer, wgpu::Buffer) {
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("undo-region-readback"),
size: byte_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("undo-region-staging"),
size: byte_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
(readback, staging)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn padded_row_alignment() {
assert_eq!(padded_row(128, 4), 512);
assert_eq!(padded_row(100, 4), 512);
assert_eq!(padded_row(64, 1), 256);
assert_eq!(padded_row(256, 1), 256);
assert_eq!(padded_row(1, 4), 256);
}
}