forked from 1b-agent/opencadstudio-upstream
435 lines
15 KiB
Rust
435 lines
15 KiB
Rust
// Image GPU buffers — renders raster images as textured quads.
|
|
//
|
|
// Group 1 bindings per image:
|
|
// binding 0 — texture_2d<f32> (RGBA image texture)
|
|
// binding 1 — sampler (bilinear filtering)
|
|
// binding 2 — ImageParams (opacity uniform, 16 bytes)
|
|
|
|
use crate::scene::model::image_model::ImageModel;
|
|
use iced::wgpu;
|
|
use iced::wgpu::util::DeviceExt;
|
|
use std::sync::Arc;
|
|
|
|
// ── Vertex ────────────────────────────────────────────────────────────────
|
|
|
|
#[repr(C)]
|
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
|
pub struct ImageVertex {
|
|
pub pos: [f32; 3],
|
|
pub uv: [f32; 2],
|
|
pub pos_low: [f32; 3],
|
|
}
|
|
|
|
impl ImageVertex {
|
|
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
|
const ATTRS: &[wgpu::VertexAttribute] = &[
|
|
wgpu::VertexAttribute {
|
|
offset: std::mem::offset_of!(ImageVertex, pos) as u64,
|
|
shader_location: 0,
|
|
format: wgpu::VertexFormat::Float32x3,
|
|
},
|
|
wgpu::VertexAttribute {
|
|
offset: std::mem::offset_of!(ImageVertex, uv) as u64,
|
|
shader_location: 1,
|
|
format: wgpu::VertexFormat::Float32x2,
|
|
},
|
|
wgpu::VertexAttribute {
|
|
offset: std::mem::offset_of!(ImageVertex, pos_low) as u64,
|
|
shader_location: 2,
|
|
format: wgpu::VertexFormat::Float32x3,
|
|
},
|
|
];
|
|
wgpu::VertexBufferLayout {
|
|
array_stride: std::mem::size_of::<ImageVertex>() as u64,
|
|
step_mode: wgpu::VertexStepMode::Vertex,
|
|
attributes: ATTRS,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
|
pub struct ImageInstance {
|
|
pub translation: [f32; 3],
|
|
pub translation_low: [f32; 3],
|
|
pub draw_depth: f32,
|
|
pub _pad: [f32; 3],
|
|
}
|
|
|
|
impl ImageInstance {
|
|
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
|
const ATTRS: &[wgpu::VertexAttribute] = &[
|
|
wgpu::VertexAttribute { offset: std::mem::offset_of!(ImageInstance, translation) as u64, shader_location: 3, format: wgpu::VertexFormat::Float32x3 },
|
|
wgpu::VertexAttribute { offset: std::mem::offset_of!(ImageInstance, translation_low) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32x3 },
|
|
wgpu::VertexAttribute { offset: std::mem::offset_of!(ImageInstance, draw_depth) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32 },
|
|
];
|
|
wgpu::VertexBufferLayout {
|
|
array_stride: std::mem::size_of::<Self>() as u64,
|
|
step_mode: wgpu::VertexStepMode::Instance,
|
|
attributes: ATTRS,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Uniform ───────────────────────────────────────────────────────────────
|
|
|
|
#[repr(C)]
|
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
|
struct ImageParams {
|
|
opacity: f32,
|
|
/// Signed draw-order depth (-1,1); applied as a clip-z bias in the shader
|
|
/// so the raster orders against other entity types. 0.0 = neutral.
|
|
draw_depth: f32,
|
|
_pad: [f32; 2],
|
|
} // 16 bytes
|
|
|
|
// ── Per-image GPU handle ──────────────────────────────────────────────────
|
|
|
|
pub struct ImageGpu {
|
|
pub vertex_buffer: wgpu::Buffer,
|
|
pub instance_buffer: Arc<wgpu::Buffer>,
|
|
/// Number of triangle vertices in `vertex_buffer` — 6 for a plain quad, or
|
|
/// more when the raster is clipped to a triangulated polygon.
|
|
pub vertex_count: u32,
|
|
pub instance_count: u32,
|
|
pub bind_group: wgpu::BindGroup,
|
|
_texture: wgpu::Texture,
|
|
_sampler: Arc<wgpu::Sampler>,
|
|
_params_buf: Arc<wgpu::Buffer>,
|
|
}
|
|
|
|
impl ImageGpu {
|
|
pub fn from_models(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
models: &[ImageModel],
|
|
bgl1: &wgpu::BindGroupLayout,
|
|
) -> Vec<Self> {
|
|
let mut slots = rustc_hash::FxHashMap::default();
|
|
let mut groups: Vec<Vec<&ImageModel>> = Vec::new();
|
|
for (index, model) in models.iter().enumerate() {
|
|
let key = model
|
|
.render_instance
|
|
.map(|instance| (true, instance.source_id))
|
|
.unwrap_or((false, index as u64));
|
|
let slot = *slots.entry(key).or_insert_with(|| {
|
|
let slot = groups.len();
|
|
groups.push(Vec::new());
|
|
slot
|
|
});
|
|
groups[slot].push(model);
|
|
}
|
|
let sampler = Arc::new(device.create_sampler(&wgpu::SamplerDescriptor {
|
|
label: Some("image.sampler"),
|
|
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
|
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
|
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
|
mag_filter: wgpu::FilterMode::Linear,
|
|
min_filter: wgpu::FilterMode::Linear,
|
|
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
|
..Default::default()
|
|
}));
|
|
groups
|
|
.into_iter()
|
|
.flat_map(|group| Self::new(device, queue, &group, bgl1, &sampler))
|
|
.collect()
|
|
}
|
|
|
|
fn new(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
models: &[&ImageModel],
|
|
bgl1: &wgpu::BindGroupLayout,
|
|
sampler: &Arc<wgpu::Sampler>,
|
|
) -> Vec<Self> {
|
|
let Some(&model) = models.first() else {
|
|
return Vec::new();
|
|
};
|
|
if model.pixels.is_empty() || model.width == 0 || model.height == 0 {
|
|
return Vec::new();
|
|
}
|
|
|
|
let limit = device.limits().max_texture_dimension_2d.max(1);
|
|
let x_tiles = tile_ranges(model.width, limit);
|
|
let y_tiles = tile_ranges(model.height, limit);
|
|
let base = model
|
|
.render_instance
|
|
.map_or([0.0; 3], |instance| instance.translation);
|
|
let instances: Vec<ImageInstance> = models
|
|
.iter()
|
|
.map(|model| {
|
|
let translation = model
|
|
.render_instance
|
|
.map_or([0.0; 3], |instance| instance.translation);
|
|
let delta = [
|
|
translation[0] - base[0],
|
|
translation[1] - base[1],
|
|
translation[2] - base[2],
|
|
];
|
|
let high = delta.map(|value| value as f32);
|
|
ImageInstance {
|
|
translation: high,
|
|
translation_low: [
|
|
(delta[0] - high[0] as f64) as f32,
|
|
(delta[1] - high[1] as f64) as f32,
|
|
(delta[2] - high[2] as f64) as f32,
|
|
],
|
|
draw_depth: model.draw_depth,
|
|
_pad: [0.0; 3],
|
|
}
|
|
})
|
|
.collect();
|
|
let instance_buffer = Arc::new(device.create_buffer_init(
|
|
&wgpu::util::BufferInitDescriptor {
|
|
label: Some("image.instances"),
|
|
contents: bytemuck::cast_slice(&instances),
|
|
usage: wgpu::BufferUsages::VERTEX,
|
|
},
|
|
));
|
|
let params = ImageParams {
|
|
opacity: model.opacity.clamp(0.0, 1.0),
|
|
draw_depth: 0.0,
|
|
_pad: [0.0; 2],
|
|
};
|
|
let params_buf = Arc::new(device.create_buffer_init(
|
|
&wgpu::util::BufferInitDescriptor {
|
|
label: Some("image.params"),
|
|
contents: bytemuck::bytes_of(¶ms),
|
|
usage: wgpu::BufferUsages::UNIFORM,
|
|
},
|
|
));
|
|
|
|
let mut output = Vec::with_capacity(x_tiles.len() * y_tiles.len());
|
|
for y in &y_tiles {
|
|
for x in &x_tiles {
|
|
let verts = tile_vertices(model, *x, *y);
|
|
if verts.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
let width = x.data_end - x.data_start;
|
|
let height = y.data_end - y.data_start;
|
|
let tex_label = format!(
|
|
"image.texture:{}:{}:{}",
|
|
model.file_path, x.content_start, y.content_start
|
|
);
|
|
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
|
label: Some(&tex_label),
|
|
size: wgpu::Extent3d {
|
|
width,
|
|
height,
|
|
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(
|
|
texture.as_image_copy(),
|
|
&model.pixels[((y.data_start as usize * model.width as usize
|
|
+ x.data_start as usize)
|
|
* 4)..],
|
|
wgpu::TexelCopyBufferLayout {
|
|
offset: 0,
|
|
bytes_per_row: Some(4 * model.width),
|
|
rows_per_image: Some(model.height),
|
|
},
|
|
wgpu::Extent3d {
|
|
width,
|
|
height,
|
|
depth_or_array_layers: 1,
|
|
},
|
|
);
|
|
let tex_view =
|
|
texture.create_view(&wgpu::TextureViewDescriptor::default());
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("image.bind_group1"),
|
|
layout: bgl1,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: wgpu::BindingResource::TextureView(&tex_view),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: wgpu::BindingResource::Sampler(sampler),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 2,
|
|
resource: params_buf.as_entire_binding(),
|
|
},
|
|
],
|
|
});
|
|
let vertex_buffer =
|
|
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("image.vbuf"),
|
|
contents: bytemuck::cast_slice(&verts),
|
|
usage: wgpu::BufferUsages::VERTEX,
|
|
});
|
|
output.push(Self {
|
|
vertex_buffer,
|
|
instance_buffer: Arc::clone(&instance_buffer),
|
|
vertex_count: verts.len() as u32,
|
|
instance_count: instances.len() as u32,
|
|
bind_group,
|
|
_texture: texture,
|
|
_sampler: Arc::clone(sampler),
|
|
_params_buf: Arc::clone(¶ms_buf),
|
|
});
|
|
}
|
|
}
|
|
output
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct TileRange {
|
|
content_start: u32,
|
|
content_end: u32,
|
|
data_start: u32,
|
|
data_end: u32,
|
|
}
|
|
|
|
fn tile_ranges(size: u32, limit: u32) -> Vec<TileRange> {
|
|
if size <= limit {
|
|
return vec![TileRange {
|
|
content_start: 0,
|
|
content_end: size,
|
|
data_start: 0,
|
|
data_end: size,
|
|
}];
|
|
}
|
|
let gutter = u32::from(limit > 2);
|
|
let content_size = (limit - gutter * 2).max(1);
|
|
let mut ranges = Vec::new();
|
|
let mut start = 0;
|
|
while start < size {
|
|
let end = start.saturating_add(content_size).min(size);
|
|
ranges.push(TileRange {
|
|
content_start: start,
|
|
content_end: end,
|
|
data_start: start.saturating_sub(gutter),
|
|
data_end: end.saturating_add(gutter).min(size),
|
|
});
|
|
start = end;
|
|
}
|
|
ranges
|
|
}
|
|
|
|
fn tile_vertices(model: &ImageModel, x: TileRange, y: TileRange) -> Vec<ImageVertex> {
|
|
let source: Vec<ImageVertex> = model
|
|
.verts
|
|
.iter()
|
|
.map(|vertex| ImageVertex {
|
|
pos: vertex.pos,
|
|
uv: vertex.uv,
|
|
pos_low: vertex.pos_low,
|
|
})
|
|
.collect();
|
|
if x.content_start == 0
|
|
&& x.content_end == model.width
|
|
&& y.content_start == 0
|
|
&& y.content_end == model.height
|
|
{
|
|
return source;
|
|
}
|
|
|
|
let bounds = [
|
|
(0, x.content_start as f32 / model.width as f32, true),
|
|
(0, x.content_end as f32 / model.width as f32, false),
|
|
(1, y.content_start as f32 / model.height as f32, true),
|
|
(1, y.content_end as f32 / model.height as f32, false),
|
|
];
|
|
let mut output = Vec::new();
|
|
for triangle in source.chunks_exact(3) {
|
|
let mut polygon = triangle.to_vec();
|
|
for (axis, boundary, keep_greater) in bounds {
|
|
polygon = clip_vertices(&polygon, axis, boundary, keep_greater);
|
|
}
|
|
for index in 1..polygon.len().saturating_sub(1) {
|
|
output.push(polygon[0]);
|
|
output.push(polygon[index]);
|
|
output.push(polygon[index + 1]);
|
|
}
|
|
}
|
|
|
|
let data_width = (x.data_end - x.data_start) as f32;
|
|
let data_height = (y.data_end - y.data_start) as f32;
|
|
for vertex in &mut output {
|
|
vertex.uv = [
|
|
((vertex.uv[0] * model.width as f32 - x.data_start as f32) / data_width)
|
|
.clamp(0.0, 1.0),
|
|
((vertex.uv[1] * model.height as f32 - y.data_start as f32) / data_height)
|
|
.clamp(0.0, 1.0),
|
|
];
|
|
}
|
|
output
|
|
}
|
|
|
|
fn clip_vertices(
|
|
input: &[ImageVertex],
|
|
axis: usize,
|
|
boundary: f32,
|
|
keep_greater: bool,
|
|
) -> Vec<ImageVertex> {
|
|
if input.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
let inside = |vertex: &ImageVertex| {
|
|
if keep_greater {
|
|
vertex.uv[axis] >= boundary
|
|
} else {
|
|
vertex.uv[axis] <= boundary
|
|
}
|
|
};
|
|
let mut output = Vec::new();
|
|
let mut previous = *input.last().unwrap();
|
|
let mut previous_inside = inside(&previous);
|
|
for ¤t in input {
|
|
let current_inside = inside(¤t);
|
|
if current_inside != previous_inside {
|
|
output.push(intersect_vertex(previous, current, axis, boundary));
|
|
}
|
|
if current_inside {
|
|
output.push(current);
|
|
}
|
|
previous = current;
|
|
previous_inside = current_inside;
|
|
}
|
|
output
|
|
}
|
|
|
|
fn intersect_vertex(
|
|
start: ImageVertex,
|
|
end: ImageVertex,
|
|
axis: usize,
|
|
boundary: f32,
|
|
) -> ImageVertex {
|
|
let distance = end.uv[axis] - start.uv[axis];
|
|
let t = if distance.abs() > f32::EPSILON {
|
|
((boundary - start.uv[axis]) / distance).clamp(0.0, 1.0)
|
|
} else {
|
|
0.0
|
|
};
|
|
ImageVertex {
|
|
pos: interpolate3(start.pos, end.pos, t),
|
|
uv: [
|
|
start.uv[0] + (end.uv[0] - start.uv[0]) * t,
|
|
start.uv[1] + (end.uv[1] - start.uv[1]) * t,
|
|
],
|
|
pos_low: interpolate3(start.pos_low, end.pos_low, t),
|
|
}
|
|
}
|
|
|
|
fn interpolate3(start: [f32; 3], end: [f32; 3], t: f32) -> [f32; 3] {
|
|
[
|
|
start[0] + (end[0] - start[0]) * t,
|
|
start[1] + (end[1] - start[1]) * t,
|
|
start[2] + (end[2] - start[2]) * t,
|
|
]
|
|
}
|