mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
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:
parent
5f3a529920
commit
6e8d43a037
5 changed files with 148 additions and 68 deletions
|
@ -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 }
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in a new issue