From 6e8d43a03745de768cbdd2e81b9947fd298c2bc3 Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Mon, 15 Jul 2024 08:06:02 -0700 Subject: [PATCH] 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` --- crates/bevy_pbr/Cargo.toml | 13 +- crates/bevy_pbr/src/meshlet/asset.rs | 192 ++++++++++++++++------- crates/bevy_pbr/src/meshlet/from_mesh.rs | 1 - crates/bevy_pbr/src/meshlet/mod.rs | 7 +- examples/3d/meshlet.rs | 3 +- 5 files changed, 148 insertions(+), 68 deletions(-) diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index 4c75bbaff2..247be052a9 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -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 } diff --git a/crates/bevy_pbr/src/meshlet/asset.rs b/crates/bevy_pbr/src/meshlet/asset.rs index 31f6ecd66e..5701e0f288 100644 --- a/crates/bevy_pbr/src/meshlet/asset.rs +++ b/crates/bevy_pbr/src/meshlet/asset.rs @@ -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 { - 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 { + // 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 { +async fn async_read_u64(reader: &mut dyn Reader) -> Result { 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 { + let mut bytes = [0u8; 8]; + reader.read_exact(&mut bytes)?; + Ok(u64::from_le_bytes(bytes)) +} + +fn write_slice( + 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(reader: &mut dyn Read) -> Result, 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 { + 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 { + block_on(self.0.read(buf)) + } +} diff --git a/crates/bevy_pbr/src/meshlet/from_mesh.rs b/crates/bevy_pbr/src/meshlet/from_mesh.rs index 882820fd97..3b876ca00b 100644 --- a/crates/bevy_pbr/src/meshlet/from_mesh.rs +++ b/crates/bevy_pbr/src/meshlet/from_mesh.rs @@ -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, diff --git a/crates/bevy_pbr/src/meshlet/mod.rs b/crates/bevy_pbr/src/meshlet/mod.rs index 3d00683bef..2a19bbd280 100644 --- a/crates/bevy_pbr/src/meshlet/mod.rs +++ b/crates/bevy_pbr/src/meshlet/mod.rs @@ -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::() - .register_asset_loader(MeshletMeshSaverLoad) + .register_asset_loader(MeshletMeshSaverLoader) .insert_resource(Msaa::Off) .add_systems( PostUpdate, diff --git a/examples/3d/meshlet.rs b/examples/3d/meshlet.rs index ecd3201918..24b3979f38 100644 --- a/examples/3d/meshlet.rs +++ b/examples/3d/meshlet.rs @@ -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() {