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`
This commit is contained in:
JMS55 2024-07-15 08:06:02 -07:00 committed by GitHub
parent 5f3a529920
commit 6e8d43a037
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 148 additions and 68 deletions

View file

@ -18,13 +18,7 @@ shader_format_glsl = ["bevy_render/shader_format_glsl"]
trace = ["bevy_render/trace"]
ios_simulator = ["bevy_render/ios_simulator"]
# Enables the meshlet renderer for dense high-poly scenes (experimental)
meshlet = [
"dep:lz4_flex",
"dep:serde",
"dep:bincode",
"dep:thiserror",
"dep:range-alloc",
]
meshlet = ["dep:lz4_flex", "dep:thiserror", "dep:range-alloc", "dep:bevy_tasks"]
# Enables processing meshes into meshlet meshes
meshlet_processor = ["meshlet", "dep:meshopt", "dep:metis", "dep:itertools"]
@ -34,16 +28,17 @@ bevy_app = { path = "../bevy_app", version = "0.15.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.15.0-dev" }
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.15.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [
"bevy",
] }
bevy_render = { path = "../bevy_render", version = "0.15.0-dev" }
bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev", optional = true }
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" }
# other
@ -53,8 +48,6 @@ fixedbitset = "0.5"
lz4_flex = { version = "0.11", default-features = false, features = [
"frame",
], optional = true }
serde = { version = "1", features = ["derive", "rc"], optional = true }
bincode = { version = "1", optional = true }
thiserror = { version = "1", optional = true }
range-alloc = { version = "0.1", optional = true }
meshopt = { version = "0.3.0", optional = true }

View file

@ -5,13 +5,19 @@ use bevy_asset::{
};
use bevy_math::Vec3;
use bevy_reflect::TypePath;
use bevy_tasks::block_on;
use bytemuck::{Pod, Zeroable};
use lz4_flex::frame::{FrameDecoder, FrameEncoder};
use serde::{Deserialize, Serialize};
use std::{io::Cursor, sync::Arc};
use std::{
io::{Read, Write},
sync::Arc,
};
/// Unique identifier for the [`MeshletMesh`] asset format.
const MESHLET_MESH_ASSET_MAGIC: u64 = 1717551717668;
/// The current version of the [`MeshletMesh`] asset format.
pub const MESHLET_MESH_ASSET_VERSION: u64 = 0;
pub const MESHLET_MESH_ASSET_VERSION: u64 = 1;
/// A mesh that has been pre-processed into multiple small clusters of triangles called meshlets.
///
@ -27,24 +33,24 @@ pub const MESHLET_MESH_ASSET_VERSION: u64 = 0;
/// * Limited control over [`bevy_render::render_resource::RenderPipelineDescriptor`] attributes.
///
/// See also [`super::MaterialMeshletMeshBundle`] and [`super::MeshletPlugin`].
#[derive(Asset, TypePath, Serialize, Deserialize, Clone)]
#[derive(Asset, TypePath, Clone)]
pub struct MeshletMesh {
/// The total amount of triangles summed across all LOD 0 meshlets in the mesh.
pub worst_case_meshlet_triangles: u64,
pub(crate) worst_case_meshlet_triangles: u64,
/// Raw vertex data bytes for the overall mesh.
pub vertex_data: Arc<[u8]>,
pub(crate) vertex_data: Arc<[u8]>,
/// Indices into `vertex_data`.
pub vertex_ids: Arc<[u32]>,
pub(crate) vertex_ids: Arc<[u32]>,
/// Indices into `vertex_ids`.
pub indices: Arc<[u8]>,
pub(crate) indices: Arc<[u8]>,
/// The list of meshlets making up this mesh.
pub meshlets: Arc<[Meshlet]>,
pub(crate) meshlets: Arc<[Meshlet]>,
/// Spherical bounding volumes.
pub bounding_spheres: Arc<[MeshletBoundingSpheres]>,
pub(crate) bounding_spheres: Arc<[MeshletBoundingSpheres]>,
}
/// A single meshlet within a [`MeshletMesh`].
#[derive(Serialize, Deserialize, Copy, Clone, Pod, Zeroable)]
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
pub struct Meshlet {
/// 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 {
}
/// Bounding spheres used for culling and choosing level of detail for a [`Meshlet`].
#[derive(Serialize, Deserialize, Copy, Clone, Pod, Zeroable)]
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
pub struct MeshletBoundingSpheres {
/// The bounding sphere used for frustum and occlusion culling for this meshlet.
@ -68,7 +74,7 @@ pub struct MeshletBoundingSpheres {
}
/// A spherical bounding volume used for a [`Meshlet`].
#[derive(Serialize, Deserialize, Copy, Clone, Pod, Zeroable)]
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
pub struct MeshletBoundingSphere {
pub center: Vec3,
@ -76,37 +82,9 @@ pub struct MeshletBoundingSphere {
}
/// An [`AssetLoader`] and [`AssetSaver`] for `.meshlet_mesh` [`MeshletMesh`] assets.
pub struct MeshletMeshSaverLoad;
pub struct MeshletMeshSaverLoader;
impl AssetLoader for MeshletMeshSaverLoad {
type Asset = MeshletMesh;
type Settings = ();
type Error = MeshletMeshSaveOrLoadError;
async fn load<'a>(
&'a self,
reader: &'a mut dyn Reader,
_settings: &'a Self::Settings,
_load_context: &'a mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let version = read_u64(reader).await?;
if version != MESHLET_MESH_ASSET_VERSION {
return Err(MeshletMeshSaveOrLoadError::WrongVersion { found: version });
}
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let asset = bincode::deserialize_from(FrameDecoder::new(Cursor::new(bytes)))?;
Ok(asset)
}
fn extensions(&self) -> &[&str] {
&["meshlet_mesh"]
}
}
impl AssetSaver for MeshletMeshSaverLoad {
impl AssetSaver for MeshletMeshSaverLoader {
type Asset = MeshletMesh;
type Settings = ();
type OutputLoader = Self;
@ -115,37 +93,143 @@ impl AssetSaver for MeshletMeshSaverLoad {
async fn save<'a>(
&'a self,
writer: &'a mut Writer,
asset: SavedAsset<'a, Self::Asset>,
_settings: &'a Self::Settings,
) -> Result<(), Self::Error> {
asset: SavedAsset<'a, MeshletMesh>,
_settings: &'a (),
) -> Result<(), MeshletMeshSaveOrLoadError> {
// Write asset magic number
writer
.write_all(&MESHLET_MESH_ASSET_MAGIC.to_le_bytes())
.await?;
// Write asset version
writer
.write_all(&MESHLET_MESH_ASSET_VERSION.to_le_bytes())
.await?;
let mut bytes = Vec::new();
let mut sync_writer = FrameEncoder::new(&mut bytes);
bincode::serialize_into(&mut sync_writer, asset.get())?;
sync_writer.finish()?;
writer.write_all(&bytes).await?;
// Compress and write asset data
writer
.write_all(&asset.worst_case_meshlet_triangles.to_le_bytes())
.await?;
let mut writer = FrameEncoder::new(AsyncWriteSyncAdapter(writer));
write_slice(&asset.vertex_data, &mut writer)?;
write_slice(&asset.vertex_ids, &mut writer)?;
write_slice(&asset.indices, &mut writer)?;
write_slice(&asset.meshlets, &mut writer)?;
write_slice(&asset.bounding_spheres, &mut writer)?;
writer.finish()?;
Ok(())
}
}
impl AssetLoader for MeshletMeshSaverLoader {
type Asset = MeshletMesh;
type Settings = ();
type Error = MeshletMeshSaveOrLoadError;
async fn load<'a>(
&'a self,
reader: &'a mut dyn Reader,
_settings: &'a (),
_load_context: &'a mut LoadContext<'_>,
) -> Result<MeshletMesh, MeshletMeshSaveOrLoadError> {
// Load and check magic number
let magic = async_read_u64(reader).await?;
if magic != MESHLET_MESH_ASSET_MAGIC {
return Err(MeshletMeshSaveOrLoadError::WrongFileType);
}
// Load and check asset version
let version = async_read_u64(reader).await?;
if version != MESHLET_MESH_ASSET_VERSION {
return Err(MeshletMeshSaveOrLoadError::WrongVersion { found: version });
}
// Load and decompress asset data
let worst_case_meshlet_triangles = async_read_u64(reader).await?;
let reader = &mut FrameDecoder::new(AsyncReadSyncAdapter(reader));
let vertex_data = read_slice(reader)?;
let vertex_ids = read_slice(reader)?;
let indices = read_slice(reader)?;
let meshlets = read_slice(reader)?;
let bounding_spheres = read_slice(reader)?;
Ok(MeshletMesh {
worst_case_meshlet_triangles,
vertex_data,
vertex_ids,
indices,
meshlets,
bounding_spheres,
})
}
fn extensions(&self) -> &[&str] {
&["meshlet_mesh"]
}
}
#[derive(thiserror::Error, Debug)]
pub enum MeshletMeshSaveOrLoadError {
#[error("file was not a MeshletMesh asset")]
WrongFileType,
#[error("expected asset version {MESHLET_MESH_ASSET_VERSION} but found version {found}")]
WrongVersion { found: u64 },
#[error("failed to serialize or deserialize asset data")]
SerializationOrDeserialization(#[from] bincode::Error),
#[error("failed to compress or decompress asset data")]
CompressionOrDecompression(#[from] lz4_flex::frame::Error),
#[error("failed to read or write asset data")]
Io(#[from] std::io::Error),
}
async fn read_u64(reader: &mut dyn Reader) -> Result<u64, bincode::Error> {
async fn async_read_u64(reader: &mut dyn Reader) -> Result<u64, std::io::Error> {
let mut bytes = [0u8; 8];
reader.read_exact(&mut bytes).await?;
Ok(u64::from_le_bytes(bytes))
}
fn read_u64(reader: &mut dyn Read) -> Result<u64, std::io::Error> {
let mut bytes = [0u8; 8];
reader.read_exact(&mut bytes)?;
Ok(u64::from_le_bytes(bytes))
}
fn write_slice<T: Pod>(
field: &[T],
writer: &mut dyn Write,
) -> Result<(), MeshletMeshSaveOrLoadError> {
writer.write_all(&(field.len() as u64).to_le_bytes())?;
writer.write_all(bytemuck::cast_slice(field))?;
Ok(())
}
fn read_slice<T: Pod>(reader: &mut dyn Read) -> Result<Arc<[T]>, std::io::Error> {
let len = read_u64(reader)? as usize;
let mut data: Arc<[T]> = std::iter::repeat_with(T::zeroed).take(len).collect();
let slice = Arc::get_mut(&mut data).unwrap();
reader.read_exact(bytemuck::cast_slice_mut(slice))?;
Ok(data)
}
// TODO: Use async for everything and get rid of this adapter
struct AsyncWriteSyncAdapter<'a>(&'a mut Writer);
impl Write for AsyncWriteSyncAdapter<'_> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
block_on(self.0.write(buf))
}
fn flush(&mut self) -> std::io::Result<()> {
block_on(self.0.flush())
}
}
// TODO: Use async for everything and get rid of this adapter
struct AsyncReadSyncAdapter<'a>(&'a mut dyn Reader);
impl Read for AsyncReadSyncAdapter<'_> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
block_on(self.0.read(buf))
}
}

View file

@ -294,7 +294,6 @@ fn simplify_meshlet_groups(
let target_error = target_error_relative * mesh_scale;
// Simplify the group to ~50% triangle count
// TODO: Use simplify_with_locks()
let mut error = 0.0;
let simplified_group_indices = simplify(
&group_indices,

View file

@ -30,7 +30,7 @@ pub(crate) use self::{
},
};
pub use self::asset::*;
pub use self::asset::{MeshletMesh, MeshletMeshSaverLoader};
#[cfg(feature = "meshlet_processor")]
pub use self::from_mesh::MeshToMeshletMeshConversionError;
@ -118,6 +118,9 @@ pub struct MeshletPlugin;
impl Plugin for MeshletPlugin {
fn build(&self, app: &mut App) {
#[cfg(target_endian = "big")]
compile_error!("MeshletPlugin is only supported on little-endian processors.");
load_internal_asset!(
app,
MESHLET_BINDINGS_SHADER_HANDLE,
@ -168,7 +171,7 @@ impl Plugin for MeshletPlugin {
);
app.init_asset::<MeshletMesh>()
.register_asset_loader(MeshletMeshSaverLoad)
.register_asset_loader(MeshletMeshSaverLoader)
.insert_resource(Msaa::Off)
.add_systems(
PostUpdate,

View file

@ -16,7 +16,8 @@ use bevy::{
use camera_controller::{CameraController, CameraControllerPlugin};
use std::{f32::consts::PI, path::Path, process::ExitCode};
const ASSET_URL: &str = "https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/bd869887bc5c9c6e74e353f657d342bef84bacd8/bunny.meshlet_mesh";
const ASSET_URL: &str =
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/b6c712cfc87c65de419f856845401aba336a7bcd/bunny.meshlet_mesh";
fn main() -> ExitCode {
if !Path::new("./assets/models/bunny.meshlet_mesh").exists() {