forked from 1b-agent/opencadstudio-upstream
perf(raster): avoid needless image resampling
This commit is contained in:
1 parent
32c3844526
commit
36e0e40b36
2 files changed
+293
-151
No files matched your search
@@ -360,8 +360,10 @@ pub struct DecodedImage {
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
fn image_cache() -> &'static Mutex<HashMap<String, Option<DecodedImage>>> {
|
||||
static CACHE: OnceLock<Mutex<HashMap<String, Option<DecodedImage>>>> = OnceLock::new();
|
||||
type ImageCacheEntry = Arc<OnceLock<Option<DecodedImage>>>;
|
||||
|
||||
fn image_cache() -> &'static Mutex<HashMap<String, ImageCacheEntry>> {
|
||||
static CACHE: OnceLock<Mutex<HashMap<String, ImageCacheEntry>>> = OnceLock::new();
|
||||
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
@@ -385,22 +387,20 @@ pub fn resolve_image(path: &str) -> Option<DecodedImage> {
|
||||
if path.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// Cache probe in its own scope so the lock is released before any fetch.
|
||||
{
|
||||
if let Ok(cache) = image_cache().lock() {
|
||||
if let Some(cached) = cache.get(path) {
|
||||
return cached.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
let decoded = decode_reference(path);
|
||||
if let Ok(mut cache) = image_cache().lock() {
|
||||
cache.insert(path.to_string(), decoded.clone());
|
||||
}
|
||||
decoded
|
||||
let entry = {
|
||||
let mut cache = image_cache()
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
Arc::clone(
|
||||
cache
|
||||
.entry(path.to_string())
|
||||
.or_insert_with(|| Arc::new(OnceLock::new())),
|
||||
)
|
||||
};
|
||||
entry.get_or_init(|| decode_reference(path)).clone()
|
||||
}
|
||||
|
||||
/// Decode a reference (local path or remote URL) to downscaled RGBA pixels.
|
||||
/// Decode a reference (local path or remote URL) to RGBA pixels.
|
||||
fn decode_reference(path: &str) -> Option<DecodedImage> {
|
||||
let lower = path.to_ascii_lowercase();
|
||||
let img = if lower.starts_with("http://") || lower.starts_with("https://") {
|
||||
@@ -409,20 +409,7 @@ fn decode_reference(path: &str) -> Option<DecodedImage> {
|
||||
} else {
|
||||
image::open(Path::new(path)).ok()?
|
||||
};
|
||||
// GPUs cap 2-D texture dimensions (8192 with wgpu's default limits).
|
||||
// Downscale oversized images to fit, preserving aspect ratio, so texture
|
||||
// creation can't fail — they're displayed scaled-down anyway.
|
||||
const MAX_DIM: u32 = 8192;
|
||||
let img = if img.width() > MAX_DIM || img.height() > MAX_DIM {
|
||||
let longest = img.width().max(img.height()) as f32;
|
||||
let scale = MAX_DIM as f32 / longest;
|
||||
let nw = ((img.width() as f32 * scale) as u32).clamp(1, MAX_DIM);
|
||||
let nh = ((img.height() as f32 * scale) as u32).clamp(1, MAX_DIM);
|
||||
img.resize(nw, nh, image::imageops::FilterType::Triangle)
|
||||
} else {
|
||||
img
|
||||
};
|
||||
let rgba = img.to_rgba8();
|
||||
let rgba = img.into_rgba8();
|
||||
let (width, height) = rgba.dimensions();
|
||||
Some(DecodedImage {
|
||||
pixels: Arc::new(rgba.into_raw()),
|
||||
|
||||
+276
-121
@@ -8,6 +8,7 @@
|
||||
use crate::scene::model::image_model::ImageModel;
|
||||
use iced::wgpu;
|
||||
use iced::wgpu::util::DeviceExt;
|
||||
use std::sync::Arc;
|
||||
|
||||
// ── Vertex ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -86,15 +87,15 @@ struct ImageParams {
|
||||
|
||||
pub struct ImageGpu {
|
||||
pub vertex_buffer: wgpu::Buffer,
|
||||
pub instance_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: wgpu::Sampler,
|
||||
_params_buf: wgpu::Buffer,
|
||||
_sampler: Arc<wgpu::Sampler>,
|
||||
_params_buf: Arc<wgpu::Buffer>,
|
||||
}
|
||||
|
||||
impl ImageGpu {
|
||||
@@ -118,57 +119,7 @@ impl ImageGpu {
|
||||
});
|
||||
groups[slot].push(model);
|
||||
}
|
||||
groups
|
||||
.into_iter()
|
||||
.filter_map(|group| Self::new(device, queue, &group, bgl1))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn new(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
models: &[&ImageModel],
|
||||
bgl1: &wgpu::BindGroupLayout,
|
||||
) -> Option<Self> {
|
||||
let &model = models.first()?;
|
||||
if model.pixels.is_empty() || model.width == 0 || model.height == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// ── Upload texture ────────────────────────────────────────────────
|
||||
let tex_label = format!("image.texture:{}", model.file_path);
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some(&tex_label),
|
||||
size: wgpu::Extent3d {
|
||||
width: model.width,
|
||||
height: model.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[..],
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(4 * model.width),
|
||||
rows_per_image: Some(model.height),
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: model.width,
|
||||
height: model.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
let tex_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
// ── Sampler ───────────────────────────────────────────────────────
|
||||
let _sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
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,
|
||||
@@ -177,61 +128,30 @@ impl ImageGpu {
|
||||
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()
|
||||
}
|
||||
|
||||
// ── Opacity uniform ───────────────────────────────────────────────
|
||||
let params = ImageParams {
|
||||
opacity: model.opacity.clamp(0.0, 1.0),
|
||||
draw_depth: 0.0,
|
||||
_pad: [0.0; 2],
|
||||
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();
|
||||
};
|
||||
let _params_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("image.params"),
|
||||
contents: bytemuck::bytes_of(¶ms),
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
|
||||
// ── Bind group ────────────────────────────────────────────────────
|
||||
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(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// ── Vertex buffer — the image's visible triangles ─────────────────
|
||||
// `model.verts` already holds either the full quad or the triangulated
|
||||
// clip polygon, each vertex carrying an RTE-split position and its UV.
|
||||
let verts: Vec<ImageVertex> = model
|
||||
.verts
|
||||
.iter()
|
||||
.map(|v| ImageVertex {
|
||||
pos: v.pos,
|
||||
uv: v.uv,
|
||||
pos_low: v.pos_low,
|
||||
})
|
||||
.collect();
|
||||
if verts.is_empty() {
|
||||
return None;
|
||||
if model.pixels.is_empty() || model.width == 0 || model.height == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let vertex_count = verts.len() as u32;
|
||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("image.vbuf"),
|
||||
contents: bytemuck::cast_slice(&verts),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -259,21 +179,256 @@ impl ImageGpu {
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("image.instances"),
|
||||
contents: bytemuck::cast_slice(&instances),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
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,
|
||||
},
|
||||
));
|
||||
|
||||
Some(Self {
|
||||
vertex_buffer,
|
||||
instance_buffer,
|
||||
vertex_count,
|
||||
instance_count: instances.len() as u32,
|
||||
bind_group,
|
||||
_texture: texture,
|
||||
_sampler,
|
||||
_params_buf,
|
||||
})
|
||||
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::Rgba8UnormSrgb,
|
||||
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,
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user