Skip to content

Commit

Permalink
Don't keep around additional cpu copy of loaded mesh files (#7824)
Browse files Browse the repository at this point in the history
### 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`.
  • Loading branch information
Wumpf authored Oct 22, 2024
1 parent de4c389 commit 1c02564
Show file tree
Hide file tree
Showing 18 changed files with 252 additions and 203 deletions.
95 changes: 95 additions & 0 deletions crates/viewer/re_renderer/src/importer/cpu_model.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use std::sync::Arc;

use slotmap::{SecondaryMap, SlotMap};

use crate::{
mesh::{CpuMesh, GpuMesh, MeshError},
renderer::GpuMeshInstance,
RenderContext,
};

slotmap::new_key_type! {
/// Key for identifying a cpu mesh in a model.
pub struct CpuModelMeshKey;
}

/// Like [`GpuMeshInstance`], but for CPU sided usage in a [`CpuModel`] only.
pub struct CpuMeshInstance {
pub mesh: CpuModelMeshKey,
pub world_from_mesh: glam::Affine3A,
// TODO(andreas): Expose other properties we have on [`GpuMeshInstance`].
}

/// A collection of meshes & mesh instances on the CPU.
///
/// Note that there is currently no `GpuModel` equivalent, since
/// [`GpuMeshInstance`]es use shared ownership of [`GpuMesh`]es.
///
/// This is the output of a model loader and is ready to be converted into
/// a series of [`GpuMeshInstance`]s that can be rendered.
///
/// This is meant as a useful intermediate structure for doing post-processing steps on the model prior to gpu upload.
#[derive(Default)]
pub struct CpuModel {
pub meshes: SlotMap<CpuModelMeshKey, CpuMesh>,
pub instances: Vec<CpuMeshInstance>,
}

impl CpuModel {
/// Creates a new [`CpuModel`] from a single [`CpuMesh`], creating a single instance with identity transform.
pub fn from_single_mesh(mesh: CpuMesh) -> Self {
let mut model = Self::default();
model.add_single_instance_mesh(mesh);
model
}

/// Adds a new [`CpuMesh`] to the model, creating a single instance with identity transform.
pub fn add_single_instance_mesh(&mut self, mesh: CpuMesh) {
let mesh_key = self.meshes.insert(mesh);
self.instances.push(CpuMeshInstance {
mesh: mesh_key,
world_from_mesh: glam::Affine3A::IDENTITY,
});
}

pub fn calculate_bounding_box(&self) -> re_math::BoundingBox {
re_math::BoundingBox::from_points(
self.instances
.iter()
.filter_map(|mesh_instance| {
self.meshes.get(mesh_instance.mesh).map(|mesh| {
mesh.vertex_positions
.iter()
.map(|p| mesh_instance.world_from_mesh.transform_point3(*p))
})
})
.flatten(),
)
}

/// Converts the entire model into a serious of mesh instances that can be rendered.
///
/// Silently ignores:
/// * instances with invalid mesh keys
/// * unreferenced meshes
pub fn into_gpu_meshes(self, ctx: &RenderContext) -> Result<Vec<GpuMeshInstance>, MeshError> {
let mut gpu_meshes = SecondaryMap::with_capacity(self.meshes.len());
for (mesh_key, mesh) in &self.meshes {
gpu_meshes.insert(mesh_key, Arc::new(GpuMesh::new(ctx, mesh)?));
}

Ok(self
.instances
.into_iter()
.filter_map(|instance| {
Some(GpuMeshInstance {
gpu_mesh: gpu_meshes.get(instance.mesh)?.clone(),
world_from_mesh: instance.world_from_mesh,
additive_tint: Default::default(),
outline_mask_ids: Default::default(),
picking_layer_id: Default::default(),
})
})
.collect())
}
}
46 changes: 23 additions & 23 deletions crates/viewer/re_renderer/src/importer/gltf.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
use std::sync::Arc;

use ahash::{HashMap, HashMapExt};
use gltf::texture::WrappingMode;
use itertools::Itertools;
use smallvec::SmallVec;

use crate::{
mesh::{GpuMesh, Material, Mesh, MeshError},
renderer::MeshInstance,
mesh::{CpuMesh, Material, MeshError},
resource_managers::{GpuTexture2D, ImageDataDesc, TextureManager2D},
RenderContext, Rgba32Unmul,
CpuMeshInstance, CpuModel, CpuModelMeshKey, RenderContext, Rgba32Unmul,
};

