Skip to content

Commit 1c02564

Browse files
authored
Don't keep around additional cpu copy of loaded mesh files (#7824)
### What This is mainly a refactor of `re_renderer`'s model loading pipeline, but as the title (== changelog entry) points out, a nice side-effect that arose culling unnecessary memory usage which may be important for large meshes. @EtaLoop's attempt to add a color option to `Asset3D` (see #7458) made it clear that the output of the mesh loaders is really hard to work with: Prior to this PR, they would eagerly create gpu-sided meshes and then store them alongside an optional (but always filled-out) "cpu mesh" (in essence the unpacked model file). Now instead all model loading goes to a intermediate `CpuModel` which can be rather easily post processed. Gpu resources are then created as a separate step, consuming the `CpuModel` (it should be trivial to create a variant that doesn't consume if we need this in the future) ### Checklist * [x] I have read and agree to [Contributor Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and the [Code of Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md) * [x] I've included a screenshot or gif (if applicable) * [x] I have tested the web demo (if applicable): * Using examples from latest `main` build: [rerun.io/viewer](https://rerun.io/viewer/pr/7824?manifest_url=https://app.rerun.io/version/main/examples_manifest.json) * Using full set of examples from `nightly` build: [rerun.io/viewer](https://rerun.io/viewer/pr/7824?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json) * [x] The PR title and labels are set such as to maximize their usefulness for the next release's CHANGELOG * [x] If applicable, add a new check to the [release checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)! * [x] If have noted any breaking changes to the log API in `CHANGELOG.md` and the migration guide - [PR Build Summary](https://build.rerun.io/pr/7824) - [Recent benchmark results](https://build.rerun.io/graphs/crates.html) - [Wasm size tracking](https://build.rerun.io/graphs/sizes.html) To run all checks from `main`, comment on the PR with `@rerun-bot full-check`.
1 parent de4c389 commit 1c02564

File tree

18 files changed

+252
-203
lines changed

18 files changed

+252
-203
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use std::sync::Arc;
2+
3+
use slotmap::{SecondaryMap, SlotMap};
4+
5+
use crate::{
6+
mesh::{CpuMesh, GpuMesh, MeshError},
7+
renderer::GpuMeshInstance,
8+
RenderContext,
9+
};
10+
11+
slotmap::new_key_type! {
12+
/// Key for identifying a cpu mesh in a model.
13+
pub struct CpuModelMeshKey;
14+
}
15+
16+
/// Like [`GpuMeshInstance`], but for CPU sided usage in a [`CpuModel`] only.
17+
pub struct CpuMeshInstance {
18+
pub mesh: CpuModelMeshKey,
19+
pub world_from_mesh: glam::Affine3A,
20+
// TODO(andreas): Expose other properties we have on [`GpuMeshInstance`].
21+
}
22+
23+
/// A collection of meshes & mesh instances on the CPU.
24+
///
25+
/// Note that there is currently no `GpuModel` equivalent, since
26+
/// [`GpuMeshInstance`]es use shared ownership of [`GpuMesh`]es.
27+
///
28+
/// This is the output of a model loader and is ready to be converted into
29+
/// a series of [`GpuMeshInstance`]s that can be rendered.
30+
///
31+
/// This is meant as a useful intermediate structure for doing post-processing steps on the model prior to gpu upload.
32+
#[derive(Default)]
33+
pub struct CpuModel {
34+
pub meshes: SlotMap<CpuModelMeshKey, CpuMesh>,
35+
pub instances: Vec<CpuMeshInstance>,
36+
}
37+
38+
impl CpuModel {
39+
/// Creates a new [`CpuModel`] from a single [`CpuMesh`], creating a single instance with identity transform.
40+
pub fn from_single_mesh(mesh: CpuMesh) -> Self {
41+
let mut model = Self::default();
42+
model.add_single_instance_mesh(mesh);
43+
model
44+
}
45+
46+
/// Adds a new [`CpuMesh`] to the model, creating a single instance with identity transform.
47+
pub fn add_single_instance_mesh(&mut self, mesh: CpuMesh) {
48+
let mesh_key = self.meshes.insert(mesh);
49+
self.instances.push(CpuMeshInstance {
50+
mesh: mesh_key,
51+
world_from_mesh: glam::Affine3A::IDENTITY,
52+
});
53+
}
54+
55+
pub fn calculate_bounding_box(&self) -> re_math::BoundingBox {
56+
re_math::BoundingBox::from_points(
57+
self.instances
58+
.iter()
59+
.filter_map(|mesh_instance| {
60+
self.meshes.get(mesh_instance.mesh).map(|mesh| {
61+
mesh.vertex_positions
62+
.iter()
63+
.map(|p| mesh_instance.world_from_mesh.transform_point3(*p))
64+
})
65+
})
66+
.flatten(),
67+
)
68+
}
69+
70+
/// Converts the entire model into a serious of mesh instances that can be rendered.
71+
///
72+
/// Silently ignores:
73+
/// * instances with invalid mesh keys
74+
/// * unreferenced meshes
75+
pub fn into_gpu_meshes(self, ctx: &RenderContext) -> Result<Vec<GpuMeshInstance>, MeshError> {
76+
let mut gpu_meshes = SecondaryMap::with_capacity(self.meshes.len());
77+
for (mesh_key, mesh) in &self.meshes {
78+
gpu_meshes.insert(mesh_key, Arc::new(GpuMesh::new(ctx, mesh)?));
79+
}
80+
81+
Ok(self
82+
.instances
83+
.into_iter()
84+
.filter_map(|instance| {
85+
Some(GpuMeshInstance {
86+
gpu_mesh: gpu_meshes.get(instance.mesh)?.clone(),
87+
world_from_mesh: instance.world_from_mesh,
88+
additive_tint: Default::default(),
89+
outline_mask_ids: Default::default(),
90+
picking_layer_id: Default::default(),
91+
})
92+
})
93+
.collect())
94+
}
95+
}

