use crate::gpu::atlas::LayerTexture;
use crate::layer::LayerId;
pub use crate::transform::{
affine_inverse, affine_multiply, affine_rotate, affine_scale, affine_transform,
affine_translate, Affine2D, IDENTITY,
};
pub enum ClearShape {
Rect(crate::coord::CanvasRect),
Selection { mask_bind_group: wgpu::BindGroup },
}
pub enum FloatingMode {
Paste { created_layer_id: Option<LayerId> },
Transform {
clear_shape: ClearShape,
},
}
pub struct FloatingContent {
pub source_origin: (i32, i32),
pub source_width: u32,
pub source_height: u32,
pub matrix: Affine2D,
pub target_layer: LayerId,
pub mode: FloatingMode,
}
impl FloatingContent {
pub fn transformed_bounds(&self) -> (i32, i32, i32, i32) {
let (ox, oy) = self.source_origin;
let w = self.source_width as f32;
let h = self.source_height as f32;
let corners = [
affine_transform(&self.matrix, 0.0, 0.0),
affine_transform(&self.matrix, w, 0.0),
affine_transform(&self.matrix, 0.0, h),
affine_transform(&self.matrix, w, h),
];
let mut min_x = f32::MAX;
let mut min_y = f32::MAX;
let mut max_x = f32::MIN;
let mut max_y = f32::MIN;
for (cx, cy) in &corners {
min_x = min_x.min(*cx);
min_y = min_y.min(*cy);
max_x = max_x.max(*cx);
max_y = max_y.max(*cy);
}
(
(min_x + ox as f32).floor() as i32,
(min_y + oy as f32).floor() as i32,
(max_x + ox as f32).ceil() as i32,
(max_y + oy as f32).ceil() as i32,
)
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct TransformBlendUniforms {
pub inv_row0: [f32; 4],
pub inv_row1: [f32; 4],
pub source_origin: [f32; 2],
pub source_size: [f32; 2],
pub target_offset: [f32; 2],
pub target_size: [f32; 2],
pub canvas_size: [f32; 2],
pub opacity: f32,
pub is_r8: f32,
}
pub struct TransformState {
pub source_texture: wgpu::Texture,
pub source_view: wgpu::TextureView,
pub uniform_buf: wgpu::Buffer,
pub commit_bind_group: wgpu::BindGroup,
pub target_layer: LayerId,
pub target_format: wgpu::TextureFormat,
pub preview_texture: wgpu::Texture,
pub preview_view: wgpu::TextureView,
pub preview_mask_bind_group: Option<wgpu::BindGroup>,
pub preview_blend_uniform_buf: wgpu::Buffer,
}
pub struct TransformPass {
commit_rgba_pipeline: wgpu::RenderPipeline,
commit_r8_pipeline: wgpu::RenderPipeline,
commit_bind_group_layout: wgpu::BindGroupLayout,
single_tex_bgl: wgpu::BindGroupLayout,
premultiply_pipeline: wgpu::RenderPipeline,
pub active: Option<TransformState>,
}
impl TransformPass {
pub fn new(device: &wgpu::Device) -> Self {
let commit_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("transform-commit-bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let single_tex_bgl = super::straight_composite::single_texture_bind_group_layout(
device,
"transform-single-tex-bgl",
);
let commit_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("transform-commit-layout"),
bind_group_layouts: &[Some(&commit_bind_group_layout), Some(&single_tex_bgl)],
immediate_size: 0,
});
let commit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("transform-commit-shader"),
source: wgpu::ShaderSource::Wgsl(
concat!(
include_str!("../../shaders/source_over.wgsl"),
"\n",
include_str!("../../shaders/transform_commit.wgsl"),
)
.into(),
),
});
let make_commit_pipeline = |label: &str, format: wgpu::TextureFormat| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&commit_layout),
vertex: wgpu::VertexState {
module: &commit_shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &commit_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
})
};
let commit_rgba_pipeline =
make_commit_pipeline("transform-commit-rgba", wgpu::TextureFormat::Rgba8Unorm);
let commit_r8_pipeline =
make_commit_pipeline("transform-commit-r8", wgpu::TextureFormat::R8Unorm);
let premultiply_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("premultiply-shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/premultiply.wgsl").into()),
});
let premultiply_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("premultiply-layout"),
bind_group_layouts: &[Some(&single_tex_bgl)],
immediate_size: 0,
});
let premultiply_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("premultiply-pipeline"),
layout: Some(&premultiply_layout),
vertex: wgpu::VertexState {
module: &premultiply_shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &premultiply_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8Unorm,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
TransformPass {
commit_rgba_pipeline,
commit_r8_pipeline,
commit_bind_group_layout,
single_tex_bgl,
premultiply_pipeline,
active: None,
}
}
#[allow(clippy::too_many_arguments)]
pub fn set_floating_content(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
sampler: &wgpu::Sampler,
rgba_data: &[u8],
source_width: u32,
source_height: u32,
target_layer: LayerId,
target_format: wgpu::TextureFormat,
preview_texture: wgpu::Texture,
preview_view: wgpu::TextureView,
preview_mask_bind_group: Option<wgpu::BindGroup>,
preview_blend_uniform_buf: wgpu::Buffer,
) {
let source_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("transform-source"),
size: wgpu::Extent3d {
width: source_width.max(1),
height: source_height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let temp_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("transform-source-staging"),
size: wgpu::Extent3d {
width: source_width.max(1),
height: source_height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &temp_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
rgba_data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(source_width * 4),
rows_per_image: None,
},
wgpu::Extent3d {
width: source_width.max(1),
height: source_height.max(1),
depth_or_array_layers: 1,
},
);
let source_view = source_texture.create_view(&wgpu::TextureViewDescriptor::default());
{
let temp_view = temp_texture.create_view(&wgpu::TextureViewDescriptor::default());
let premul_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("transform-source-premul-bg"),
layout: &self.single_tex_bgl,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&temp_view),
}],
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("transform-source-premul"),
});
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("transform-source-premul-pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &source_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
..Default::default()
});
rpass.set_pipeline(&self.premultiply_pipeline);
rpass.set_bind_group(0, &premul_bg, &[]);
rpass.draw(0..3, 0..1);
}
queue.submit(std::iter::once(encoder.finish()));
}
let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("transform-uniforms"),
size: std::mem::size_of::<TransformBlendUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let commit_bind_group =
self.make_commit_bind_group(device, &source_view, sampler, &uniform_buf);
self.active = Some(TransformState {
source_texture,
source_view,
uniform_buf,
commit_bind_group,
target_layer,
target_format,
preview_texture,
preview_view,
preview_mask_bind_group,
preview_blend_uniform_buf,
});
}
#[allow(clippy::too_many_arguments)]
pub fn set_floating_content_from_gpu(
&mut self,
device: &wgpu::Device,
_queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
sampler: &wgpu::Sampler,
layer: &LayerTexture,
source_origin: (i32, i32),
source_width: u32,
source_height: u32,
target_layer: LayerId,
target_format: wgpu::TextureFormat,
preview_texture: wgpu::Texture,
preview_view: wgpu::TextureView,
preview_mask_bind_group: Option<wgpu::BindGroup>,
preview_blend_uniform_buf: wgpu::Buffer,
) {
let layer_texture = layer.texture();
let layer_canvas = layer.canvas_extent();
let layer_offset = (layer_canvas.x0(), layer_canvas.y0());
let layer_dims = (layer_canvas.width, layer_canvas.height);
let is_r8 = target_format == wgpu::TextureFormat::R8Unorm;
let source_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("transform-source-gpu"),
size: wgpu::Extent3d {
width: source_width.max(1),
height: source_height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: target_format,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let local_src_x_signed = source_origin.0 - layer_offset.0;
let local_src_y_signed = source_origin.1 - layer_offset.1;
let src_x = local_src_x_signed.max(0) as u32;
let src_y = local_src_y_signed.max(0) as u32;
let copy_w = source_width.min(layer_dims.0.saturating_sub(src_x));
let copy_h = source_height.min(layer_dims.1.saturating_sub(src_y));
let dst_x = (-local_src_x_signed).max(0) as u32;
let dst_y = (-local_src_y_signed).max(0) as u32;
let copy_src = wgpu::TexelCopyTextureInfo {
texture: layer_texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: src_x,
y: src_y,
z: 0,
},
aspect: wgpu::TextureAspect::All,
};
let copy_size = wgpu::Extent3d {
width: copy_w.min(source_width.saturating_sub(dst_x)),
height: copy_h.min(source_height.saturating_sub(dst_y)),
depth_or_array_layers: 1,
};
if !is_r8 && copy_size.width > 0 && copy_size.height > 0 {
let temp_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("premultiply-temp"),
size: wgpu::Extent3d {
width: source_width.max(1),
height: source_height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
encoder.copy_texture_to_texture(
copy_src,
wgpu::TexelCopyTextureInfo {
texture: &temp_texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: dst_x,
y: dst_y,
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
copy_size,
);
let temp_view = temp_texture.create_view(&wgpu::TextureViewDescriptor::default());
let premul_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("premultiply-bg"),
layout: &self.single_tex_bgl,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&temp_view),
}],
});
let premul_target_view =
source_texture.create_view(&wgpu::TextureViewDescriptor::default());
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("premultiply"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &premul_target_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
..Default::default()
});
rpass.set_pipeline(&self.premultiply_pipeline);
rpass.set_bind_group(0, &premul_bg, &[]);
rpass.draw(0..3, 0..1);
}
} else if copy_size.width > 0 && copy_size.height > 0 {
encoder.copy_texture_to_texture(
copy_src,
wgpu::TexelCopyTextureInfo {
texture: &source_texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: dst_x,
y: dst_y,
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
copy_size,
);
}
let source_view = source_texture.create_view(&wgpu::TextureViewDescriptor::default());
let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("transform-uniforms"),
size: std::mem::size_of::<TransformBlendUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let commit_bind_group =
self.make_commit_bind_group(device, &source_view, sampler, &uniform_buf);
self.active = Some(TransformState {
source_texture,
source_view,
uniform_buf,
commit_bind_group,
target_layer,
target_format,
preview_texture,
preview_view,
preview_mask_bind_group,
preview_blend_uniform_buf,
});
}
fn make_commit_bind_group(
&self,
device: &wgpu::Device,
source_view: &wgpu::TextureView,
sampler: &wgpu::Sampler,
uniform_buf: &wgpu::Buffer,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("transform-commit-bg"),
layout: &self.commit_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(source_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: uniform_buf.as_entire_binding(),
},
],
})
}
#[allow(clippy::too_many_arguments)]
pub fn update_uniforms(
&self,
queue: &wgpu::Queue,
matrix: &Affine2D,
source_origin: (i32, i32),
source_width: u32,
source_height: u32,
target_offset: (i32, i32),
target_width: u32,
target_height: u32,
canvas_width: u32,
canvas_height: u32,
) {
let Some(state) = self.active.as_ref() else {
return;
};
let inv = affine_inverse(matrix).unwrap_or(IDENTITY);
let is_r8 = if state.target_format == wgpu::TextureFormat::R8Unorm {
1.0
} else {
0.0
};
let uniforms = TransformBlendUniforms {
inv_row0: [inv[0], inv[1], inv[2], 0.0],
inv_row1: [inv[3], inv[4], inv[5], 0.0],
source_origin: [source_origin.0 as f32, source_origin.1 as f32],
source_size: [source_width as f32, source_height as f32],
target_offset: [target_offset.0 as f32, target_offset.1 as f32],
target_size: [target_width as f32, target_height as f32],
canvas_size: [canvas_width as f32, canvas_height as f32],
opacity: 1.0,
is_r8,
};
queue.write_buffer(&state.uniform_buf, 0, bytemuck::bytes_of(&uniforms));
}
pub fn render_commit(
&self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
target_texture: &wgpu::Texture,
target_view: &wgpu::TextureView,
) {
let Some(state) = self.active.as_ref() else {
return;
};
let dest_bg = super::straight_composite::copy_for_compositing(
device,
encoder,
&self.single_tex_bgl,
target_texture,
state.target_format,
);
let pipeline = match state.target_format {
wgpu::TextureFormat::R8Unorm => &self.commit_r8_pipeline,
_ => &self.commit_rgba_pipeline,
};
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("transform-commit"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
..Default::default()
});
rpass.set_pipeline(pipeline);
rpass.set_bind_group(0, &state.commit_bind_group, &[]);
rpass.set_bind_group(1, &dest_bg, &[]);
rpass.draw(0..3, 0..1);
}
pub fn clear(&mut self) {
self.active = None;
}
pub fn targets_layer(&self, layer_id: LayerId) -> bool {
self.active
.as_ref()
.is_some_and(|s| s.target_layer == layer_id)
}
}