Skip to content

Commit 6e8d43a

Browse files
authored
Faster MeshletMesh deserialization (#14193)
# Objective - Using bincode to deserialize binary into a MeshletMesh is expensive (~77ms for a 5mb file). ## Solution - Write a custom deserializer using bytemuck's Pod types and slice casting. - Total asset load time has gone from ~102ms to ~12ms. - Change some types I never meant to be public to private and other misc cleanup. ## Testing - Ran the meshlet example and added timing spans to the asset loader. --- ## Changelog - Improved `MeshletMesh` loading speed - The `MeshletMesh` disk format has changed, and `MESHLET_MESH_ASSET_VERSION` has been bumped - `MeshletMesh` fields are now private - Renamed `MeshletMeshSaverLoad` to `MeshletMeshSaverLoader` - The `Meshlet`, `MeshletBoundingSpheres`, and `MeshletBoundingSphere` types are now private - Removed `MeshletMeshSaveOrLoadError::SerializationOrDeserialization` - Added `MeshletMeshSaveOrLoadError::WrongFileType` ## Migration Guide - Regenerate your `MeshletMesh` assets, as the disk format has changed, and `MESHLET_MESH_ASSET_VERSION` has been bumped - `MeshletMesh` fields are now private - `MeshletMeshSaverLoad` is now named `MeshletMeshSaverLoader` - The `Meshlet`, `MeshletBoundingSpheres`, and `MeshletBoundingSphere` types are now private - `MeshletMeshSaveOrLoadError::SerializationOrDeserialization` has been removed - Added `MeshletMeshSaveOrLoadError::WrongFileType`, match on this variant if you match on `MeshletMeshSaveOrLoadError`
1 parent 5f3a529 commit 6e8d43a

File tree

5 files changed

+144
-64
lines changed

5 files changed

+144
-64
lines changed

crates/bevy_pbr/Cargo.toml

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,7 @@ shader_format_glsl = ["bevy_render/shader_format_glsl"]
1818
trace = ["bevy_render/trace"]
1919
ios_simulator = ["bevy_render/ios_simulator"]
2020
# Enables the meshlet renderer for dense high-poly scenes (experimental)
21-
meshlet = [
22-
"dep:lz4_flex",
23-
"dep:serde",
24-
"dep:bincode",
25-
"dep:thiserror",
26-
"dep:range-alloc",
27-
]
21+
meshlet = ["dep:lz4_flex", "dep:thiserror", "dep:range-alloc", "dep:bevy_tasks"]
2822
# Enables processing meshes into meshlet meshes
2923
meshlet_processor = ["meshlet", "dep:meshopt", "dep:metis", "dep:itertools"]
3024

@@ -34,16 +28,17 @@ bevy_app = { path = "../bevy_app", version = "0.15.0-dev" }
3428
bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" }
3529
bevy_color = { path = "../bevy_color", version = "0.15.0-dev" }
3630
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev" }
31+
bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" }
3732
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" }
3833
bevy_math = { path = "../bevy_math", version = "0.15.0-dev" }
3934
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [
4035
"bevy",
4136
] }
4237
bevy_render = { path = "../bevy_render", version = "0.15.0-dev" }
38+
bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev", optional = true }
4339
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
4440
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
4541
bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }
46-
bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" }
4742

4843

4944
# other
@@ -53,8 +48,6 @@ fixedbitset = "0.5"
5348
lz4_flex = { version = "0.11", default-features = false, features = [
5449
"frame",
5550
], optional = true }
56-
serde = { version = "1", features = ["derive", "rc"], optional = true }
57-
bincode = { version = "1", optional = true }
5851
thiserror = { version = "1", optional = true }
5952
range-alloc = { version = "0.1", optional = true }
6053
meshopt = { version = "0.3.0", optional = true }

crates/bevy_pbr/src/meshlet/asset.rs

Lines changed: 134 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@ use bevy_asset::{
55
};
66
use bevy_math::Vec3;
77
use bevy_reflect::TypePath;
8+
use bevy_tasks::block_on;
89
use bytemuck::{Pod, Zeroable};
910
use lz4_flex::frame::{FrameDecoder, FrameEncoder};
10-
use serde::{Deserialize, Serialize};
11-
use std::{io::Cursor, sync::Arc};
11+
use std::{
12+
io::{Read, Write},
13+
sync::Arc,
14+
};
15+
16+
/// Unique identifier for the [`MeshletMesh`] asset format.
17+
const MESHLET_MESH_ASSET_MAGIC: u64 = 1717551717668;
1218