crates/viewer/re_renderer/src/importer/gltf.rs

+23-23
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
use std::sync::Arc;
2-
31
use ahash::{HashMap, HashMapExt};
42
use gltf::texture::WrappingMode;
53
use itertools::Itertools;
64
use smallvec::SmallVec;
75

86
use crate::{
9-
mesh::{GpuMesh, Material, Mesh, MeshError},
10-
renderer::MeshInstance,
7+
mesh::{CpuMesh, Material, MeshError},
118
resource_managers::{GpuTexture2D, ImageDataDesc, TextureManager2D},
12-
RenderContext, Rgba32Unmul,
9+
CpuMeshInstance, CpuModel, CpuModelMeshKey, RenderContext, Rgba32Unmul,
1310
};
1411

1512
#[derive(thiserror::Error, Debug)]
@@ -41,7 +38,7 @@ pub fn load_gltf_from_buffer(
4138
mesh_name: &str,
4239
buffer: &[u8],
4340
ctx: &RenderContext,
44-
) -> Result<Vec<MeshInstance>, GltfImportError> {
41+
) -> Result<CpuModel, GltfImportError> {
4542
re_tracing::profile_function!();
4643

4744
let (doc, buffers, images) = {
@@ -105,25 +102,28 @@ pub fn load_gltf_from_buffer(
105102
});
106103
}
107104

108-
let mut meshes = HashMap::with_capacity(doc.meshes().len());
105+
let mut re_model = CpuModel::default();
106+
let mut mesh_keys = HashMap::with_capacity(doc.meshes().len());
109107
for ref mesh in doc.meshes() {
110108
re_tracing::profile_scope!("mesh");
111109

112110
let re_mesh = import_mesh(mesh, &buffers, &images_as_textures, &ctx.texture_manager_2d)?;
113-
meshes.insert(
114-
mesh.index(),
115-
(Arc::new(GpuMesh::new(ctx, &re_mesh)?), Arc::new(re_mesh)),
116-
);
111+
let re_mesh_key = re_model.meshes.insert(re_mesh);
112+
mesh_keys.insert(mesh.index(), re_mesh_key);
117113
}
118114

