forked from 1b-agent/opencadstudio-upstream
526 lines
18 KiB
Rust
526 lines
18 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; nearest magnification for raster images)
|
||
// binding 2 — ImageParams (opacity uniform, 16 bytes)
|
||
|
||
use crate::scene::model::image_model::ImageModel;
|
||
use iced::wgpu;
|
||
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,
|
||
/// 1.0 when pixel alpha applies, 0.0 when pixels draw opaque.
|
||
use_alpha: f32,
|
||
_pad: f32,
|
||
} // 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 = |mag_filter| {
|
||
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,
|
||
min_filter: wgpu::FilterMode::Linear,
|
||
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
||
..Default::default()
|
||
}))
|
||
};
|
||
// Raster images show square pixels when magnified.
|
||
let smooth = sampler(wgpu::FilterMode::Linear);
|
||
let pixelated = sampler(wgpu::FilterMode::Nearest);
|
||
groups
|
||
.into_iter()
|
||
.flat_map(|group| {
|
||
let sampler = if group.first().is_some_and(|m| m.pixelated) {
|
||
&pixelated
|
||
} else {
|
||
&smooth
|
||
};
|
||
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(super::gpu_upload::upload_buffer(
|
||
device,
|
||
queue,
|
||
"image.instances",
|
||
&instances,
|
||
wgpu::BufferUsages::VERTEX,
|
||
));
|
||
let params = ImageParams {
|
||
opacity: model.opacity.clamp(0.0, 1.0),
|
||
draw_depth: 0.0,
|
||
use_alpha: if model.use_alpha { 1.0 } else { 0.0 },
|
||
_pad: 0.0,
|
||
};
|
||
let params_buf = Arc::new(super::gpu_upload::upload_buffer(
|
||
device,
|
||
queue,
|
||
"image.params",
|
||
std::slice::from_ref(¶ms),
|
||
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 levels = mip_chain(
|
||
&model.pixels,
|
||
model.width,
|
||
x.data_start,
|
||
y.data_start,
|
||
width,
|
||
height,
|
||
);
|
||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||
label: Some(&tex_label),
|
||
size: wgpu::Extent3d {
|
||
width,
|
||
height,
|
||
depth_or_array_layers: 1,
|
||
},
|
||
mip_level_count: levels.len() as u32,
|
||
sample_count: 1,
|
||
dimension: wgpu::TextureDimension::D2,
|
||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||
view_formats: &[],
|
||
});
|
||
for (level, (w, h, data)) in levels.iter().enumerate() {
|
||
queue.write_texture(
|
||
wgpu::TexelCopyTextureInfo {
|
||
texture: &texture,
|
||
mip_level: level as u32,
|
||
origin: wgpu::Origin3d::ZERO,
|
||
aspect: wgpu::TextureAspect::All,
|
||
},
|
||
data,
|
||
wgpu::TexelCopyBufferLayout {
|
||
offset: 0,
|
||
bytes_per_row: Some(4 * w),
|
||
rows_per_image: Some(*h),
|
||
},
|
||
wgpu::Extent3d {
|
||
width: *w,
|
||
height: *h,
|
||
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 = super::gpu_upload::upload_buffer(
|
||
device,
|
||
queue,
|
||
"image.vbuf",
|
||
&verts,
|
||
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,
|
||
}
|
||
|
||
/// A tile's pixels and its smaller levels down to 1×1, so a far view
|
||
/// samples a level near its own size. A level keeps the most opaque of each
|
||
/// 2×2 block unless all four are equally opaque (then their average): thin
|
||
/// lines on a transparent page (PDF, DWF, DGN underlays) stay visible when
|
||
/// zoomed out instead of falling between samples, and opaque images are
|
||
/// averaged as usual.
|
||
fn mip_chain(
|
||
pixels: &[u8],
|
||
stride: u32,
|
||
x0: u32,
|
||
y0: u32,
|
||
w: u32,
|
||
h: u32,
|
||
) -> Vec<(u32, u32, Vec<u8>)> {
|
||
let (stride, x0, y0) = (stride as usize, x0 as usize, y0 as usize);
|
||
let mut base = Vec::with_capacity(w as usize * h as usize * 4);
|
||
for row in 0..h as usize {
|
||
let start = ((y0 + row) * stride + x0) * 4;
|
||
base.extend_from_slice(&pixels[start..start + w as usize * 4]);
|
||
}
|
||
let mut levels = vec![(w, h, base)];
|
||
loop {
|
||
let (pw, ph, prev) = levels.last().expect("base level");
|
||
if *pw == 1 && *ph == 1 {
|
||
break;
|
||
}
|
||
let (pw, ph) = (*pw as usize, *ph as usize);
|
||
let (nw, nh) = ((pw / 2).max(1), (ph / 2).max(1));
|
||
let mut next = Vec::with_capacity(nw * nh * 4);
|
||
for y in 0..nh {
|
||
for x in 0..nw {
|
||
let block = [(0, 0), (1, 0), (0, 1), (1, 1)].map(|(dx, dy)| {
|
||
let at = (((2 * y + dy).min(ph - 1)) * pw + (2 * x + dx).min(pw - 1)) * 4;
|
||
[prev[at], prev[at + 1], prev[at + 2], prev[at + 3]]
|
||
});
|
||
if block.iter().all(|p| p[3] == block[0][3]) {
|
||
let mut sum = [0u32; 4];
|
||
for p in &block {
|
||
for c in 0..4 {
|
||
sum[c] += p[c] as u32;
|
||
}
|
||
}
|
||
next.extend(sum.map(|v| ((v + 2) / 4) as u8));
|
||
} else {
|
||
next.extend(*block.iter().max_by_key(|p| p[3]).expect("four pixels"));
|
||
}
|
||
}
|
||
}
|
||
levels.push((nw as u32, nh as u32, next));
|
||
}
|
||
levels
|
||
}
|
||
|
||
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,
|
||
]
|
||
}
|