1319
/// The current version of the [`MeshletMesh`] asset format.
14-
pub const MESHLET_MESH_ASSET_VERSION: u64 = 0;
20+
pub const MESHLET_MESH_ASSET_VERSION: u64 = 1;
1521

1622
/// A mesh that has been pre-processed into multiple small clusters of triangles called meshlets.
1723
///
@@ -27,24 +33,24 @@ pub const MESHLET_MESH_ASSET_VERSION: u64 = 0;
2733
/// * Limited control over [`bevy_render::render_resource::RenderPipelineDescriptor`] attributes.
2834
///
2935
/// See also [`super::MaterialMeshletMeshBundle`] and [`super::MeshletPlugin`].
30-
#[derive(Asset, TypePath, Serialize, Deserialize, Clone)]
36+
#[derive(Asset, TypePath, Clone)]
3137
pub struct MeshletMesh {
3238
/// The total amount of triangles summed across all LOD 0 meshlets in the mesh.
33-
pub worst_case_meshlet_triangles: u64,
39+
pub(crate) worst_case_meshlet_triangles: u64,
3440
/// Raw vertex data bytes for the overall mesh.
35-
pub vertex_data: Arc<[u8]>,
41+
pub(crate) vertex_data: Arc<[u8]>,
3642
/// Indices into `vertex_data`.
37-
pub vertex_ids: Arc<[u32]>,
43+
pub(crate) vertex_ids: Arc<[u32]>,
3844
/// Indices into `vertex_ids`.
39-
pub indices: Arc<[u8]>,
45+
pub(crate) indices: Arc<[u8]>,
4046
/// The list of meshlets making up this mesh.
41-
pub meshlets: Arc<[Meshlet]>,
47+
pub(crate) meshlets: Arc<[Meshlet]>,
4248
/// Spherical bounding volumes.
43-
pub bounding_spheres: Arc<[MeshletBoundingSpheres]>,
49+
pub(crate) bounding_spheres: Arc<[MeshletBoundingSpheres]>,
4450
}
4551