#[derive(thiserror::Error, Debug)]
Expand Down Expand Up @@ -41,7 +38,7 @@ pub fn load_gltf_from_buffer(
mesh_name: &str,
buffer: &[u8],
ctx: &RenderContext,
) -> Result<Vec<MeshInstance>, GltfImportError> {
) -> Result<CpuModel, GltfImportError> {
re_tracing::profile_function!();

let (doc, buffers, images) = {
Expand Down Expand Up @@ -105,25 +102,28 @@ pub fn load_gltf_from_buffer(
});
}

let mut meshes = HashMap::with_capacity(doc.meshes().len());
let mut re_model = CpuModel::default();
let mut mesh_keys = HashMap::with_capacity(doc.meshes().len());
for ref mesh in doc.meshes() {
re_tracing::profile_scope!("mesh");

let re_mesh = import_mesh(mesh, &buffers, &images_as_textures, &ctx.texture_manager_2d)?;
meshes.insert(
mesh.index(),
(Arc::new(GpuMesh::new(ctx, &re_mesh)?), Arc::new(re_mesh)),
);
let re_mesh_key = re_model.meshes.insert(re_mesh);
mesh_keys.insert(mesh.index(), re_mesh_key);
}

let mut instances = Vec::new();
for scene in doc.scenes() {
for node in scene.nodes() {
gather_instances_recursive(&mut instances, &node, &glam::Affine3A::IDENTITY, &meshes);
gather_instances_recursive(
&mut re_model.instances,
&node,
&glam::Affine3A::IDENTITY,
&mesh_keys,
);
}
}

Ok(instances)
Ok(re_model)
}

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

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

let mesh = Mesh {
let mesh = CpuMesh {
label: mesh.name().into(),
triangle_indices,
vertex_positions,
Expand All @@ -293,10 +293,10 @@ fn import_mesh(
}

fn gather_instances_recursive(
instances: &mut Vec<MeshInstance>,
instances: &mut Vec<CpuMeshInstance>,
node: &gltf::Node<'_>,
transform: &glam::Affine3A,
meshes: &HashMap<usize, (Arc<GpuMesh>, Arc<Mesh>)>,
meshes: &HashMap<usize, CpuModelMeshKey>,
) {
let (scale, rotation, translation) = match node.transform() {
gltf::scene::Transform::Matrix { matrix } => {
Expand Down Expand Up @@ -324,11 +324,11 @@ fn gather_instances_recursive(
}

if let Some(mesh) = node.mesh() {
if let Some((gpu_mesh, mesh)) = meshes.get(&mesh.index()) {
let mut gpu_mesh =
MeshInstance::new_with_cpu_mesh(gpu_mesh.clone(), Some(mesh.clone()));
gpu_mesh.world_from_mesh = transform;
instances.push(gpu_mesh);
if let Some(mesh_key) = meshes.get(&mesh.index()) {
instances.push(CpuMeshInstance {
mesh: *mesh_key,
world_from_mesh: transform,
});
}
}
}
31 changes: 3 additions & 28 deletions crates/viewer/re_renderer/src/importer/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod cpu_model;

#[cfg(feature = "import-obj")]
pub mod obj;

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

use re_math::Vec3Ext as _;

use crate::renderer::MeshInstance;

pub fn to_uniform_scale(scale: glam::Vec3) -> f32 {
if scale.has_equal_components(0.001) {
scale.x
} else {
let uniform_scale = (scale.x * scale.y * scale.z).cbrt();
re_log::warn!("mesh has non-uniform scale ({:?}). This is currently not supported. Using geometric mean {}", scale,uniform_scale);
uniform_scale
}
}

pub fn calculate_bounding_box(instances: &[MeshInstance]) -> re_math::BoundingBox {
re_math::BoundingBox::from_points(
instances
.iter()
.filter_map(|mesh_instance| {
mesh_instance.mesh.as_ref().map(|mesh| {
mesh.vertex_positions
.iter()
.map(|p| mesh_instance.world_from_mesh.transform_point3(*p))
})
})
.flatten(),
)
}
pub use cpu_model::{CpuMeshInstance, CpuModel, CpuModelMeshKey};
Loading

0 comments on commit 1c02564

Please sign in to comment.