Files
OneCAD-Draft/src/scene/pipeline/mesh_gpu.rs
T

2505 lines
89 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// GPU buffers for triangle meshes.
use crate::scene::model::mesh_model::{MeshLodSet, MeshModel};
use iced::wgpu;
// ── Vertex layout ─────────────────────────────────────────────────────────
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct MeshVertex {
pub position: [f32; 3],
pub normal: [f32; 3],
pub position_low: [f32; 3],
pub uv_diffuse: [f32; 2],
pub uv_specular: [f32; 2],
pub uv_reflection: [f32; 2],
pub uv_opacity: [f32; 2],
pub uv_bump: [f32; 2],
pub uv_refraction: [f32; 2],
pub uv_normal: [f32; 2],
}
impl MeshVertex {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, position) as u64,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, normal) as u64,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, position_low) as u64,
shader_location: 3,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_diffuse) as u64,
shader_location: 6,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_specular) as u64,
shader_location: 10,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_opacity) as u64,
shader_location: 11,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_refraction) as u64,
shader_location: 12,
format: wgpu::VertexFormat::Float32x4,
},
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<MeshVertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: ATTRS,
}
}
/// Surface-independent layout for mesh edge pipelines.
pub fn edge_layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshEdgeVertex, position) as u64,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshEdgeVertex, color) as u64,
shader_location: 2,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshEdgeVertex, position_low) as u64,
shader_location: 3,
format: wgpu::VertexFormat::Float32x3,
},
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<MeshEdgeVertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: ATTRS,
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub struct MeshPlainVertex {
pub position: [f32; 3],
pub normal: [f32; 3],
pub position_low: [f32; 3],
}
impl MeshPlainVertex {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshPlainVertex, position) as u64,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshPlainVertex, normal) as u64,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshPlainVertex, position_low) as u64,
shader_location: 3,
format: wgpu::VertexFormat::Float32x3,
},
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: ATTRS,
}
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct MeshEdgeVertex {
pub position: [f32; 3],
pub color: [f32; 4],
pub position_low: [f32; 3],
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct SilhouetteVertex {
pub position: [f32; 3],
pub color: [f32; 4],
pub position_low: [f32; 3],
}
impl SilhouetteVertex {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute {
offset: std::mem::offset_of!(SilhouetteVertex, position) as u64,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(SilhouetteVertex, color) as u64,
shader_location: 2,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(SilhouetteVertex, position_low) as u64,
shader_location: 3,
format: wgpu::VertexFormat::Float32x3,
},
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: ATTRS,
}
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct SilhouetteInstance {
pub translation: [f32; 3],
pub translation_low: [f32; 3],
}
impl SilhouetteInstance {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute {
offset: std::mem::offset_of!(SilhouetteInstance, translation) as u64,
shader_location: 4,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(SilhouetteInstance, translation_low) as u64,
shader_location: 5,
format: wgpu::VertexFormat::Float32x3,
},
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: ATTRS,
}
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct MeshInstanceGpu {
pub model_row_0: [f32; 4],
pub model_row_1: [f32; 4],
pub model_row_2: [f32; 4],
pub translation_low: [f32; 4],
pub normal_row_0: [f32; 4],
pub normal_row_1: [f32; 4],
pub normal_row_2: [f32; 4],
}
impl MeshInstanceGpu {
fn identity() -> Self {
Self {
model_row_0: [1.0, 0.0, 0.0, 0.0],
model_row_1: [0.0, 1.0, 0.0, 0.0],
model_row_2: [0.0, 0.0, 1.0, 0.0],
translation_low: [0.0; 4],
normal_row_0: [1.0, 0.0, 0.0, 0.0],
normal_row_1: [0.0, 1.0, 0.0, 0.0],
normal_row_2: [0.0, 0.0, 1.0, 0.0],
}
}
fn from_transform(transform: acadrust::types::Transform) -> Self {
let m = transform.matrix.m;
let translation = [m[0][3], m[1][3], m[2][3]];
let translation_high = [
translation[0] as f32,
translation[1] as f32,
translation[2] as f32,
];
let linear = glam::DMat3::from_cols_array(&[
m[0][0], m[1][0], m[2][0],
m[0][1], m[1][1], m[2][1],
m[0][2], m[1][2], m[2][2],
]);
let normal = if linear.determinant().abs() > 1e-18 {
linear.inverse().transpose()
} else {
glam::DMat3::IDENTITY
};
let n = normal.to_cols_array();
Self {
model_row_0: [
m[0][0] as f32,
m[0][1] as f32,
m[0][2] as f32,
translation_high[0],
],
model_row_1: [
m[1][0] as f32,
m[1][1] as f32,
m[1][2] as f32,
translation_high[1],
],
model_row_2: [
m[2][0] as f32,
m[2][1] as f32,
m[2][2] as f32,
translation_high[2],
],
translation_low: [
(translation[0] - translation_high[0] as f64) as f32,
(translation[1] - translation_high[1] as f64) as f32,
(translation[2] - translation_high[2] as f64) as f32,
0.0,
],
normal_row_0: [n[0] as f32, n[3] as f32, n[6] as f32, 0.0],
normal_row_1: [n[1] as f32, n[4] as f32, n[7] as f32, 0.0],
normal_row_2: [n[2] as f32, n[5] as f32, n[8] as f32, 0.0],
}
}
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshInstanceGpu, model_row_0) as u64,
shader_location: 4,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshInstanceGpu, model_row_1) as u64,
shader_location: 5,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshInstanceGpu, model_row_2) as u64,
shader_location: 7,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshInstanceGpu, translation_low) as u64,
shader_location: 8,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshInstanceGpu, normal_row_0) as u64,
shader_location: 9,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshInstanceGpu, normal_row_1) as u64,
shader_location: 13,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshInstanceGpu, normal_row_2) as u64,
shader_location: 14,
format: wgpu::VertexFormat::Float32x4,
},
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: ATTRS,
}
}
}
// ── Batched mesh buffers ──────────────────────────────────────────────────
//
// One GPU allocation per solid means one vertex/index bind + draw call per solid —
// ~10k draw calls a frame on a heavy 3D model, which strangles the GPU front
// end. The batch concatenates every solid's LOD0 geometry into a handful of
// large buffers (split only to stay under the 256 MB per-buffer cap), so the
// whole mesh set draws in a few calls. Vertices already carry their own colour,
// so no per-mesh state is needed between draws. Built once per geometry epoch —
// selection/hover no longer rebuild it (that tint is dropped in the batch path).
#[derive(Clone)]
pub struct MeshBatchChunk {
pub vertex_buffer: wgpu::Buffer,
pub compact_vertices: bool,
/// Opaque triangle indices (mesh colour alpha ≈ 1). Drawn with depth write.
pub index_buffer: wgpu::Buffer,
pub index_count: u32,
/// Transparent triangle indices (mesh colour alpha < 1). Drawn after the
/// opaque fills with depth write disabled so they blend over — rather than
/// erase — the geometry behind them.
pub transp_index_buffer: wgpu::Buffer,
pub transp_index_count: u32,
/// Expanded triangle-edge line list for plain meshes that carry no B-rep
/// edges. Keeping the edge endpoints together avoids coupling this buffer
/// to the surface mesh's indexing and vertex representation.
pub wire_vertex_buffer: wgpu::Buffer,
pub wire_vertex_count: u32,
/// B-rep feature edges of ACIS solids, as a standalone LineList vertex
/// buffer (pairs of endpoints), drawn non-indexed. Empty for plain meshes.
pub edge_vertex_buffer: wgpu::Buffer,
pub edge_vertex_count: u32,
pub instance_buffer: wgpu::Buffer,
pub instance_count: u32,
pub highlight_ranges: Vec<MeshBatchRange>,
pub handles: rustc_hash::FxHashSet<acadrust::Handle>,
pub material: Option<crate::scene::model::material_model::MeshMaterial>,
pub face_color: [f32; 4],
pub material_bind_group: Option<wgpu::BindGroup>,
}
#[derive(Clone, Copy)]
pub struct MeshBatchRange {
pub handle: acadrust::Handle,
pub index_start: u32,
pub index_count: u32,
pub transparent: bool,
pub instance_start: u32,
pub instance_count: u32,
}
struct MeshBatchStubs {
index: wgpu::Buffer,
vertex: wgpu::Buffer,
}
impl MeshBatchStubs {
fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
Self {
index: super::gpu_upload::upload_buffer(
device,
queue,
"mesh.batch.empty_ibuf",
&[0u32],
wgpu::BufferUsages::INDEX,
),
vertex: device.create_buffer(&wgpu::BufferDescriptor {
label: Some("mesh.batch.empty_vbuf"),
size: std::mem::size_of::<MeshVertex>() as u64,
usage: wgpu::BufferUsages::VERTEX,
mapped_at_creation: false,
}),
}
}
}
fn make_chunk(
device: &wgpu::Device,
queue: &wgpu::Queue,
stubs: &MeshBatchStubs,
verts: &[MeshVertex],
indices: &[u32],
transp_indices: &[u32],
wire_indices: &[u32],
edge_verts: &[MeshEdgeVertex],
highlight_ranges: &[MeshBatchRange],
instances: &[MeshInstanceGpu],
handles: &rustc_hash::FxHashSet<acadrust::Handle>,
_bounds_override: Option<[f32; 6]>,
material: Option<&crate::scene::model::material_model::MeshMaterial>,
face_color: [f32; 4],
vertex_buffer_override: Option<wgpu::Buffer>,
edge_buffer_override: Option<wgpu::Buffer>,
) -> MeshBatchChunk {
let mk_index = |data: &[u32], label: &'static str| {
if data.is_empty() {
return stubs.index.clone();
}
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: std::mem::size_of_val(data) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&buffer, 0, bytemuck::cast_slice(data));
buffer
};
let mk_vertex = |data: &[MeshVertex], label: &'static str| {
if data.is_empty() {
return stubs.vertex.clone();
}
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: std::mem::size_of_val(data) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&buffer, 0, bytemuck::cast_slice(data));
buffer
};
let compact_vertices = !material_has_textures(material);
let mk_plain_vertex = |data: &[MeshVertex]| {
if data.is_empty() {
return stubs.vertex.clone();
}
let compact: Vec<MeshPlainVertex> = data
.iter()
.map(|vertex| MeshPlainVertex {
position: vertex.position,
normal: vertex.normal,
position_low: vertex.position_low,
})
.collect();
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("mesh.batch.plain_vbuf"),
size: std::mem::size_of_val(compact.as_slice()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&buffer, 0, bytemuck::cast_slice(&compact));
buffer
};
let mk_edge_vertex = |data: &[MeshEdgeVertex], label: &'static str| {
if data.is_empty() {
return stubs.vertex.clone();
}
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: std::mem::size_of_val(data) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&buffer, 0, bytemuck::cast_slice(data));
buffer
};
let identity = [MeshInstanceGpu::identity()];
let instance_data = if instances.is_empty() {
&identity[..]
} else {
instances
};
let mut wire_vertices = Vec::with_capacity(wire_indices.len());
for line in wire_indices.chunks_exact(2) {
let (Some(start), Some(end)) = (
verts.get(line[0] as usize),
verts.get(line[1] as usize),
) else {
continue;
};
for vertex in [start, end] {
wire_vertices.push(MeshEdgeVertex {
position: vertex.position,
color: face_color,
position_low: vertex.position_low,
});
}
}
MeshBatchChunk {
vertex_buffer: vertex_buffer_override.unwrap_or_else(|| {
if compact_vertices {
mk_plain_vertex(verts)
} else {
mk_vertex(verts, "mesh.batch.vbuf")
}
}),
compact_vertices,
index_buffer: mk_index(indices, "mesh.batch.ibuf"),
index_count: indices.len() as u32,
transp_index_buffer: mk_index(transp_indices, "mesh.batch.transp_ibuf"),
transp_index_count: transp_indices.len() as u32,
wire_vertex_buffer: mk_edge_vertex(&wire_vertices, "mesh.batch.wire_vbuf"),
wire_vertex_count: wire_vertices.len() as u32,
edge_vertex_buffer: edge_buffer_override
.unwrap_or_else(|| mk_edge_vertex(edge_verts, "mesh.batch.edge_vbuf")),
edge_vertex_count: edge_verts.len() as u32,
instance_buffer: {
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("mesh.batch.instances"),
size: std::mem::size_of_val(instance_data) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&buffer, 0, bytemuck::cast_slice(instance_data));
buffer
},
instance_count: instance_data.len() as u32,
highlight_ranges: highlight_ranges.to_vec(),
handles: handles.clone(),
material: material.cloned(),
face_color,
material_bind_group: None,
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct MaterialMapParams {
/// diffuse, specular, reflection and opacity blend factors.
blends0: [f32; 4],
/// Presence bits for the same four maps.
present0: [u32; 4],
/// bump, refraction, normal and reserved blend factors.
blends1: [f32; 4],
/// Presence bits for the same four maps.
present1: [u32; 4],
/// Tiling modes for diffuse, specular, reflection and opacity.
tiling0: [u32; 4],
/// Tiling modes for bump, refraction, normal and reserved.
tiling1: [u32; 4],
/// Two-sided, normal-map method, global-illumination and final-gather modes.
render_modes: [u32; 4],
/// Advanced-data-present, anonymous and two reserved source-state bits.
source_state: [u32; 4],
/// Color-bleed scale and reserved render values.
indirect: [f32; 4],
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct MeshSurfaceParams {
face_color: [f32; 4],
material: [f32; 4],
specular: [f32; 4],
ambient: [f32; 4],
advanced: [f32; 4],
flags: [u32; 4],
}
fn valid_rgba_size(width: u32, height: u32, pixels: &[u8]) -> bool {
width > 0 && height > 0
&& (width as usize).checked_mul(height as usize).and_then(|size| size.checked_mul(4)) == Some(pixels.len())
}
fn downscale_rgba_to_limit(width: u32, height: u32, rgba: &[u8], limit: u32) -> Option<(u32, u32, Vec<u8>)> {
if limit == 0 || !valid_rgba_size(width, height, rgba) {
return None;
}
let buffer = image::RgbaImage::from_raw(width, height, rgba.to_vec())?;
let scale = (limit as f64 / width.max(height) as f64).min(1.0);
let new_width = ((width as f64 * scale).round() as u32).clamp(1, limit);
let new_height = ((height as f64 * scale).round() as u32).clamp(1, limit);
let resized = image::imageops::resize(&buffer, new_width, new_height, image::imageops::FilterType::Triangle);
Some((new_width, new_height, resized.into_raw()))
}
fn upload_rgba_texture(
device: &wgpu::Device,
queue: &wgpu::Queue,
label: &'static str,
image: Option<&crate::scene::model::material_model::MaterialImage>,
fallback: [u8; 4],
srgb: bool,
) -> wgpu::TextureView {
let limit = device.limits().max_texture_dimension_2d.max(1);
let resized;
let image = image.filter(|image| valid_rgba_size(image.width, image.height, &image.rgba));
let (width, height, pixels): (u32, u32, &[u8]) = match image {
Some(image) if image.width > limit || image.height > limit => {
match downscale_rgba_to_limit(image.width, image.height, &image.rgba, limit) {
Some((w, h, buf)) => {
resized = buf;
(w, h, resized.as_slice())
}
None => (1, 1, fallback.as_slice()),
}
}
Some(image) => (image.width, image.height, image.rgba.as_slice()),
None => (1, 1, fallback.as_slice()),
};
let 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: if srgb {
wgpu::TextureFormat::Rgba8UnormSrgb
} else {
wgpu::TextureFormat::Rgba8Unorm
},
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
texture.as_image_copy(),
pixels,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(width * 4),
rows_per_image: Some(height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
texture.create_view(&wgpu::TextureViewDescriptor::default())
}
pub struct MeshMaterialResources {
diffuse_view: wgpu::TextureView,
specular_view: wgpu::TextureView,
reflection_view: wgpu::TextureView,
opacity_view: wgpu::TextureView,
bump_view: wgpu::TextureView,
refraction_view: wgpu::TextureView,
normal_view: wgpu::TextureView,
diffuse_sampler: wgpu::Sampler,
specular_sampler: wgpu::Sampler,
reflection_sampler: wgpu::Sampler,
opacity_sampler: wgpu::Sampler,
bump_sampler: wgpu::Sampler,
refraction_sampler: wgpu::Sampler,
normal_sampler: wgpu::Sampler,
params_buffer: wgpu::Buffer,
}
pub fn create_material_resources(
device: &wgpu::Device,
queue: &wgpu::Queue,
material: Option<&crate::scene::model::material_model::MeshMaterial>,
) -> MeshMaterialResources {
let diffuse = material.and_then(|material| material.diffuse_map.image.as_deref());
let specular = material.and_then(|material| material.specular_map.image.as_deref());
let reflection = material.and_then(|material| material.reflection_map.image.as_deref());
let opacity = material.and_then(|material| material.opacity_map.image.as_deref());
let bump = material.and_then(|material| material.bump_map.image.as_deref());
let refraction = material.and_then(|material| material.refraction_map.image.as_deref());
let normal = material.and_then(|material| material.normal_map.image.as_deref());
let diffuse_view =
upload_rgba_texture(device, queue, "mesh.material.diffuse", diffuse, [255; 4], true);
let specular_view =
upload_rgba_texture(device, queue, "mesh.material.specular", specular, [255; 4], true);
let reflection_view =
upload_rgba_texture(device, queue, "mesh.material.reflection", reflection, [0, 0, 0, 255], true);
let opacity_view =
upload_rgba_texture(device, queue, "mesh.material.opacity", opacity, [255; 4], false);
let bump_view =
upload_rgba_texture(device, queue, "mesh.material.bump", bump, [128, 128, 128, 255], false);
let refraction_view =
upload_rgba_texture(device, queue, "mesh.material.refraction", refraction, [255; 4], true);
let normal_view = upload_rgba_texture(
device,
queue,
"mesh.material.normal",
normal,
[128, 128, 255, 255],
false,
);
let sampler = |label: &'static str, tiling: u8| {
let address = match tiling {
0 | 1 => wgpu::AddressMode::Repeat,
4 => wgpu::AddressMode::MirrorRepeat,
_ => wgpu::AddressMode::ClampToEdge,
};
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some(label),
address_mode_u: address,
address_mode_v: address,
address_mode_w: address,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
..Default::default()
})
};
let tiling = |channel: usize| {
material.map_or(1, |material| {
[
material.diffuse_map.tiling,
material.specular_map.tiling,
material.reflection_map.tiling,
material.opacity_map.tiling,
material.bump_map.tiling,
material.refraction_map.tiling,
material.normal_map.tiling,
][channel]
})
};
let diffuse_sampler = sampler("mesh.material.diffuse_sampler", tiling(0));
let specular_sampler = sampler("mesh.material.specular_sampler", tiling(1));
let reflection_sampler = sampler("mesh.material.reflection_sampler", tiling(2));
let opacity_sampler = sampler("mesh.material.opacity_sampler", tiling(3));
let bump_sampler = sampler("mesh.material.bump_sampler", tiling(4));
let refraction_sampler = sampler("mesh.material.refraction_sampler", tiling(5));
let normal_sampler = sampler("mesh.material.normal_sampler", tiling(6));
let channel_flags = material.map_or(0x7f, |material| material.channel_flags as u32);
let channel_present = |channel: usize, bit: u32| {
material.is_some_and(|material| {
let maps = [
&material.diffuse_map,
&material.specular_map,
&material.reflection_map,
&material.opacity_map,
&material.bump_map,
&material.refraction_map,
&material.normal_map,
];
channel_flags & bit != 0 && maps[channel].image.is_some()
}) as u32
};
let params = MaterialMapParams {
blends0: material.map_or([0.0; 4], |material| {
[
material.diffuse_map.blend_factor,
material.specular_map.blend_factor,
material.reflection_map.blend_factor,
material.opacity_map.blend_factor,
]
}),
present0: material.map_or([0; 4], |_material| {
[
channel_present(0, 0x01),
channel_present(1, 0x02),
channel_present(2, 0x04),
channel_present(3, 0x08),
]
}),
blends1: material.map_or([0.0; 4], |material| {
[
material.bump_map.blend_factor,
material.refraction_map.blend_factor,
material.normal_map.blend_factor,
0.0,
]
}),
present1: material.map_or([0; 4], |material| {
[
channel_present(4, 0x10),
channel_present(5, 0x20),
material.normal_map.image.is_some() as u32
* (material.normal_map_method == 0) as u32,
0,
]
}),
tiling0: material.map_or([1; 4], |material| {
[
material.diffuse_map.tiling as u32,
material.specular_map.tiling as u32,
material.reflection_map.tiling as u32,
material.opacity_map.tiling as u32,
]
}),
tiling1: material.map_or([1; 4], |material| {
[
material.bump_map.tiling as u32,
material.refraction_map.tiling as u32,
material.normal_map.tiling as u32,
1,
]
}),
render_modes: material.map_or([1, 0, 0, 0], |material| {
[
material.two_sided as u32,
material.normal_map_method as u32,
material.global_illumination as u32,
material.final_gather as u32,
]
}),
source_state: material.map_or([0; 4], |material| {
[
material.advanced_data_present as u32,
material.is_anonymous as u32,
material.handle.is_some() as u32,
0,
]
}),
indirect: material.map_or([1.0, 0.0, 0.0, 0.0], |material| {
[material.color_bleed_scale, 0.0, 0.0, 0.0]
}),
};
let params_buffer = super::gpu_upload::upload_buffer(
device,
queue,
"mesh.material.params",
std::slice::from_ref(&params),
wgpu::BufferUsages::UNIFORM,
);
MeshMaterialResources {
diffuse_view,
specular_view,
reflection_view,
opacity_view,
bump_view,
refraction_view,
normal_view,
diffuse_sampler,
specular_sampler,
reflection_sampler,
opacity_sampler,
bump_sampler,
refraction_sampler,
normal_sampler,
params_buffer,
}
}
pub fn create_material_bind_group_from_resources(
device: &wgpu::Device,
queue: &wgpu::Queue,
layout: &wgpu::BindGroupLayout,
resources: &MeshMaterialResources,
material: Option<&crate::scene::model::material_model::MeshMaterial>,
face_color: [f32; 4],
) -> wgpu::BindGroup {
let (material_params, specular, ambient, advanced, flags) =
material_vertex_params(material);
let surface = MeshSurfaceParams {
face_color,
material: material_params,
specular,
ambient,
advanced,
flags,
};
let surface_buffer = super::gpu_upload::upload_buffer(
device,
queue,
"mesh.material.surface",
std::slice::from_ref(&surface),
wgpu::BufferUsages::UNIFORM,
);
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("mesh.material.bind_group"),
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&resources.diffuse_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&resources.specular_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(&resources.reflection_view),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(&resources.opacity_view),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::TextureView(&resources.bump_view),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::TextureView(&resources.refraction_view),
},
wgpu::BindGroupEntry {
binding: 6,
resource: wgpu::BindingResource::TextureView(&resources.normal_view),
},
wgpu::BindGroupEntry {
binding: 7,
resource: wgpu::BindingResource::Sampler(&resources.diffuse_sampler),
},
wgpu::BindGroupEntry {
binding: 8,
resource: wgpu::BindingResource::Sampler(&resources.specular_sampler),
},
wgpu::BindGroupEntry {
binding: 9,
resource: wgpu::BindingResource::Sampler(&resources.reflection_sampler),
},
wgpu::BindGroupEntry {
binding: 10,
resource: wgpu::BindingResource::Sampler(&resources.opacity_sampler),
},
wgpu::BindGroupEntry {
binding: 11,
resource: wgpu::BindingResource::Sampler(&resources.bump_sampler),
},
wgpu::BindGroupEntry {
binding: 12,
resource: wgpu::BindingResource::Sampler(&resources.refraction_sampler),
},
wgpu::BindGroupEntry {
binding: 13,
resource: wgpu::BindingResource::Sampler(&resources.normal_sampler),
},
wgpu::BindGroupEntry {
binding: 14,
resource: resources.params_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 16,
resource: surface_buffer.as_entire_binding(),
},
],
})
}
pub fn create_material_bind_group(
device: &wgpu::Device,
queue: &wgpu::Queue,
layout: &wgpu::BindGroupLayout,
material: Option<&crate::scene::model::material_model::MeshMaterial>,
) -> wgpu::BindGroup {
let resources = create_material_resources(device, queue, material);
let color = material.map_or([0.8, 0.8, 0.8, 1.0], |material| material.diffuse);
create_material_bind_group_from_resources(
device,
queue,
layout,
&resources,
material,
color,
)
}
pub fn upload_chunk_material_bind_groups(
device: &wgpu::Device,
queue: &wgpu::Queue,
layout: &wgpu::BindGroupLayout,
chunks: &mut [MeshBatchChunk],
) {
let mut fallback = None;
let mut materials = rustc_hash::FxHashMap::default();
for chunk in chunks {
let resources = match chunk.material.as_ref() {
None => fallback
.get_or_insert_with(|| create_material_resources(device, queue, None)),
Some(material) => {
let Some(handle) = material.handle else {
let resources = create_material_resources(device, queue, Some(material));
chunk.material_bind_group = Some(create_material_bind_group_from_resources(
device,
queue,
layout,
&resources,
chunk.material.as_ref(),
chunk.face_color,
));
continue;
};
materials.entry(handle.value()).or_insert_with(|| {
create_material_resources(device, queue, Some(material))
})
}
};
chunk.material_bind_group = Some(create_material_bind_group_from_resources(
device,
queue,
layout,
resources,
chunk.material.as_ref(),
chunk.face_color,
));
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct MaterialBatchKey([u32; 40]);
fn material_key(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
color: [f32; 4],
) -> MaterialBatchKey {
let mut key = [0u32; 40];
key[2..6].copy_from_slice(&color.map(f32::to_bits));
let Some(material) = material else {
return MaterialBatchKey(key);
};
let handle = material.handle.map_or(0, |handle| handle.value());
key[0] = handle as u32;
key[1] = (handle >> 32) as u32;
key[6..10].copy_from_slice(&material.diffuse.map(f32::to_bits));
key[10..13].copy_from_slice(&material.ambient.map(f32::to_bits));
key[13..16].copy_from_slice(&material.specular.map(f32::to_bits));
key[16] = material.gloss.to_bits();
key[17] = material.reflectivity.to_bits();
key[18] = material.self_illumination.to_bits();
key[19] = material.translucence.to_bits();
key[20] = material.refraction_index.to_bits();
key[21] = material.luminance.to_bits();
key[22] = material.two_sided as u32;
key[23] = material.illumination_model as u32;
key[24] = material.channel_flags as u32;
key[25] = material.mode as u32;
key[26] = material.indirect_bump_scale.to_bits();
key[27] = material.reflectance_scale.to_bits();
key[28] = material.transmittance_scale.to_bits();
key[29] = material.luminance_mode as u32;
key[30] = material.normal_map_method as u32;
key[31] = material.normal_map_strength.to_bits();
key[32] = material.is_anonymous as u32;
key[33] = material.global_illumination as u32;
key[34] = material.final_gather as u32;
key[35] = material.color_bleed_scale.to_bits();
key[36] = material.advanced_data_present as u32;
MaterialBatchKey(key)
}
fn morton_axis(mut value: u32) -> u64 {
value &= 0x3ff;
let mut result = 0u64;
for bit in 0..10 {
result |= ((value >> bit) as u64 & 1) << (bit * 3);
}
result
}
fn mesh_spatial_key(set: &MeshLodSet, bounds: [f32; 6]) -> u64 {
let center = [
(set.world_aabb[0] + set.world_aabb[2]) * 0.5,
(set.world_aabb[1] + set.world_aabb[3]) * 0.5,
(set.z_aabb[0] + set.z_aabb[1]) * 0.5,
];
let quantize = |value: f32, min: f32, max: f32| {
let span = max - min;
if !value.is_finite() || !span.is_finite() || span <= f32::EPSILON {
0
} else {
(((value - min) / span).clamp(0.0, 1.0) * 1023.0).round() as u32
}
};
let x = quantize(center[0], bounds[0], bounds[3]);
let y = quantize(center[1], bounds[1], bounds[4]);
let z = quantize(center[2], bounds[2], bounds[5]);
morton_axis(x) | (morton_axis(y) << 1) | (morton_axis(z) << 2)
}
struct MeshBatchPart<'a> {
set: &'a MeshLodSet,
mesh: &'a MeshModel,
uv_mesh: &'a MeshModel,
entity_handle: Option<acadrust::Handle>,
display_color: [f32; 4],
material: Option<&'a crate::scene::model::material_model::MeshMaterial>,
color: [f32; 4],
indices: std::sync::Arc<[u32]>,
index_hash: u64,
include_faces: bool,
include_edges: bool,
part_slot: u32,
}
#[derive(Clone, PartialEq, Eq, Hash)]
struct FacePartitionKey {
instanced: bool,
source: u64,
base_material: MaterialBatchKey,
face_materials: Vec<(u64, MaterialBatchKey)>,
visual_style: [u64; 12],
display_color: [u32; 4],
include_faces: bool,
include_edges: bool,
}
#[derive(Clone)]
struct CachedFacePart<'a> {
material: Option<&'a crate::scene::model::material_model::MeshMaterial>,
color: [f32; 4],
indices: std::sync::Arc<[u32]>,
index_hash: u64,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct InstanceGroupKey {
material: MaterialBatchKey,
source: u64,
part_slot: u32,
index_count: usize,
index_hash: u64,
include_faces: bool,
include_edges: bool,
}
fn index_hash(indices: &[u32]) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = rustc_hash::FxHasher::default();
indices.hash(&mut hasher);
hasher.finish()
}
fn optimize_triangle_indices(indices: &[u32], vertex_count: usize) -> std::sync::Arc<[u32]> {
#[cfg(not(target_arch = "wasm32"))]
{
return meshopt::optimize::optimize_vertex_cache(indices, vertex_count).into();
}
#[cfg(target_arch = "wasm32")]
{
let _ = vertex_count;
indices.into()
}
}
fn visual_style_partition_key(
style: Option<&crate::scene::model::visual_style_model::MeshVisualStyle>,
) -> [u64; 12] {
let mut key = [0; 12];
let Some(style) = style else {
return key;
};
key[0] = style.face_lighting_model as u64;
key[1] = style.face_color_mode as u64;
key[2] = style.face_modifier as u64;
key[3] = style.face_opacity.to_bits() as u64;
if let Some(color) = style.mono_color {
key[4] = 1;
for (slot, value) in key[5..9].iter_mut().zip(color) {
*slot = value.to_bits() as u64;
}
}
key
}
#[derive(Default)]
struct InstancedBuildProfile {
vertices: std::time::Duration,
edges: std::time::Duration,
instances: std::time::Duration,
upload: std::time::Duration,
vertex_count: usize,
edge_count: usize,
index_count: usize,
chunk_count: usize,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct InstancedVertexKey {
source: u64,
material: u64,
compact: bool,
}
#[derive(Default)]
struct InstancedBufferCache {
vertices: rustc_hash::FxHashMap<InstancedVertexKey, wgpu::Buffer>,
edges: rustc_hash::FxHashMap<(u64, [u32; 4]), wgpu::Buffer>,
}
fn material_has_textures(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
) -> bool {
material.is_some_and(|material| {
[
&material.diffuse_map,
&material.specular_map,
&material.reflection_map,
&material.opacity_map,
&material.bump_map,
&material.refraction_map,
&material.normal_map,
]
.into_iter()
.any(|map| map.image.is_some())
})
}
fn build_instanced_chunks(
device: &wgpu::Device,
queue: &wgpu::Queue,
stubs: &MeshBatchStubs,
buffers: &mut InstancedBufferCache,
parts: &[MeshBatchPart<'_>],
profile_enabled: bool,
) -> Option<(Vec<MeshBatchChunk>, u64, InstancedBuildProfile)> {
let started = profile_enabled.then(iced::time::Instant::now);
let first = parts.first()?;
let source = first.set.instance_source.as_ref()?;
let mesh = first.mesh;
let material = first.material;
let color = first.color;
let source_handle = source.handle.value();
let compact_vertices = !material_has_textures(material);
let material_identity = if material_has_textures(material) {
material.map_or(0, |material| {
material
.handle
.map_or(material as *const _ as usize as u64, |handle| handle.value())
})
} else {
0
};
let vertex_key = InstancedVertexKey {
source: source_handle,
material: material_identity,
compact: compact_vertices,
};
let budget = super::gpu_budget::buffer_budget(device);
let needs_wire_vertices = first.include_edges && source.edge_verts.is_empty();
let split_geometry = mesh
.verts
.len()
.saturating_mul(std::mem::size_of::<MeshVertex>())
> budget
|| first
.indices
.len()
.saturating_mul(std::mem::size_of::<u32>())
> budget
|| (needs_wire_vertices
&& first
.indices
.len()
.saturating_mul(2 * std::mem::size_of::<MeshEdgeVertex>())
> budget)
|| (first.include_edges
&& source
.edge_verts
.len()
.saturating_mul(std::mem::size_of::<MeshEdgeVertex>())
> budget);
let shared_vertex_buffer = (!split_geometry)
.then(|| buffers.vertices.get(&vertex_key).cloned())
.flatten();
let has_normals = mesh.normals.len() == mesh.verts.len();
let bounds = mesh_bounds(mesh);
let vertex = |index: usize| {
let normal = if has_normals {
mesh.normals[index]
} else {
[0.0, 1.0, 0.0]
};
let position = mesh.verts[index];
let position_low = mesh.verts_low.get(index).copied().unwrap_or([0.0; 3]);
let uvs = material_uvs(
material,
position,
normal,
bounds,
position,
normal,
bounds,
);
MeshVertex {
position,
normal,
position_low,
uv_diffuse: uvs[0],
uv_specular: uvs[1],
uv_reflection: uvs[2],
uv_opacity: uvs[3],
uv_bump: uvs[4],
uv_refraction: uvs[5],
uv_normal: uvs[6],
}
};
let verts: Vec<_> =
if !split_geometry && (shared_vertex_buffer.is_none() || needs_wire_vertices) {
(0..mesh.verts.len()).map(vertex).collect()
} else {
Vec::new()
};
let vertices_at = profile_enabled.then(iced::time::Instant::now);
let edge_color = first
.set
.visual_style
.as_ref()
.map_or(first.display_color, |style| {
style.edge_color(first.display_color)
});
let edge_key = (source_handle, edge_color.map(f32::to_bits));
let shared_edge_buffer = (first.include_edges && !split_geometry)
.then(|| buffers.edges.get(&edge_key).cloned())
.flatten();
let edge_verts: Vec<_> = if first.include_edges && shared_edge_buffer.is_none() {
(0..source.edge_verts.len())
.map(|index| MeshEdgeVertex {
position: source.edge_verts[index],
color: edge_color,
position_low: source
.edge_verts_low
.get(index)
.copied()
.unwrap_or([0.0; 3]),
})
.collect()
} else {
Vec::new()
};
let has_feature_edges = first.include_edges && !source.edge_verts.is_empty();
let mut wire_indices = Vec::new();
if first.include_edges && !has_feature_edges && !split_geometry {
wire_indices.reserve(first.indices.len() * 2);
for triangle in first.indices.chunks_exact(3) {
wire_indices.extend_from_slice(&[
triangle[0],
triangle[1],
triangle[1],
triangle[2],
triangle[2],
triangle[0],
]);
}
}
let edges_at = profile_enabled.then(iced::time::Instant::now);
let transparent = material_is_transparent(material, color);
let face_indices = first.include_faces.then_some(first.indices.as_ref());
let opaque_indices = if transparent {
&[][..]
} else {
face_indices.unwrap_or(&[])
};
let transparent_indices = if transparent {
face_indices.unwrap_or(&[])
} else {
&[][..]
};
let mut instances = Vec::with_capacity(parts.len());
let mut highlights = Vec::with_capacity(parts.len());
let mut handles = rustc_hash::FxHashSet::default();
let mut bounds = [
f32::INFINITY,
f32::INFINITY,
f32::INFINITY,
f32::NEG_INFINITY,
f32::NEG_INFINITY,
f32::NEG_INFINITY,
];
for part in parts {
let transform = part.set.instance_transform?;
let instance_start = instances.len() as u32;
instances.push(MeshInstanceGpu::from_transform(transform));
bounds[0] = bounds[0].min(part.set.world_aabb[0]);
bounds[1] = bounds[1].min(part.set.world_aabb[1]);
bounds[2] = bounds[2].min(part.set.z_aabb[0]);
bounds[3] = bounds[3].max(part.set.world_aabb[2]);
bounds[4] = bounds[4].max(part.set.world_aabb[3]);
bounds[5] = bounds[5].max(part.set.z_aabb[1]);
if let Some(handle) = part.entity_handle {
handles.insert(handle);
if first.include_faces {
highlights.push(MeshBatchRange {
handle,
index_start: 0,
index_count: first.indices.len() as u32,
transparent,
instance_start,
instance_count: 1,
});
}
}
}
let instances_at = profile_enabled.then(iced::time::Instant::now);
let triangles = if first.include_faces {
(first.indices.len() / 3) as u64 * instances.len() as u64
} else {
0
};
// Upload each source chunk once, then share it across all instance chunks.
let mut geometry = Vec::new();
if split_geometry {
let max_triangles = (super::gpu_budget::max_elements::<MeshVertex>(device) / 3)
.min(super::gpu_budget::max_elements::<u32>(device) / 3)
.min(super::gpu_budget::max_elements::<MeshEdgeVertex>(device) / 6)
.max(1);
if first.include_faces || needs_wire_vertices {
for triangles in first.indices.chunks(max_triangles * 3) {
let vertices: Vec<_> = triangles
.iter()
.map(|index| vertex(*index as usize))
.collect();
let indices: Vec<_> = (0..vertices.len() as u32).collect();
let wire_indices: Vec<_> = if needs_wire_vertices {
indices
.chunks_exact(3)
.flat_map(|t| [t[0], t[1], t[1], t[2], t[2], t[0]])
.collect()
} else {
Vec::new()
};
geometry.push(make_chunk(
device,
queue,
stubs,
&vertices,
if first.include_faces && !transparent {
&indices
} else {
&[]
},
if first.include_faces && transparent {
&indices
} else {
&[]
},
&wire_indices,
&[],
&[],
&[],
&handles,
Some(bounds),
material,
color,
None,
None,
));
}
}
for edges in edge_verts.chunks(super::gpu_budget::max_elements_grouped::<MeshEdgeVertex>(
device, 2,
)) {
geometry.push(make_chunk(
device,
queue,
stubs,
&[],
&[],
&[],
&[],
edges,
&[],
&[],
&handles,
Some(bounds),
material,
color,
None,
None,
));
}
} else {
let mut chunk = make_chunk(
device,
queue,
stubs,
&verts,
opaque_indices,
transparent_indices,
&wire_indices,
&edge_verts,
&[],
&[],
&handles,
Some(bounds),
material,
color,
shared_vertex_buffer,
shared_edge_buffer,
);
if first.include_edges {
// Cached buffers still carry the complete source edge count.
chunk.edge_vertex_count = source.edge_verts.len() as u32;
buffers
.edges
.entry(edge_key)
.or_insert_with(|| chunk.edge_vertex_buffer.clone());
}
buffers
.vertices
.entry(vertex_key)
.or_insert_with(|| chunk.vertex_buffer.clone());
geometry.push(chunk);
}
let mut chunks = Vec::new();
let max_instances = super::gpu_budget::max_elements::<MeshInstanceGpu>(device);
for (group, instance_data) in instances.chunks(max_instances).enumerate() {
let start = group * max_instances;
let end = start + instance_data.len();
let instance_buffer = super::gpu_upload::upload_buffer(
device,
queue,
"mesh.batch.instances",
instance_data,
wgpu::BufferUsages::VERTEX,
);
let group_handles: rustc_hash::FxHashSet<_> = parts[start..end]
.iter()
.filter_map(|part| part.entity_handle)
.collect();
for source_chunk in &geometry {
let mut chunk = source_chunk.clone();
chunk.instance_buffer = instance_buffer.clone();
chunk.instance_count = instance_data.len() as u32;
chunk.handles = group_handles.clone();
let index_count = chunk.index_count + chunk.transp_index_count;
chunk.highlight_ranges = highlights
.iter()
.filter(|range| {
(start..end).contains(&(range.instance_start as usize)) && index_count > 0
})
.map(|range| MeshBatchRange {
instance_start: range.instance_start - start as u32,
index_count,
..*range
})
.collect();
chunks.push(chunk);
}
}
let uploaded_at = profile_enabled.then(iced::time::Instant::now);
let chunk_count = chunks.len();
Some((
chunks,
triangles,
InstancedBuildProfile {
vertices: started
.zip(vertices_at)
.map(|(start, end)| end.duration_since(start))
.unwrap_or_default(),
edges: vertices_at
.zip(edges_at)
.map(|(start, end)| end.duration_since(start))
.unwrap_or_default(),
instances: edges_at
.zip(instances_at)
.map(|(start, end)| end.duration_since(start))
.unwrap_or_default(),
upload: instances_at
.zip(uploaded_at)
.map(|(start, end)| end.duration_since(start))
.unwrap_or_default(),
vertex_count: if split_geometry && (first.include_faces || needs_wire_vertices) {
first.indices.len()
} else {
verts.len()
},
edge_count: edge_verts.len(),
index_count: first.indices.len(),
chunk_count,
},
))
}
fn mesh_bounds(mesh: &MeshModel) -> [f32; 6] {
let mut bounds = [
f32::INFINITY,
f32::INFINITY,
f32::INFINITY,
f32::NEG_INFINITY,
f32::NEG_INFINITY,
f32::NEG_INFINITY,
];
for (index, high) in mesh.verts.iter().copied().enumerate() {
let low = mesh.verts_low.get(index).copied().unwrap_or([0.0; 3]);
for axis in 0..3 {
let value = high[axis] + low[axis];
bounds[axis] = bounds[axis].min(value);
bounds[axis + 3] = bounds[axis + 3].max(value);
}
}
bounds
}
fn material_is_transparent(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
color: [f32; 4],
) -> bool {
color[3] < 0.999
|| material.is_some_and(|material| {
material.translucence > 0.0
|| (material.channel_flags as u32 & 0x08 != 0
&& material.opacity_map.image.is_some())
})
}
fn material_map_uv(
map: &crate::scene::model::material_model::MeshTextureMap,
local_position: [f32; 3],
local_normal: [f32; 3],
local_bounds: [f32; 6],
model_position: [f32; 3],
model_normal: [f32; 3],
model_bounds: [f32; 6],
) -> [f32; 2] {
let (mut position, normal, bounds) = if map.auto_transform & 4 != 0 {
(model_position, model_normal, model_bounds)
} else {
(local_position, local_normal, local_bounds)
};
if map.auto_transform & 2 != 0 {
for axis in 0..3 {
let extent = bounds[axis + 3] - bounds[axis];
position[axis] = if extent.is_finite() && extent.abs() > f32::EPSILON {
(position[axis] - bounds[axis]) / extent
} else {
0.0
};
}
}
let m = &map.transform;
let p = [
position[0] * m[0] + position[1] * m[1] + position[2] * m[2] + m[3],
position[0] * m[4] + position[1] * m[5] + position[2] * m[6] + m[7],
position[0] * m[8] + position[1] * m[9] + position[2] * m[10] + m[11],
];
match map.projection {
3 => [
p[1].atan2(p[0]) / std::f32::consts::TAU + 0.5,
p[2],
],
4 => {
let radius = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
if radius <= f32::EPSILON {
[0.0; 2]
} else {
[
p[1].atan2(p[0]) / std::f32::consts::TAU + 0.5,
(p[2] / radius).clamp(-1.0, 1.0).acos() / std::f32::consts::PI,
]
}
}
2 => {
let n = [normal[0].abs(), normal[1].abs(), normal[2].abs()];
if n[0] >= n[1] && n[0] >= n[2] {
[p[1], p[2]]
} else if n[1] >= n[2] {
[p[0], p[2]]
} else {
[p[0], p[1]]
}
}
_ => [p[0], p[1]],
}
}
fn material_uvs(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
local_position: [f32; 3],
local_normal: [f32; 3],
local_bounds: [f32; 6],
model_position: [f32; 3],
model_normal: [f32; 3],
model_bounds: [f32; 6],
) -> [[f32; 2]; 7] {
let Some(material) = material else {
return [[0.0; 2]; 7];
};
let uv = |map: &crate::scene::model::material_model::MeshTextureMap| {
if map.image.is_none() {
[0.0; 2]
} else {
material_map_uv(
map,
local_position,
local_normal,
local_bounds,
model_position,
model_normal,
model_bounds,
)
}
};
[
uv(&material.diffuse_map),
uv(&material.specular_map),
uv(&material.reflection_map),
uv(&material.opacity_map),
uv(&material.bump_map),
uv(&material.refraction_map),
uv(&material.normal_map),
]
}
fn material_vertex_params(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
) -> ([f32; 4], [f32; 4], [f32; 4], [f32; 4], [u32; 4]) {
let Some(material) = material else {
return (
[0.5, 0.0, 0.0, 0.0],
[0.08, 0.08, 0.08, 1.0],
[0.3, 0.3, 0.3, 0.0],
[1.0; 4],
[0, 127, 0, 0],
);
};
(
[
material.gloss,
material.reflectivity,
material.self_illumination,
material.luminance,
],
[
material.specular[0],
material.specular[1],
material.specular[2],
material.refraction_index,
],
[
material.ambient[0],
material.ambient[1],
material.ambient[2],
material.translucence,
],
[
material.normal_map_strength,
material.indirect_bump_scale,
material.reflectance_scale,
material.transmittance_scale,
],
[
material.illumination_model as u32,
material.channel_flags as u32,
material.mode as u32,
material.luminance_mode as u32,
],
)
}
/// Concatenate every set's first non-empty LOD into a few large GPU buffers.
/// Returns the chunks plus the total triangle count drawn (for diagnostics).
///
/// Every emitted buffer stays under the device's `max_buffer_size` (default
/// 256 MB). Both the vertex buffer (`size_of::<MeshVertex>()` B/vert) and the
/// wire-index buffer (6 u32 = 24 B/triangle — the fattest index buffer) are
/// bounded; a single mesh too large for one chunk is split into triangle-soup
/// sub-chunks so an XREF-heavy model can never overflow a single buffer (#203).
pub fn build_mesh_batch(
device: &wgpu::Device,
queue: &wgpu::Queue,
sets: &[MeshLodSet],
) -> (Vec<MeshBatchChunk>, u64) {
build_mesh_batch_filtered(device, queue, sets, None)
}
pub fn build_mesh_batch_filtered(
device: &wgpu::Device,
queue: &wgpu::Queue,
sets: &[MeshLodSet],
handles: Option<&rustc_hash::FxHashSet<acadrust::Handle>>,
) -> (Vec<MeshBatchChunk>, u64) {
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
// Derive the caps from the real device limit and vertex size. The previous
// fixed 6 M-vertex cap assumed 40 B/vertex, but `position_low` (RTE) grew
// MeshVertex to 52 B, so 6 M × 52 B = 312 MB blew past the 256 MB cap.
let budget = super::gpu_budget::buffer_budget(device);
let max_verts = super::gpu_budget::max_elements::<MeshVertex>(device).max(3);
// A triangle contributes three line segments (six standalone edge
// vertices). Bound chunks by the largest buffer produced for wire meshes.
let max_tris =
(budget / (6 * std::mem::size_of::<MeshEdgeVertex>())).max(1);
let stubs = MeshBatchStubs::new(device, queue);
let mut chunks = Vec::new();
let mut verts: Vec<MeshVertex> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
let mut transp_indices: Vec<u32> = Vec::new();
let mut wire_indices: Vec<u32> = Vec::new();
let mut edge_verts: Vec<MeshEdgeVertex> = Vec::new();
let mut highlight_ranges: Vec<MeshBatchRange> = Vec::new();
let mut chunk_handles: rustc_hash::FxHashSet<acadrust::Handle> =
rustc_hash::FxHashSet::default();
let mut total_tris = 0u64;
let mut ordered: Vec<MeshBatchPart<'_>> = Vec::new();
let mut source_indices: rustc_hash::FxHashMap<
(bool, u64),
(std::sync::Arc<[u32]>, u64),
> = rustc_hash::FxHashMap::default();
let mut face_partitions: rustc_hash::FxHashMap<FacePartitionKey, Vec<CachedFacePart<'_>>> =
rustc_hash::FxHashMap::default();
for set in sets {
let Some(mesh) = set
.geometry_lods()
.iter()
.find(|mesh| !mesh.indices.is_empty())
else {
continue;
};
let display_handle = set.entity_handle();
if handles.is_some_and(|wanted| {
display_handle.is_none_or(|handle| !wanted.contains(&handle))
}) {
continue;
}
let display_color = set.display_color().unwrap_or(mesh.color);
let source_identity = set.instance_source.as_ref().map_or_else(
|| (false, mesh as *const MeshModel as usize as u64),
|source| (true, source.handle.value()),
);
let triangle_count = mesh.indices.len() / 3;
let has_face_materials =
mesh.triangle_material_handles.len() == triangle_count
&& !set.face_materials.is_empty();
let has_face_colors = mesh.triangle_colors.len() == triangle_count
&& mesh.triangle_colors.iter().any(Option::is_some);
let include_faces = set
.visual_style
.as_ref()
.map_or(true, |style| style.face_visible());
let include_edges = set
.visual_style
.as_ref()
.map_or(true, |style| style.edges_visible());
if !has_face_materials && !has_face_colors {
let base_color =
set.material.as_ref().map_or(display_color, |material| material.diffuse);
let color = set
.visual_style
.as_ref()
.map_or(base_color, |style| style.face_color(base_color));
let (shared_indices, shared_hash) = source_indices
.entry(source_identity)
.or_insert_with(|| {
let indices = optimize_triangle_indices(&mesh.indices, mesh.verts.len());
let hash = index_hash(indices.as_ref());
(indices, hash)
})
.clone();
ordered.push(MeshBatchPart {
set,
mesh,
uv_mesh: mesh,
entity_handle: display_handle,
display_color,
material: set.material.as_ref(),
color,
indices: shared_indices,
index_hash: shared_hash,
include_faces,
include_edges,
part_slot: 0,
});
continue;
}
let mut face_materials: Vec<_> = set
.face_materials
.iter()
.map(|(handle, material)| {
(
handle.value(),
material_key(Some(material), material.diffuse),
)
})
.collect();
face_materials.sort_by_key(|(handle, _)| *handle);
let partition_key = FacePartitionKey {
instanced: source_identity.0,
source: source_identity.1,
base_material: material_key(set.material.as_ref(), display_color),
face_materials,
visual_style: visual_style_partition_key(set.visual_style.as_ref()),
display_color: display_color.map(f32::to_bits),
include_faces,
include_edges,
};
let cached_parts = if let Some(parts) = face_partitions.get(&partition_key) {
parts.clone()
} else {
let mut groups: std::collections::BTreeMap<
MaterialBatchKey,
(
Option<&crate::scene::model::material_model::MeshMaterial>,
[f32; 4],
Vec<u32>,
),
> = std::collections::BTreeMap::new();
for (triangle, indices) in mesh.indices.chunks_exact(3).enumerate() {
let material = if has_face_materials {
mesh.triangle_material_handles[triangle]
.and_then(|handle| set.face_materials.get(&handle))
.or(set.material.as_ref())
} else {
set.material.as_ref()
};
let base_color =
material.map_or(display_color, |material| material.diffuse);
let base_color = if has_face_colors {
mesh.triangle_colors[triangle].unwrap_or(base_color)
} else {
base_color
};
let color = set
.visual_style
.as_ref()
.map_or(base_color, |style| style.face_color(base_color));
groups
.entry(material_key(material, color))
.or_insert_with(|| (material, color, Vec::new()))
.2
.extend_from_slice(indices);
}
let parts: Vec<_> = groups
.into_values()
.map(|(material, color, indices)| {
let indices = optimize_triangle_indices(&indices, mesh.verts.len());
let index_hash = index_hash(indices.as_ref());
CachedFacePart {
material,
color,
indices,
index_hash,
}
})
.collect();
face_partitions.insert(partition_key, parts.clone());
parts
};
for (part_index, part) in cached_parts.into_iter().enumerate() {
ordered.push(MeshBatchPart {
set,
mesh,
uv_mesh: mesh,
entity_handle: display_handle,
display_color,
material: part.material,
color: part.color,
indices: part.indices,
index_hash: part.index_hash,
include_faces,
include_edges: include_edges && part_index == 0,
part_slot: part_index as u32,
});
}
}
let prepared_at = iced::time::Instant::now();
let spatial_bounds = sets.iter().fold(
[
f32::INFINITY,
f32::INFINITY,
f32::INFINITY,
f32::NEG_INFINITY,
f32::NEG_INFINITY,
f32::NEG_INFINITY,
],
|mut bounds, set| {
bounds[0] = bounds[0].min(set.world_aabb[0]);
bounds[1] = bounds[1].min(set.world_aabb[1]);
bounds[2] = bounds[2].min(set.z_aabb[0]);
bounds[3] = bounds[3].max(set.world_aabb[2]);
bounds[4] = bounds[4].max(set.world_aabb[3]);
bounds[5] = bounds[5].max(set.z_aabb[1]);
bounds
},
);
ordered.sort_by_key(|part| {
(
material_key(part.material, part.color),
mesh_spatial_key(part.set, spatial_bounds),
)
});
let mut instance_groups: std::collections::BTreeMap<
InstanceGroupKey,
Vec<MeshBatchPart<'_>>,
> = std::collections::BTreeMap::new();
let mut direct_parts = Vec::with_capacity(ordered.len());
for part in ordered {
let eligible = part.set.instance_transform.is_some()
&& part.set.instance_source.is_some()
&& part
.indices
.iter()
.all(|index| (*index as usize) < part.mesh.verts.len());
if eligible {
let source = part
.set
.instance_source
.as_ref()
.expect("checked above")
.handle
.value();
instance_groups
.entry(InstanceGroupKey {
material: material_key(part.material, part.color),
source,
part_slot: part.part_slot,
index_count: part.indices.len(),
index_hash: part.index_hash,
include_faces: part.include_faces,
include_edges: part.include_edges,
})
.or_default()
.push(part);
} else {
direct_parts.push(part);
}
}
direct_parts.sort_by_key(|part| {
(
material_key(part.material, part.color),
mesh_spatial_key(part.set, spatial_bounds),
)
});
let grouped_at = iced::time::Instant::now();
let ordered = direct_parts;
let mut active_key: Option<MaterialBatchKey> = None;
let mut active_material: Option<&crate::scene::model::material_model::MeshMaterial> = None;
let mut active_color = [1.0; 4];
for part in ordered {
let set = part.set;
let mesh = part.mesh;
let material = part.material;
let part_color = part.color;
let entity_handle = part.entity_handle;
let key = material_key(material, part_color);
if active_key.is_some_and(|active| active != key)
&& (!verts.is_empty() || !edge_verts.is_empty())
{
chunks.push(make_chunk(
device,
queue,
&stubs,
&verts,
&indices,
&transp_indices,
&wire_indices,
&edge_verts,
&highlight_ranges,
&[],
&chunk_handles,
None,
active_material,
active_color,
None,
None,
));
verts.clear();
indices.clear();
transp_indices.clear();
wire_indices.clear();
edge_verts.clear();
highlight_ranges.clear();
chunk_handles.clear();
}
active_key = Some(key);
active_material = material;
active_color = part_color;
if let Some(handle) = entity_handle {
chunk_handles.insert(handle);
}
let has_normals = mesh.normals.len() == mesh.verts.len();
let uv_mesh = part.uv_mesh;
let local_bounds = mesh_bounds(uv_mesh);
let model_bounds = mesh_bounds(mesh);
let edge_color = set
.visual_style
.as_ref()
.map_or(part.display_color, |style| style.edge_color(part.display_color));
let vtx = |vi: usize| {
let normal = if has_normals {
mesh.normals[vi]
} else {
[0.0, 1.0, 0.0]
};
let local_position = uv_mesh
.verts
.get(vi)
.copied()
.unwrap_or(mesh.verts[vi]);
let local_normal = uv_mesh
.normals
.get(vi)
.copied()
.unwrap_or(normal);
let uv = material_uvs(
material,
local_position,
local_normal,
local_bounds,
mesh.verts[vi],
normal,
model_bounds,
);
MeshVertex {
position: mesh.verts[vi],
normal,
position_low: mesh.verts_low.get(vi).copied().unwrap_or([0.0; 3]),
uv_diffuse: uv[0],
uv_specular: uv[1],
uv_reflection: uv[2],
uv_opacity: uv[3],
uv_bump: uv[4],
uv_refraction: uv[5],
uv_normal: uv[6],
}
};
// A solid whose baked colour is not fully opaque routes into the
// transparent index stream so it is drawn last, without depth writes.
let is_transp = material_is_transparent(material, part_color);
let mesh_tris = part.indices.len() / 3;
if part.include_faces {
total_tris += mesh_tris as u64;
}
// Feature edges present (ACIS solid) → emit the B-rep edges as a line
// list and skip the triangulation wireframe. Absent (plain mesh) → keep
// the triangle edges so the mesh still shows a wireframe.
let has_feat = !set.edge_verts.is_empty();
if has_feat && part.include_edges {
// Feature edges use their own vertex buffer, so they need their
// own cap. Large ACIS models can have relatively few faces but
// millions of B-rep edge vertices; only checking `mesh.verts`
// allowed `edge_vbuf` to exceed wgpu's max_buffer_size.
let mut edge_start = 0;
let edge_end = set.edge_verts.len() & !1usize;
while edge_start < edge_end {
let available = max_verts.saturating_sub(edge_verts.len());
// LineList consumes pairs. Never split a segment between
// chunks even when the vertex budget is odd.
let take = available
.min(edge_end - edge_start)
& !1usize;
if take == 0 {
chunks.push(make_chunk(
device,
queue,
&stubs,
&verts,
&indices,
&transp_indices,
&wire_indices,
&edge_verts,
&highlight_ranges,
&[],
&chunk_handles,
None,
active_material,
active_color,
None,
None,
));
verts.clear();
indices.clear();
transp_indices.clear();
wire_indices.clear();
edge_verts.clear();
highlight_ranges.clear();
chunk_handles.clear();
if let Some(handle) = entity_handle {
chunk_handles.insert(handle);
}
continue;
}
for i in edge_start..edge_start + take {
edge_verts.push(MeshEdgeVertex {
position: set.edge_verts[i],
color: edge_color,
position_low: set.edge_verts_low.get(i).copied().unwrap_or([0.0; 3]),
});
}
edge_start += take;
if edge_start < edge_end {
chunks.push(make_chunk(
device,
queue,
&stubs,
&verts,
&indices,
&transp_indices,
&wire_indices,
&edge_verts,
&highlight_ranges,
&[],
&chunk_handles,
None,
active_material,
active_color,
None,
None,
));
verts.clear();
indices.clear();
transp_indices.clear();
wire_indices.clear();
edge_verts.clear();
highlight_ranges.clear();
chunk_handles.clear();
if let Some(handle) = entity_handle {
chunk_handles.insert(handle);
}
}
}
}
// A single mesh larger than a whole chunk: emit as triangle-soup
// sub-chunks (corners expanded, no vertex sharing) so each buffer fits.
if mesh.verts.len() > max_verts || mesh_tris > max_tris {
if !verts.is_empty() || !edge_verts.is_empty() {
chunks.push(make_chunk(
device,
queue,
&stubs,
&verts,
&indices,
&transp_indices,
&wire_indices,
&edge_verts,
&highlight_ranges,
&[],
&chunk_handles,
None,
active_material,
active_color,
None,
None,
));
verts.clear();
indices.clear();
transp_indices.clear();
wire_indices.clear();
edge_verts.clear();
highlight_ranges.clear();
chunk_handles.clear();
if let Some(handle) = entity_handle {
chunk_handles.insert(handle);
}
}
let tris_per = (max_verts / 3).min(max_tris).max(1);
let mut t = 0;
while t < mesh_tris {
let end = (t + tris_per).min(mesh_tris);
let (mut sv, mut si, mut swi) = (Vec::new(), Vec::new(), Vec::new());
for tri in t..end {
let ix = &part.indices[tri * 3..tri * 3 + 3];
let b = sv.len() as u32;
sv.push(vtx(ix[0] as usize));
sv.push(vtx(ix[1] as usize));
sv.push(vtx(ix[2] as usize));
if part.include_faces {
si.extend_from_slice(&[b, b + 1, b + 2]);
}
if part.include_edges && !has_feat {
swi.extend_from_slice(&[b, b + 1, b + 1, b + 2, b + 2, b]);
}
}
// The whole mesh shares one colour, so a sub-chunk is entirely
// opaque or entirely transparent.
let sub_ranges: Vec<MeshBatchRange> = if part.include_faces {
entity_handle
.map(|handle| {
vec![MeshBatchRange {
handle,
index_start: 0,
index_count: si.len() as u32,
transparent: is_transp,
instance_start: 0,
instance_count: 1,
}]
})
.unwrap_or_default()
} else {
Vec::new()
};
if is_transp {
let sub_handles: rustc_hash::FxHashSet<_> =
entity_handle.into_iter().collect();
chunks.push(make_chunk(
device,
queue,
&stubs,
&sv,
&[],
&si,
&swi,
&[],
&sub_ranges,
&[],
&sub_handles,
None,
active_material,
active_color,
None,
None,
));
} else {
let sub_handles: rustc_hash::FxHashSet<_> =
entity_handle.into_iter().collect();
chunks.push(make_chunk(
device,
queue,
&stubs,
&sv,
&si,
&[],
&swi,
&[],
&sub_ranges,
&[],
&sub_handles,
None,
active_material,
active_color,
None,
None,
));
}
t = end;
}
chunk_handles.clear();
continue;
}
// Flush when adding this mesh would overflow either the surface vertex
// buffer or the expanded wire-vertex buffer.
if !verts.is_empty()
&& (verts.len() + mesh.verts.len() > max_verts
|| wire_indices.len() / 6 + mesh_tris > max_tris)
{
chunks.push(make_chunk(
device,
queue,
&stubs,
&verts,
&indices,
&transp_indices,
&wire_indices,
&edge_verts,
&highlight_ranges,
&[],
&chunk_handles,
None,
active_material,
active_color,
None,
None,
));
verts.clear();
indices.clear();
transp_indices.clear();
wire_indices.clear();
edge_verts.clear();
highlight_ranges.clear();
chunk_handles.clear();
if let Some(handle) = entity_handle {
chunk_handles.insert(handle);
}
}
let base = verts.len() as u32;
for i in 0..mesh.verts.len() {
verts.push(vtx(i));
}
if part.include_faces {
let fill = if is_transp { &mut transp_indices } else { &mut indices };
let index_start = fill.len() as u32;
for &idx in part.indices.iter() {
fill.push(base + idx);
}
if let Some(handle) = entity_handle {
highlight_ranges.push(MeshBatchRange {
handle,
index_start,
index_count: part.indices.len() as u32,
transparent: is_transp,
instance_start: 0,
instance_count: 1,
});
}
}
if part.include_edges && !has_feat {
for tri in part.indices.chunks_exact(3) {
let (a, b, c) = (base + tri[0], base + tri[1], base + tri[2]);
wire_indices.extend_from_slice(&[a, b, b, c, c, a]);
}
}
}
if !indices.is_empty()
|| !transp_indices.is_empty()
|| !wire_indices.is_empty()
|| !edge_verts.is_empty()
{
chunks.push(make_chunk(
device,
queue,
&stubs,
&verts,
&indices,
&transp_indices,
&wire_indices,
&edge_verts,
&highlight_ranges,
&[],
&chunk_handles,
None,
active_material,
active_color,
None,
None,
));
}
let direct_at = iced::time::Instant::now();
let mut instanced_profile = InstancedBuildProfile::default();
let mut instanced_buffers = InstancedBufferCache::default();
for parts in instance_groups.values() {
if let Some((instanced, triangles, profile)) = build_instanced_chunks(
device,
queue,
&stubs,
&mut instanced_buffers,
parts,
perf_started.is_some(),
) {
total_tris += triangles;
chunks.extend(instanced);
instanced_profile.vertices += profile.vertices;
instanced_profile.edges += profile.edges;
instanced_profile.instances += profile.instances;
instanced_profile.upload += profile.upload;
instanced_profile.vertex_count += profile.vertex_count;
instanced_profile.edge_count += profile.edge_count;
instanced_profile.index_count += profile.index_count;
instanced_profile.chunk_count += profile.chunk_count;
}
}
if let Some(started) = perf_started {
let done = iced::time::Instant::now();
crate::perf_record!(
"[perf] mesh-build-detail prepare={:.1} group={:.1} direct={:.1} instanced={:.1}",
prepared_at.duration_since(started).as_secs_f64() * 1000.0,
grouped_at.duration_since(prepared_at).as_secs_f64() * 1000.0,
direct_at.duration_since(grouped_at).as_secs_f64() * 1000.0,
done.duration_since(direct_at).as_secs_f64() * 1000.0,
);
crate::perf_record!(
"[perf] mesh-instanced vertices={:.1} edges={:.1} instances={:.1} upload={:.1} chunks={} verts={} edge_verts={} indices={}",
instanced_profile.vertices.as_secs_f64() * 1000.0,
instanced_profile.edges.as_secs_f64() * 1000.0,
instanced_profile.instances.as_secs_f64() * 1000.0,
instanced_profile.upload.as_secs_f64() * 1000.0,
instanced_profile.chunk_count,
instanced_profile.vertex_count,
instanced_profile.edge_count,
instanced_profile.index_count,
);
}
(chunks, total_tris)
}
#[cfg(test)]
mod texture_limit_tests {
use super::downscale_rgba_to_limit;
#[test]
fn downscale_keeps_both_dimensions_within_the_limit() {
// A wide-and-short image: the limit is only crossed by width, but
// both dimensions must come out <= limit and aspect ratio preserved.
let (w, h) = (500u32, 10u32);
let rgba = vec![0u8; (w * h * 4) as usize];
let (new_w, new_h, buf) = downscale_rgba_to_limit(w, h, &rgba, 200).expect("well-formed input must resize");
assert!(new_w <= 200 && new_h <= 200, "both dimensions must respect the limit, got {new_w}x{new_h}");
assert_eq!(buf.len(), (new_w * new_h * 4) as usize, "the returned buffer must match its reported dimensions");
// Aspect ratio 50:1 should be preserved within rounding.
let ratio = new_w as f64 / new_h as f64;
assert!((ratio - 50.0).abs() < 1.0, "expected aspect ratio near 50:1, got {ratio}");
}
#[test]
fn downscale_handles_a_square_image_over_the_limit() {
let (w, h) = (300u32, 300u32);
let rgba = vec![255u8; (w * h * 4) as usize];
let (new_w, new_h, buf) = downscale_rgba_to_limit(w, h, &rgba, 128).expect("well-formed input must resize");
assert_eq!(new_w, 128);
assert_eq!(new_h, 128);
assert_eq!(buf.len(), (128 * 128 * 4) as usize);
}
#[test]
fn downscale_rejects_a_buffer_whose_length_does_not_match_its_dimensions() {
// Corrupt/mismatched input (audit's "validate decoded byte length" ask):
// must fail cleanly, not panic or silently misread the buffer.
let rgba = vec![0u8; 10];
assert!(downscale_rgba_to_limit(1000, 1000, &rgba, 512).is_none());
}
#[test]
fn malformed_material_images_are_rejected_even_below_the_texture_limit() {
for (width, height, pixels) in [(0, 1, vec![]), (1, 0, vec![]), (1, 1, vec![0; 3]), (1, 1, vec![0; 5])] {
assert!(!super::valid_rgba_size(width, height, &pixels));
assert!(downscale_rgba_to_limit(width, height, &pixels, 16).is_none());
}
assert!(downscale_rgba_to_limit(1, 1, &[0; 4], 0).is_none());
}
}