4652
/// A single meshlet within a [`MeshletMesh`].
47-
#[derive(Serialize, Deserialize, Copy, Clone, Pod, Zeroable)]
53+
#[derive(Copy, Clone, Pod, Zeroable)]
4854
#[repr(C)]
4955
pub struct Meshlet {
5056
/// The offset within the parent mesh's [`MeshletMesh::vertex_ids`] buffer where the indices for this meshlet begin.
@@ -56,7 +62,7 @@ pub struct Meshlet {
5662
}
5763

5864
/// Bounding spheres used for culling and choosing level of detail for a [`Meshlet`].
59-
#[derive(Serialize, Deserialize, Copy, Clone, Pod, Zeroable)]
65+
#[derive(Copy, Clone, Pod, Zeroable)]
6066
#[repr(C)]
6167
pub struct MeshletBoundingSpheres {
6268
/// The bounding sphere used for frustum and occlusion culling for this meshlet.
@@ -68,84 +74,162 @@ pub struct MeshletBoundingSpheres {
6874
}
6975

7076
/// A spherical bounding volume used for a [`Meshlet`].
71-
#[derive(Serialize, Deserialize, Copy, Clone, Pod, Zeroable)]
77+
#[derive(Copy, Clone, Pod, Zeroable)]
7278
#[repr(C)]
7379
pub struct MeshletBoundingSphere {
7480
pub center: Vec3,
7581
pub radius: f32,
7682
}
7783

7884
/// An [`AssetLoader`] and [`AssetSaver`] for `.meshlet_mesh` [`MeshletMesh`] assets.
79-
pub struct MeshletMeshSaverLoad;
85+
pub struct MeshletMeshSaverLoader;
8086

81-
impl AssetLoader for MeshletMeshSaverLoad {
87+
impl AssetSaver for MeshletMeshSaverLoader {
8288
type Asset = MeshletMesh;
8389
type Settings = ();
90+
type OutputLoader = Self;
8491
type Error = MeshletMeshSaveOrLoadError;
8592

86-
async fn load<'a>(
93+
async fn save<'a>(
8794
&'a self,
88-
reader: &'a mut dyn Reader,
89-
_settings: &'a Self::Settings,
90-
_load_context: &'a mut LoadContext<'_>,
91-
) -> Result<Self::Asset, Self::Error> {
92-
let version = read_u64(reader).await?;
93-
if version != MESHLET_MESH_ASSET_VERSION {
94-
return Err(MeshletMeshSaveOrLoadError::WrongVersion { found: version });
95-
}
95+
writer: &'a mut Writer,
96+
asset: SavedAsset<'a, MeshletMesh>,
97+
_settings: &'a (),
98+
) -> Result<(), MeshletMeshSaveOrLoadError> {
99+
// Write asset magic number
100+
writer
101+
.write_all(&MESHLET_MESH_ASSET_MAGIC.to_le_bytes())
102+
.await?;
96103

97-
let mut bytes = Vec::new();
98-
reader.read_to_end(&mut bytes).await?;
99-
let asset = bincode::deserialize_from(FrameDecoder::new(Cursor::new(bytes)))?;
104+
// Write asset version
105+
writer
106+
.write_all(&MESHLET_MESH_ASSET_VERSION.to_le_bytes())
107+
.await?;
100108

101-
Ok(asset)
102-
}
109+
// Compress and write asset data
110+
writer
111+
.write_all(&asset.worst_case_meshlet_triangles.to_le_bytes())
112+
.await?;
113+
let mut writer = FrameEncoder::new(AsyncWriteSyncAdapter(writer));
114+
write_slice(&asset.vertex_data, &mut writer)?;
115+
write_slice(&asset.vertex_ids, &mut writer)?;
116+
write_slice(&asset.indices, &mut writer)?;
117+
write_slice(&asset.meshlets, &mut writer)?;
118+
write_slice(&asset.bounding_spheres, &mut writer)?;
119+
writer.finish()?;
103120

104-
fn extensions(&self) -> &[&str] {
105-
&["meshlet_mesh"]
121+
Ok(())
106122
}
107123
}
108124

109-
impl AssetSaver for MeshletMeshSaverLoad {
125+
impl AssetLoader for MeshletMeshSaverLoader {
110126
type Asset = MeshletMesh;
111127
type Settings = ();
112-
type OutputLoader = Self;
113128
type Error = MeshletMeshSaveOrLoadError;
114129

115-
async fn save<'a>(
130+
async fn load<'a>(
116131
&'a self,
117-
writer: &'a mut Writer,
118-
asset: SavedAsset<'a, Self::Asset>,
119-
_settings: &'a Self::Settings,
120-
) -> Result<(), Self::Error> {
121-
writer
122-
.write_all(&MESHLET_MESH_ASSET_VERSION.to_le_bytes())
123-
.await?;
132+
reader: &'a mut dyn Reader,
133+
_settings: &'a (),
134+
_load_context: &'a mut LoadContext<'_>,
135+
) -> Result<MeshletMesh, MeshletMeshSaveOrLoadError> {
136+
// Load and check magic number
137+
let magic = async_read_u64(reader).await?;
138+
if magic != MESHLET_MESH_ASSET_MAGIC {
139+
return Err(MeshletMeshSaveOrLoadError::WrongFileType);
140+
}
124141

125-
let mut bytes = Vec::new();
126-
let mut sync_writer = FrameEncoder::new(&mut bytes);
127-
bincode::serialize_into(&mut sync_writer, asset.get())?;
128-
sync_writer.finish()?;
129-
writer.write_all(&bytes).await?;
142+
// Load and check asset version
143+
let version = async_read_u64(reader).await?;
144+
if version != MESHLET_MESH_ASSET_VERSION {
145+
return Err(MeshletMeshSaveOrLoadError::WrongVersion { found: version });
146+
}
130147

131-
Ok(())
148+
// Load and decompress asset data
149+
let worst_case_meshlet_triangles = async_read_u64(reader).await?;
150+
let reader = &mut FrameDecoder::new(AsyncReadSyncAdapter(reader));
151+
let vertex_data = read_slice(reader)?;
152+
let vertex_ids = read_slice(reader)?;
153+
let indices = read_slice(reader)?;
154+
let meshlets = read_slice(reader)?;
155+
let bounding_spheres = read_slice(reader)?;
156+
157+
Ok(MeshletMesh {
158+
worst_case_meshlet_triangles,
159+
vertex_data,
160+
vertex_ids,
161+
indices,
162+
meshlets,
163+
bounding_spheres,
164+
})
165+
}
166+
167+
fn extensions(&self) -> &[&str] {
168+
&["meshlet_mesh"]
132169
}
133170
}
134171

135172
#[derive(thiserror::Error, Debug)]
136173
pub enum MeshletMeshSaveOrLoadError {
174+
#[error("file was not a MeshletMesh asset")]
175+
WrongFileType,
137176
#[error("expected asset version {MESHLET_MESH_ASSET_VERSION} but found version {found}")]
138177
WrongVersion { found: u64 },
139-
#[error("failed to serialize or deserialize asset data")]
140-
SerializationOrDeserialization(#[from] bincode::Error),
141178
#[error("failed to compress or decompress asset data")]
142179
CompressionOrDecompression(#[from] lz4_flex::frame::Error),
143180
#[error("failed to read or write asset data")]
144181
Io(#[from] std::io::Error),
145182
}
146183

147-
async fn read_u64(reader: &mut dyn Reader) -> Result<u64, bincode::Error> {
184+
async fn async_read_u64(reader: &mut dyn Reader) -> Result<u64, std::io::Error> {
148185
let mut bytes = [0u8; 8];
149186
reader.read_exact(&mut bytes).await?;
150187
Ok(u64::from_le_bytes(bytes))
151188
}
189+
190+
fn read_u64(reader: &mut dyn Read) -> Result<u64, std::io::Error> {
191+
let mut bytes = [0u8; 8];
192+
reader.read_exact(&mut bytes)?;
193+
Ok(u64::from_le_bytes(bytes))
194+
}
195+
196+
fn write_slice<T: Pod>(
197+
field: &[T],
198+
writer: &mut dyn Write,
199+
) -> Result<(), MeshletMeshSaveOrLoadError> {
200+
writer.write_all(&(field.len() as u64).to_le_bytes())?;
201+
writer.write_all(bytemuck::cast_slice(field))?;
202+
Ok(())
203+
}
204+
205+
fn read_slice<T: Pod>(reader: &mut dyn Read) -> Result<Arc<[T]>, std::io::Error> {
206+
let len = read_u64(reader)? as usize;
207+
208+
let mut data: Arc<[T]> = std::iter::repeat_with(T::zeroed).take(len).collect();
209+
let slice = Arc::get_mut(&mut data).unwrap();
210+
reader.read_exact(bytemuck::cast_slice_mut(slice))?;
211+
212+
Ok(data)
213+
}
214+
215+
// TODO: Use async for everything and get rid of this adapter
216+
struct AsyncWriteSyncAdapter<'a>(&'a mut Writer);
217+
218+
impl Write for AsyncWriteSyncAdapter<'_> {
219+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
220+
block_on(self.0.write(buf))
221+
}
222+
223+
fn flush(&mut self) -> std::io::Result<()> {
224+
block_on(self.0.flush())
225+
}
226+
}
227+
228+
// TODO: Use async for everything and get rid of this adapter
229+
struct AsyncReadSyncAdapter<'a>(&'a mut dyn Reader);
230+
231+
impl Read for AsyncReadSyncAdapter<'_> {
232+
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
233+
block_on(self.0.read(buf))
234+
}
235+
}