119-
let mut instances = Vec::new();
120115
for scene in doc.scenes() {
121116
for node in scene.nodes() {
122-
gather_instances_recursive(&mut instances, &node, &glam::Affine3A::IDENTITY, &meshes);
117+
gather_instances_recursive(
118+
&mut re_model.instances,
119+
&node,
120+
&glam::Affine3A::IDENTITY,
121+
&mesh_keys,
122+
);
123123
}
124124
}
125125

126-
Ok(instances)
126+
Ok(re_model)
127127
}
128128

129129
fn map_format(format: gltf::image::Format) -> Option<wgpu::TextureFormat> {
@@ -154,7 +154,7 @@ fn import_mesh(
154154
buffers: &[gltf::buffer::Data],
155155
gpu_image_handles: &[GpuTexture2D],
156156
texture_manager: &TextureManager2D, //imported_materials: HashMap<usize, Material>,
157-
) -> Result<Mesh, GltfImportError> {
157+
) -> Result<CpuMesh, GltfImportError> {
158158
re_tracing::profile_function!();
159159

160160
let mesh_name = mesh.name().map_or("<unknown", |f| f).to_owned();
@@ -277,7 +277,7 @@ fn import_mesh(
277277
return Err(GltfImportError::NoTrianglePrimitives { mesh_name });
278278
}
279279

280-
let mesh = Mesh {
280+
let mesh = CpuMesh {
281281
label: mesh.name().into(),
282282
triangle_indices,
283283
vertex_positions,
@@ -293,10 +293,10 @@ fn import_mesh(
293293
}
294294

295295
fn gather_instances_recursive(
296-
instances: &mut Vec<MeshInstance>,
296+
instances: &mut Vec<CpuMeshInstance>,
297297
node: &gltf::Node<'_>,
298298
transform: &glam::Affine3A,
299-
meshes: &HashMap<usize, (Arc<GpuMesh>, Arc<Mesh>)>,
299+
meshes: &HashMap<usize, CpuModelMeshKey>,
300300
) {
301301
let (scale, rotation, translation) = match node.transform() {
302302
gltf::scene::Transform::Matrix { matrix } => {
@@ -324,11 +324,11 @@ fn gather_instances_recursive(
324324
}
325325

326326
if let Some(mesh) = node.mesh() {
327-
if let Some((gpu_mesh, mesh)) = meshes.get(&mesh.index()) {
328-
let mut gpu_mesh =
329-
MeshInstance::new_with_cpu_mesh(gpu_mesh.clone(), Some(mesh.clone()));
330-
gpu_mesh.world_from_mesh = transform;
331-
instances.push(gpu_mesh);
327+
if let Some(mesh_key) = meshes.get(&mesh.index()) {
328+
instances.push(CpuMeshInstance {
329+
mesh: *mesh_key,
330+
world_from_mesh: transform,
331+
});
332332
}
333333
}
334334
}
+3-28
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
mod cpu_model;
2+
13
#[cfg(feature = "import-obj")]
24
pub mod obj;
35

@@ -7,31 +9,4 @@ pub mod gltf;
79
#[cfg(feature = "import-stl")]
810
pub mod stl;
911

10-
use re_math::Vec3Ext as _;
11-
12-
use crate::renderer::MeshInstance;
13-
14-
pub fn to_uniform_scale(scale: glam::Vec3) -> f32 {
15-
if scale.has_equal_components(0.001) {
16-
scale.x
17-
} else {
18-
let uniform_scale = (scale.x * scale.y * scale.z).cbrt();
19-
re_log::warn!("mesh has non-uniform scale ({:?}). This is currently not supported. Using geometric mean {}", scale,uniform_scale);
20-
uniform_scale
21-
}
22-
}
23-
24-
pub fn calculate_bounding_box(instances: &[MeshInstance]) -> re_math::BoundingBox {
25-
re_math::BoundingBox::from_points(
26-
instances
27-
.iter()
28-
.filter_map(|mesh_instance| {
29-
mesh_instance.mesh.as_ref().map(|mesh| {
30-
mesh.vertex_positions
31-
.iter()
32-
.map(|p| mesh_instance.world_from_mesh.transform_point3(*p))
33-
})
34-
})
35-
.flatten(),
36-
)
37-
}
12+
pub use cpu_model::{CpuMeshInstance, CpuModel, CpuModelMeshKey};

0 commit comments

Comments
 (0)