crates/bevy_pbr/src/meshlet/from_mesh.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,6 @@ fn simplify_meshlet_groups(
294294
let target_error = target_error_relative * mesh_scale;
295295

296296
// Simplify the group to ~50% triangle count
297-
// TODO: Use simplify_with_locks()
298297
let mut error = 0.0;
299298
let simplified_group_indices = simplify(
300299
&group_indices,

crates/bevy_pbr/src/meshlet/mod.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub(crate) use self::{
3030
},
3131
};
3232

33-
pub use self::asset::*;
33+
pub use self::asset::{MeshletMesh, MeshletMeshSaverLoader};
3434
#[cfg(feature = "meshlet_processor")]
3535
pub use self::from_mesh::MeshToMeshletMeshConversionError;
3636

@@ -118,6 +118,9 @@ pub struct MeshletPlugin;
118118

119119
impl Plugin for MeshletPlugin {
120120
fn build(&self, app: &mut App) {
121+
#[cfg(target_endian = "big")]
122+
compile_error!("MeshletPlugin is only supported on little-endian processors.");
123+
121124
load_internal_asset!(
122125
app,
123126
MESHLET_BINDINGS_SHADER_HANDLE,
@@ -168,7 +171,7 @@ impl Plugin for MeshletPlugin {
168171
);
169172

170173
app.init_asset::<MeshletMesh>()
171-
.register_asset_loader(MeshletMeshSaverLoad)
174+
.register_asset_loader(MeshletMeshSaverLoader)
172175
.insert_resource(Msaa::Off)
173176
.add_systems(
174177
PostUpdate,

examples/3d/meshlet.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ use bevy::{
1616
use camera_controller::{CameraController, CameraControllerPlugin};
1717
use std::{f32::consts::PI, path::Path, process::ExitCode};
1818

19-
const ASSET_URL: &str = "https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/bd869887bc5c9c6e74e353f657d342bef84bacd8/bunny.meshlet_mesh";
19+
const ASSET_URL: &str =
20+
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/b6c712cfc87c65de419f856845401aba336a7bcd/bunny.meshlet_mesh";
2021

2122
fn main() -> ExitCode {
2223
if !Path::new("./assets/models/bunny.meshlet_mesh").exists() {

0 commit comments

Comments
 (0)