tirbofish/dropbear · diff
feature: memory was an issue when serializing to bytes, so i made a new model file type (eucmdl) and a new generic binary file type (eucbin).
Signature present but could not be verified.
Unverified
@@ -31,7 +31,7 @@ env_logger = "0.11" futures = "0.3" gilrs = "0.11" git2 = { version = "0.20", features = ["vendored-openssl"] } -glam = { version = "0.30", features = ["serde", "mint", "bytemuck"] } +glam = { version = "0.30", features = ["serde", "mint", "bytemuck", "rkyv", "bytecheck"] } hecs = { version = "0.11", features = ["serde"] } log = "0.4" log-once = "0.4" @@ -89,6 +89,7 @@ puffin_http = "0.16" dyn-clone = "1.0" downcast-rs = "2.0" float-derive = "0.1" +rkyv = "0.8" [workspace.dependencies.image] version = "0.24" @@ -30,7 +30,7 @@ log.workspace = true log-once.workspace = true serde.workspace = true spin_sleep.workspace = true -wgpu.workspace = true +wgpu = { workspace = true, features = ["serde"] } winit.workspace = true hecs.workspace = true parking_lot.workspace = true @@ -49,6 +49,7 @@ puffin.workspace = true bitflags.workspace = true puffin_http.workspace = true float-derive.workspace = true +rkyv.workspace = true #yakui-wgpu.workspace = true #yakui.workspace = true @@ -10,6 +10,7 @@ use gltf::texture::MinFilter; use parking_lot::RwLock; use puffin::profile_scope; use rayon::prelude::*; +use rkyv::Archive; use serde::{Deserialize, Serialize}; use std::hash::{DefaultHasher, Hash, Hasher}; use std::sync::Arc; @@ -68,7 +69,7 @@ pub struct Material { pub wrap_mode: TextureWrapMode, } -#[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize, Default)] +#[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize, Default)] pub enum AlphaMode { #[default] Opaque = 1, @@ -87,7 +88,7 @@ impl Into<AlphaMode> for gltf::material::AlphaMode { } /// Represents a node in the scene graph (can be a joint/bone or a mesh) -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)] pub struct Node { pub name: String, pub parent: Option<usize>, @@ -96,7 +97,7 @@ pub struct Node { } /// Local transform of a node relative to its parent -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)] pub struct NodeTransform { pub translation: glam::Vec3, pub rotation: glam::Quat, @@ -118,7 +119,7 @@ impl NodeTransform { } /// A skin defines how a mesh is bound to a skeleton -#[derive(Clone)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)] pub struct Skin { pub name: String, /// Indices of joints (nodes) in the Model's nodes array @@ -130,7 +131,7 @@ pub struct Skin { } /// An animation that can be played on a skeleton -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)] pub struct Animation { pub name: String, pub channels: Vec<AnimationChannel>, @@ -138,7 +139,7 @@ pub struct Animation { } /// Describes how an animation affects a specific node -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)] pub struct AnimationChannel { /// Target node index in the Model's nodes array pub target_node: usize, @@ -150,7 +151,7 @@ pub struct AnimationChannel { pub interpolation: AnimationInterpolation, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)] pub enum ChannelValues { Translations(Vec<glam::Vec3>), Rotations(Vec<glam::Quat>), @@ -342,7 +343,7 @@ impl Material { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)] pub enum AnimationInterpolation { /// The animated values are linearly interpolated between keyframes Linear, @@ -1524,7 +1525,7 @@ pub trait Vertex { /// }; /// ``` #[repr(C)] -#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable, Serialize, Deserialize)] +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)] pub struct ModelVertex { pub position: [f32; 3], pub normal: [f32; 3], @@ -7,6 +7,7 @@ use crate::model::ModelVertex; use crate::model::{Material, Mesh, Model}; use crate::utils::ResourceReference; use parking_lot::RwLock; +use rkyv::Archive; use serde::{Deserialize, Serialize}; use std::hash::{DefaultHasher, Hasher}; use std::sync::Arc; @@ -14,13 +15,13 @@ use wgpu::util::DeviceExt; pub mod cube; -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)] pub enum ProcObjType { Cuboid, } /// An object that comes with a template, and is generated through parameter input. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)] pub struct ProcedurallyGeneratedObject { pub vertices: Vec<ModelVertex>, pub indices: Vec<u32>, @@ -3,7 +3,8 @@ use std::sync::Arc; use crate::asset::AssetRegistry; use crate::graphics::SharedGraphicsContext; use crate::utils::{ResourceReference, ToPotentialString}; -use image::GenericImageView; +use image::{DynamicImage, GenericImageView, RgbaImage}; +use rkyv::Archive; use serde::{Deserialize, Serialize}; use crate::multisampling::{AntiAliasingMode}; @@ -41,22 +42,21 @@ pub struct TextureBuilder<'a> { lod_min_clamp: f32, lod_max_clamp: f32, - view_descriptor: Option<wgpu::TextureViewDescriptor<'a>>, + view_descriptor: Option<wgpu::TextureViewDescriptor<'a>>, // doesnt support serde label: Option<&'a str>, mime_type: Option<String>, - source: TextureSource<'a>, + source: TextureSource, } -enum TextureSource<'a> { +enum TextureSource { Empty, - Bytes(&'a [u8]), - RawPixels(&'a [u8]), - #[allow(dead_code)] - SurfaceConfig(&'a wgpu::SurfaceConfiguration), - #[allow(dead_code)] - Depth(&'a wgpu::SurfaceConfiguration), + Image { + image: DynamicImage, + hash: u64, + reference: ResourceReference, + }, } impl<'a> TextureBuilder<'a> { @@ -111,7 +111,7 @@ impl<'a> TextureBuilder<'a> { /// preset: depth_texture() pub fn depth(mut self, config: &'a wgpu::SurfaceConfiguration, antialiasing: AntiAliasingMode) -> Self { - self.source = TextureSource::Depth(config); + self.source = TextureSource::Empty; self.width = config.width.max(1); self.height = config.height.max(1); self.format = Texture::DEPTH_FORMAT; @@ -128,7 +128,7 @@ impl<'a> TextureBuilder<'a> { /// preset: viewport() pub fn viewport(mut self, config: &'a wgpu::SurfaceConfiguration) -> Self { - self.source = TextureSource::SurfaceConfig(config); + self.source = TextureSource::Empty; self.width = config.width.max(1); self.height = config.height.max(1); self.format = config.format.add_srgb_suffix(); @@ -159,9 +159,71 @@ impl<'a> TextureBuilder<'a> { self } - pub fn from_bytes(mut self, graphics: Arc<SharedGraphicsContext>, bytes: &'a [u8]) -> Self { + pub fn with_bytes(mut self, graphics: Arc<SharedGraphicsContext>, bytes: &'a [u8]) -> Self { self.graphics = Some(graphics); - self.source = TextureSource::Bytes(bytes); + let hash = AssetRegistry::hash_bytes(bytes); + let requested_dimensions = Some((self.width, self.height)).filter(|&d| d != (1, 1)); + + let image = match image::load_from_memory(bytes) { + Ok(image) => image, + Err(err) => { + if let Some((width, height)) = requested_dimensions { + let expected_len = (width as usize) + .saturating_mul(height as usize) + .saturating_mul(4); + if bytes.len() == expected_len { + if let Some(rgba) = RgbaImage::from_raw(width, height, bytes.to_vec()) { + DynamicImage::ImageRgba8(rgba) + } else { + log::error!( + "Texture [{:?}] decode failed ({:?}); raw RGBA reconstruction failed for dimensions {}x{}. Falling back.", + self.label, + err, + width, + height + ); + DynamicImage::ImageRgba8(RgbaImage::from_pixel( + 1, + 1, + image::Rgba([255, 0, 255, 255]), + )) + } + } else { + log::error!( + "Texture [{:?}] decode failed ({:?}); expected {} bytes for raw RGBA ({}x{}), got {}. Falling back.", + self.label, + err, + expected_len, + width, + height, + bytes.len() + ); + DynamicImage::ImageRgba8(RgbaImage::from_pixel( + 1, + 1, + image::Rgba([255, 0, 255, 255]), + )) + } + } else { + log::error!( + "Texture [{:?}] decode failed ({:?}) and no dimensions were provided; falling back to 1x1 magenta.", + self.label, + err + ); + DynamicImage::ImageRgba8(RgbaImage::from_pixel( + 1, + 1, + image::Rgba([255, 0, 255, 255]), + )) + } + } + }; + + self.source = TextureSource::Image { + image, + hash, + reference: ResourceReference::from_bytes(bytes), + }; self.auto_mip = true; self.mipmap_filter = wgpu::FilterMode::Linear; self.usage = wgpu::TextureUsages::TEXTURE_BINDING @@ -171,9 +233,56 @@ impl<'a> TextureBuilder<'a> { self } + pub fn from_bytes(self, graphics: Arc<SharedGraphicsContext>, bytes: &'a [u8]) -> Self { + self.with_bytes(graphics, bytes) + } + pub fn from_raw_pixels(mut self, graphics: Arc<SharedGraphicsContext>, pixels: &'a [u8]) -> Self { self.graphics = Some(graphics); - self.source = TextureSource::RawPixels(pixels); + let hash = AssetRegistry::hash_bytes(pixels); + + let dimensions = (self.width, self.height); + let expected_len = (dimensions.0 as usize) + .saturating_mul(dimensions.1 as usize) + .saturating_mul(4); + + let image = if pixels.len() == expected_len { + if let Some(rgba) = RgbaImage::from_raw(dimensions.0, dimensions.1, pixels.to_vec()) { + DynamicImage::ImageRgba8(rgba) + } else { + log::error!( + "Texture [{:?}] raw RGBA reconstruction failed for dimensions ({}x{}). Falling back to 1x1 magenta.", + self.label, + dimensions.0, + dimensions.1 + ); + DynamicImage::ImageRgba8(RgbaImage::from_pixel( + 1, + 1, + image::Rgba([255, 0, 255, 255]), + )) + } + } else { + log::error!( + "Texture [{:?}] raw pixel byte length {} does not match expected {} for RGBA8 ({}x{}). Falling back.", + self.label, + pixels.len(), + expected_len, + dimensions.0, + dimensions.1 + ); + DynamicImage::ImageRgba8(RgbaImage::from_pixel( + 1, + 1, + image::Rgba([255, 0, 255, 255]), + )) + }; + + self.source = TextureSource::Image { + image, + hash, + reference: ResourceReference::from_bytes(pixels), + }; self.auto_mip = true; self.mipmap_filter = wgpu::FilterMode::Linear; self.usage = wgpu::TextureUsages::TEXTURE_BINDING @@ -207,49 +316,22 @@ impl<'a> TextureBuilder<'a> { puffin::profile_function!(self.label.unwrap_or("TextureBuilder::build")); match &self.source { - TextureSource::Bytes(bytes) => { + TextureSource::Image { image, hash, reference } => { let graphics = self .graphics .as_ref() - .expect("from_bytes() requires graphics context"); - let hash = AssetRegistry::hash_bytes(bytes); + .expect("with_data() requires graphics context"); let requested_dimensions = Some((self.width, self.height)).filter(|&d| d != (1, 1)); - let (rgba, dimensions) = match image::load_from_memory(bytes) { - Ok(image) => { - let rgba = image.to_rgba8().into_raw(); - let dims = requested_dimensions.unwrap_or_else(|| image.dimensions()); - (rgba, dims) + let mut image = image.clone(); + if let Some((width, height)) = requested_dimensions { + if image.width() != width || image.height() != height { + image = image.resize_exact(width, height, image::imageops::FilterType::Triangle); } - Err(err) => { - if let Some(dims) = requested_dimensions { - let expected_len = (dims.0 as usize) - .saturating_mul(dims.1 as usize) - .saturating_mul(4); - if bytes.len() == expected_len { - (bytes.to_vec(), dims) - } else { - log::error!( - "Texture [{:?}] decode failed ({:?}); expected {} bytes for raw RGBA ({}x{}), got {}. Falling back.", - self.label, - err, - expected_len, - dims.0, - dims.1, - bytes.len() - ); - (vec![255, 0, 255, 255], (1, 1)) - } - } else { - log::error!( - "Texture [{:?}] decode failed ({:?}) and no dimensions were provided; falling back to 1x1 magenta.", - self.label, - err - ); - (vec![255, 0, 255, 255], (1, 1)) - } - } - }; + } + + let rgba = image.to_rgba8().into_raw(); + let dimensions = image.dimensions(); let size = wgpu::Extent3d { width: dimensions.0, @@ -264,67 +346,8 @@ impl<'a> TextureBuilder<'a> { &graphics, texture, size, - hash, - ResourceReference::from_bytes(bytes), - ) - } - TextureSource::RawPixels(pixels) => { - let graphics = self - .graphics - .as_ref() - .expect("from_raw_pixels() requires graphics context"); - let hash = AssetRegistry::hash_bytes(pixels); - - let (format, pixels, dimensions) = match self.format.block_copy_size(None) { - Some(bytes_per_pixel) => { - let dimensions = (self.width, self.height); - let expected_len = (dimensions.0 as usize) - .saturating_mul(dimensions.1 as usize) - .saturating_mul(bytes_per_pixel as usize); - - if pixels.len() == expected_len { - (self.format, pixels.to_vec(), dimensions) - } else { - log::error!( - "Texture [{:?}] byte length {} does not match expected {} for {:?} ({}x{}). Falling back to 1x1 magenta.", - self.label, - pixels.len(), - expected_len, - self.format, - dimensions.0, - dimensions.1 - ); - (Texture::TEXTURE_FORMAT, vec![255, 0, 255, 255], (1, 1)) - } - } - None => { - log::error!( - "Texture [{:?}] has unsupported format {:?}; falling back to 1x1 magenta.", - self.label, - self.format - ); - (Texture::TEXTURE_FORMAT, vec![255, 0, 255, 255], (1, 1)) - } - }; - - let size = wgpu::Extent3d { - width: dimensions.0, - height: dimensions.1, - depth_or_array_layers: 1, - }; - - let bytes_per_pixel = format - .block_copy_size(None) - .expect("fallback format must have a valid block size"); - let mip_level_count = self.compute_mip_level_count(size); - let texture = self.create_texture(&graphics.device, size, format, mip_level_count); - Self::upload_level0(&graphics.queue, &texture, size, &pixels, bytes_per_pixel); - self.finish_uploaded_texture( - &graphics, - texture, - size, - hash, - ResourceReference::from_bytes(pixels.as_slice()), + *hash, + reference.clone(), ) } _ => { @@ -865,7 +888,7 @@ impl Texture { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)] pub enum TextureWrapMode { Repeat, Clamp, @@ -2,6 +2,7 @@ use crate::procedural::ProcedurallyGeneratedObject; use serde::{Deserialize, Serialize}; +use rkyv::Archive; use std::fmt::{Display, Formatter}; use std::path::Path; use std::sync::Arc; @@ -65,7 +66,7 @@ pub fn canonicalize_euca_uri(uri: &str) -> anyhow::Result<String> { Ok(format!("{EUCA_SCHEME}{clean}")) } -pub fn relative_path_from_euca<'a>(uri: &'a str) -> anyhow::Result<&'a str> { +pub fn relative_path_from_euca(uri: &str) -> anyhow::Result<&str> { let without_scheme = uri.strip_prefix(EUCA_SCHEME).unwrap_or(uri); let stripped = without_scheme.trim_start_matches('/'); @@ -87,7 +88,7 @@ pub fn relative_path_from_euca<'a>(uri: &'a str) -> anyhow::Result<&'a str> { /// ); /// assert_eq!(resource_ref.as_path().unwrap(), "models/cube.obj"); /// ``` -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)] pub enum ResourceReferenceType { /// The default type; Specifies there being no resource reference type. /// Typically creates errors, so watch out! @@ -124,7 +125,7 @@ impl Default for ResourceReferenceType { /// - In the runtime (with redback-runtime), it /// translates to `/home/tk/Downloads/Maze/resources/models/cube.obj` /// _(assuming the executable is at `/home/tk/Downloads/Maze/maze_runner.exe`)_. -#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)] pub struct ResourceReference { pub ref_type: ResourceReferenceType, } @@ -192,11 +193,6 @@ impl ResourceReference { } } - /// Converts an euca URI string into a path relative to the `resources/` directory. - pub fn relative_path_from_uri<'a>(uri: &'a str) -> anyhow::Result<&'a str> { - relative_path_from_euca(uri) - } - /// Creates a `ResourceReference` from a full path by extracting the part after "resources/". /// /// # Examples @@ -48,6 +48,8 @@ thiserror.workspace = true combine.workspace = true dyn-clone.workspace = true downcast-rs.workspace = true +wgpu = {workspace = true, features = ["serde"]} +rkyv.workspace = true [features] default = [] @@ -1,7 +1,8 @@ use crate::hierarchy::EntityTransformExt; use crate::physics::PhysicsState; -use crate::states::{SerializedMaterialCustomisation, SerializedMeshRenderer}; -use crate::utils::ResolveReference; +use crate::ser::model::EucalyptusModel; +use crate::states::{Label, SerializedMaterialCustomisation, SerializedMeshRenderer}; +use crate::utils::{AsFile, ResolveReference}; use downcast_rs::{Downcast, impl_downcast}; use dropbear_engine::asset::{ASSET_REGISTRY, Handle}; use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; @@ -24,33 +25,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; pub use typetag::*; -// todo: fix up this --------------- pub const DRAGGED_ASSET_ID: &str = "dragged_asset_reference"; -fn sanitize_resource_reference_for_scene_save( - reference: ResourceReference, - context: &str, -) -> ResourceReference { - match reference.ref_type { - ResourceReferenceType::Bytes(bytes) => { - log::warn!( - "Dropping in-memory byte resource for {} during scene save ({} bytes); use a file-backed asset for persistence", - context, - bytes.len() - ); - ResourceReference::default() - } - _ => reference, - } -} - -fn sanitize_optional_resource_reference_for_scene_save( - reference: Option<ResourceReference>, - context: &str, -) -> Option<ResourceReference> { - reference.map(|resource| sanitize_resource_reference_for_scene_save(resource, context)) -} - -// ------------------------ pub struct ComponentRegistry { /// Maps TypeId to ComponentDescriptor for quick lookups @@ -553,6 +528,44 @@ impl Component for MeshRenderer { Box::pin(async move { let import_scale = ser.import_scale.unwrap_or(1.0); + async fn load_model_from_reference( + model_ref: ResourceReference, + source_label: String, + graphics: Arc<SharedGraphicsContext>, + ) -> anyhow::Result<Handle<Model>> { + let path = model_ref.resolve()?; + let extension = path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.to_ascii_lowercase()); + + match extension.as_deref() { + Some("eucmdl") => { + let bytes = std::fs::read(&path)?; + let model = rkyv::from_bytes::<EucalyptusModel, rkyv::rancor::Error>(&bytes) + .map_err(|e| anyhow::anyhow!( + "Failed to deserialize .eucmdl '{}' into EucalyptusModel: {e}", + path.display() + ))?; + + let runtime_model = model.load(model_ref.clone(), graphics); + let mut registry = ASSET_REGISTRY.write(); + Ok(registry.add_model_with_label(source_label, runtime_model)) + } + _ => { + let buffer = std::fs::read(&path)?; + Model::load_from_memory_raw( + graphics, + buffer, + Some(model_ref.clone()), + None, + ASSET_REGISTRY.clone(), + ) + .await + } + } + } + let handle = match &ser.handle.ref_type { ResourceReferenceType::None => { log::debug!("ResourceReferenceType is None, setting to `Handle::NULL`"); @@ -560,19 +573,17 @@ impl Component for MeshRenderer { } ResourceReferenceType::File(reference) => { log::debug!("Loading model from file: {:?}", ser.handle); - let path = ser.handle.clone().resolve()?; - let buffer = std::fs::read(&path)?; - Model::load_from_memory_raw( + load_model_from_reference( + ser.handle.clone(), + reference.clone(), graphics.clone(), - buffer, - Some(ser.handle.clone()), - Some(reference), - ASSET_REGISTRY.clone(), ) .await? } ResourceReferenceType::Bytes(bytes) => { log::debug!("Loading model from bytes [Len: {}]", bytes.len()); + log::warn!("ResourceReferenceType::Bytes is unsupported and is highly NOT recommended to be used for serialization as it will explode the memory on save"); + log::warn!("Please serialize to a file when you get the chance to do so (could also be an editor bug...)"); Model::load_from_memory_raw( graphics.clone(), bytes, @@ -583,6 +594,8 @@ impl Component for MeshRenderer { .await? } ResourceReferenceType::ProcObj(obj) => { + log::warn!("ResourceReferenceType::Bytes is unsupported and is highly NOT recommended to be used for serialization as it will explode the memory on save"); + log::warn!("Please serialize to a file when you get the chance to do so (could also be an editor bug...)"); obj.build_model(graphics.clone(), None, None, ASSET_REGISTRY.clone()) } }; @@ -685,16 +698,82 @@ impl Component for MeshRenderer { } } - fn save(&self, _world: &World, _entity: Entity) -> Box<dyn SerializedComponent> { + fn save(&self, world: &World, entity: Entity) -> Box<dyn SerializedComponent> { + let entity_label_raw = world + .query_one::<&Label>(entity) + .get() + .map(|label| label.as_str().to_string()) + .unwrap_or_else(|_| "unnamed_entity".to_string()); + + let sanitize_segment = |segment: &str| -> String { + let mut result = String::with_capacity(segment.len()); + for ch in segment.chars() { + if ch.is_ascii_alphanumeric() { + result.push(ch.to_ascii_lowercase()); + } else if ch == '_' || ch == '-' { + result.push(ch); + } else { + result.push('_'); + } + } + + let trimmed = result.trim_matches('_').to_string(); + if trimmed.is_empty() { + "unnamed_entity".to_string() + } else { + trimmed + } + }; + + let proc_obj_type_name = |ty: &ProcObjType| -> &'static str { + match ty { + ProcObjType::Cuboid => "cuboid", + } + }; + + let entity_label = sanitize_segment(entity_label_raw.as_str()); + + let save_reference = |reference: ResourceReference, context: &str| -> ResourceReference { + match &reference.ref_type { + ResourceReferenceType::None | ResourceReferenceType::File(_) => reference, + ResourceReferenceType::Bytes(_) | ResourceReferenceType::ProcObj(_) => { + let desired_ref = match &reference.ref_type { + ResourceReferenceType::ProcObj(obj) => Some(format!( + "gen/{}.{}.eucmdl", + entity_label, + proc_obj_type_name(&obj.ty) + )), + _ => None, + }; + + match reference + .as_file(desired_ref) + .and_then(|path| ResourceReference::from_path(path.as_path())) + { + Ok(file_ref) => file_ref, + Err(err) => { + log::warn!( + "Failed to save {} as file-backed reference: {}", + context, + err + ); + ResourceReference::default() + } + } + } + } + }; + + let save_optional_reference = |reference: Option<ResourceReference>, context: &str| { + reference.map(|resource| save_reference(resource, context)) + }; + let asset = ASSET_REGISTRY.read(); let model = asset.get_model(self.model()); let (label, handle) = if let Some(model) = model.as_ref() { ( model.label.clone(), - sanitize_resource_reference_for_scene_save( - model.path.clone(), - "mesh renderer model handle", - ), + save_reference(model.path.clone(), "mesh renderer model handle"), ) } else { if !self.model().is_null() { @@ -722,7 +801,7 @@ impl Component for MeshRenderer { .get_texture(mat.diffuse_texture) .and_then(|t| t.reference.clone()) }; - let diffuse_texture = sanitize_optional_resource_reference_for_scene_save( + let diffuse_texture = save_optional_reference( diffuse_texture, "mesh renderer diffuse texture", ); @@ -735,7 +814,7 @@ impl Component for MeshRenderer { mat.normal_texture .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone())) }; - let normal_texture = sanitize_optional_resource_reference_for_scene_save( + let normal_texture = save_optional_reference( normal_texture, "mesh renderer normal texture", ); @@ -748,7 +827,7 @@ impl Component for MeshRenderer { mat.emissive_texture .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone())) }; - let emissive_texture = sanitize_optional_resource_reference_for_scene_save( + let emissive_texture = save_optional_reference( emissive_texture, "mesh renderer emissive texture", ); @@ -761,7 +840,7 @@ impl Component for MeshRenderer { mat.occlusion_texture .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone())) }; - let occlusion_texture = sanitize_optional_resource_reference_for_scene_save( + let occlusion_texture = save_optional_reference( occlusion_texture, "mesh renderer occlusion texture", ); @@ -775,7 +854,7 @@ impl Component for MeshRenderer { mat.metallic_roughness_texture .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone())) }; - let metallic_roughness_texture = sanitize_optional_resource_reference_for_scene_save( + let metallic_roughness_texture = save_optional_reference( metallic_roughness_texture, "mesh renderer metallic-roughness texture", ); @@ -1,3 +1,5 @@ +extern crate core; + pub mod animation; pub mod asset; pub mod camera; @@ -23,6 +25,7 @@ pub mod types; pub mod utils; pub mod billboard; pub mod ui; +pub mod ser; pub use dropbear_macro as macros; @@ -0,0 +1,31 @@ +use std::fmt::Display; +use std::string::ToString; + +pub mod model; + +pub enum SerializedType { + /// This is a `*.eucbin` file type. + GenericBinary, + + /// This is a `*.eucmdl` file type. + Model, +} + +impl Display for SerializedType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + SerializedType::GenericBinary => "eucbin".to_string(), + SerializedType::Model => "eucmdl".to_string(), + }; + write!(f, "{}", str) + } +} + +impl SerializedType { + pub fn iter_extensions() -> impl Iterator<Item = String> { + [ + Self::GenericBinary.to_string(), + Self::Model.to_string() + ].into_iter() + } +} @@ -0,0 +1,315 @@ +use std::sync::Arc; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use dropbear_engine::asset::{Handle, ASSET_REGISTRY}; +use dropbear_engine::graphics::SharedGraphicsContext; +use dropbear_engine::model::{AlphaMode, Animation, Material, Mesh, Model, ModelVertex, Node, Skin}; +use dropbear_engine::texture::{Texture, TextureWrapMode}; +use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; +use dropbear_engine::wgpu::util::DeviceExt; +use dropbear_engine::wgpu; + +use crate::utils::ResolveReference; + +/// The serialized format for a Model without all the buffers and stuff. +/// +/// This is stored in the file system as `*.eucmdl`. +#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Debug, serde::Serialize, serde::Deserialize)] +pub struct EucalyptusModel { + pub label: String, + pub meshes: Vec<EucalyptusMesh>, // this needs to be custom type because of wgpu buffers + pub materials: Vec<EucalyptusMaterial>, // same here + pub skins: Vec<Skin>, + pub animations: Vec<Animation>, + pub nodes: Vec<Node>, + pub morph_deltas: Vec<f32>, +} + +impl EucalyptusModel { + fn runtime_hash(&self, source: &ResourceReference) -> u64 { + let mut hasher = DefaultHasher::default(); + source.hash(&mut hasher); + self.label.hash(&mut hasher); + self.meshes.len().hash(&mut hasher); + self.materials.len().hash(&mut hasher); + self.nodes.len().hash(&mut hasher); + + for mesh in &self.meshes { + mesh.name.hash(&mut hasher); + mesh.num_elements.hash(&mut hasher); + mesh.vertices.len().hash(&mut hasher); + mesh.material.hash(&mut hasher); + } + + hasher.finish() + } + + /// Loads the [`EucalyptusModel`] as a [`Model`] by loading the buffers. + pub fn load(&self, source: ResourceReference, graphics: Arc<SharedGraphicsContext>) -> Model { + let materials = self + .materials + .iter() + .map(|material| material.load(graphics.clone())) + .collect::<Vec<_>>(); + + let meshes = self + .meshes + .iter() + .map(|mesh| mesh.load(graphics.clone())) + .collect::<Vec<_>>(); + + let morph_deltas_buffer = if self.morph_deltas.is_empty() { + None + } else { + Some(graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("model morph deltas buffer"), + contents: bytemuck::cast_slice(&self.morph_deltas), + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + })) + }; + + Model { + hash: self.runtime_hash(&source), + label: self.label.clone(), + path: source, + meshes, + materials, + skins: self.skins.clone(), + animations: self.animations.clone(), + nodes: self.nodes.clone(), + morph_deltas_buffer, + } + } +} + +impl From<Model> for EucalyptusModel { + fn from(value: Model) -> Self { + Self { + label: value.label.clone(), + meshes: value.meshes.into_iter().map(EucalyptusMesh::from).collect(), + materials: value.materials.into_iter().map(EucalyptusMaterial::from).collect(), + skins: value.skins, + animations: value.animations, + nodes: value.nodes, + morph_deltas: Vec::new(), + } + } +} + +#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Debug, serde::Serialize, serde::Deserialize)] +pub struct EucalyptusMesh { + pub name: String, + pub num_elements: u32, + pub material: usize, + pub vertices: Vec<ModelVertex>, + pub morph_deltas_offset: u32, + pub morph_target_count: u32, + pub morph_vertex_count: u32, + pub morph_default_weights: Vec<f32>, +} + +impl From<Mesh> for EucalyptusMesh { + fn from(value: Mesh) -> Self { + Self { + name: value.name, + num_elements: value.num_elements, + material: value.material, + vertices: value.vertices, + morph_deltas_offset: value.morph_deltas_offset, + morph_target_count: value.morph_target_count, + morph_vertex_count: value.morph_vertex_count, + morph_default_weights: value.morph_default_weights, + } + } +} + +impl EucalyptusMesh { + fn load(&self, graphics: Arc<SharedGraphicsContext>) -> Mesh { + let vertex_buffer = + graphics + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{} Vertex Buffer", self.name)), + contents: bytemuck::cast_slice(&self.vertices), + usage: wgpu::BufferUsages::VERTEX, + }); + + let index_count = self.num_elements.min(self.vertices.len() as u32); + let indices = (0..index_count).collect::<Vec<u32>>(); + let index_buffer = + graphics + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{} Index Buffer", self.name)), + contents: bytemuck::cast_slice(&indices), + usage: wgpu::BufferUsages::INDEX, + }); + + Mesh { + name: self.name.clone(), + vertex_buffer, + index_buffer, + num_elements: index_count, + material: self.material, + vertices: self.vertices.clone(), + morph_deltas_offset: self.morph_deltas_offset, + morph_target_count: self.morph_target_count, + morph_vertex_count: self.morph_vertex_count, + morph_default_weights: self.morph_default_weights.clone(), + } + } +} + +#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Debug, serde::Serialize, serde::Deserialize)] +pub struct EucalyptusMaterial { + pub name: String, + pub diffuse_texture: ResourceReference, // the file can either be a eucalyptus texture or a standard file + pub normal_texture: Option<ResourceReference>, // same + pub emissive_texture: Option<ResourceReference>, // same + pub metallic_roughness_texture: Option<ResourceReference>, // same + pub occlusion_texture: Option<ResourceReference>, // same + pub tint: [f32; 4], + pub emissive_factor: [f32; 3], + pub metallic_factor: f32, + pub roughness_factor: f32, + pub alpha_mode: AlphaMode, + pub alpha_cutoff: Option<f32>, + pub double_sided: bool, + pub occlusion_strength: f32, + pub normal_scale: f32, + pub uv_tiling: [f32; 2], + pub texture_tag: Option<String>, + pub wrap_mode: TextureWrapMode, +} + +impl From<Material> for EucalyptusMaterial { + fn from(value: Material) -> Self { + let get_texture = |tex: Option<Handle<Texture>>| -> Option<ResourceReference> { + if let Some(tex) = tex { + if let Some(t) = ASSET_REGISTRY.read().get_texture(tex) { + return t.reference.clone(); + } + } + + None + }; + + Self { + name: value.name, + diffuse_texture: get_texture(Some(value.diffuse_texture)).unwrap_or_default(), + normal_texture: get_texture(value.normal_texture), + emissive_texture: get_texture(value.emissive_texture), + metallic_roughness_texture: get_texture(value.metallic_roughness_texture), + occlusion_texture: get_texture(value.occlusion_texture), + tint: value.tint, + emissive_factor: value.emissive_factor, + metallic_factor: value.metallic_factor, + roughness_factor: value.roughness_factor, + alpha_mode: value.alpha_mode, + alpha_cutoff: value.alpha_cutoff, + double_sided: value.double_sided, + occlusion_strength: value.occlusion_strength, + normal_scale: value.normal_scale, + uv_tiling: value.uv_tiling, + texture_tag: value.texture_tag, + wrap_mode: value.wrap_mode, + } + } +} + +impl EucalyptusMaterial { + fn load_texture( + &self, + graphics: Arc<SharedGraphicsContext>, + reference: &ResourceReference, + suffix: &str, + ) -> Option<Handle<Texture>> { + match &reference.ref_type { + ResourceReferenceType::None => None, + ResourceReferenceType::File(_) => { + let path = reference.resolve().ok()?; + let bytes = std::fs::read(path).ok()?; + let label = format!("{}_{}", self.name, suffix); + let mut texture = dropbear_engine::texture::TextureBuilder::new(&graphics.device) + .from_bytes(graphics.clone(), bytes.as_slice()) + .label(label.as_str()) + .build(); + texture.reference = Some(reference.clone()); + + let mut registry = ASSET_REGISTRY.write(); + Some(registry.add_texture(texture)) + } + ResourceReferenceType::Bytes(bytes) => { + let label = format!("{}_{}", self.name, suffix); + let texture = dropbear_engine::texture::TextureBuilder::new(&graphics.device) + .from_bytes(graphics.clone(), bytes) + .label(label.as_str()) + .build(); + + let mut registry = ASSET_REGISTRY.write(); + Some(registry.add_texture(texture)) + } + ResourceReferenceType::ProcObj(_) => None, + } + } + + fn load(&self, graphics: Arc<SharedGraphicsContext>) -> Material { + let diffuse_texture = { + let maybe = self.load_texture(graphics.clone(), &self.diffuse_texture, "diffuse"); + if let Some(handle) = maybe { + handle + } else { + ASSET_REGISTRY.write().solid_texture_rgba8( + graphics.clone(), + [255, 255, 255, 255], + Some(Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix()), + ) + } + }; + + let normal_texture = self + .normal_texture + .as_ref() + .and_then(|reference| self.load_texture(graphics.clone(), reference, "normal")); + let emissive_texture = self + .emissive_texture + .as_ref() + .and_then(|reference| self.load_texture(graphics.clone(), reference, "emissive")); + let metallic_roughness_texture = self + .metallic_roughness_texture + .as_ref() + .and_then(|reference| self.load_texture(graphics.clone(), reference, "metallic_roughness")); + let occlusion_texture = self + .occlusion_texture + .as_ref() + .and_then(|reference| self.load_texture(graphics.clone(), reference, "occlusion")); + + let mut registry = ASSET_REGISTRY.write(); + let mut material = Material::new( + &mut registry, + graphics.clone(), + self.name.clone(), + diffuse_texture, + normal_texture, + emissive_texture, + metallic_roughness_texture, + occlusion_texture, + self.tint, + self.texture_tag.clone(), + ); + + material.emissive_factor = self.emissive_factor; + material.metallic_factor = self.metallic_factor; + material.roughness_factor = self.roughness_factor; + material.alpha_mode = self.alpha_mode; + material.alpha_cutoff = self.alpha_cutoff; + material.double_sided = self.double_sided; + material.occlusion_strength = self.occlusion_strength; + material.normal_scale = self.normal_scale; + material.uv_tiling = self.uv_tiling; + material.wrap_mode = self.wrap_mode; + + material.sync_uniform(&graphics); + material + } +} @@ -4,9 +4,13 @@ pub mod option; use crate::scripting::result::DropbearNativeResult; use crate::states::Node; +use crate::ser::model::{EucalyptusMaterial, EucalyptusMesh, EucalyptusModel}; use dropbear_engine::utils::{ResourceReference, ResourceReferenceType, relative_path_from_euca}; use jni::JNIEnv; use jni::objects::{JObject, JValue}; +use std::collections::hash_map::DefaultHasher; +use std::fs; +use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; use std::time::Duration; use winit::keyboard::KeyCode; @@ -301,6 +305,154 @@ impl ResolveReference for ResourceReference { } } +/// Converts a [`ResourceReference`] into a file. +pub trait AsFile { + /// Converts a [`ResourceReference`] into a file. + /// + /// # Different type's behaviours + /// - [`ResourceReferenceType::None`] => This just returns an error as it's impossible. + /// - [`ResourceReferenceType::File`] => Returns that file resolved. + /// - [`ResourceReferenceType::Bytes`] => Converts the bytes into a `*.eucbin`, with the name derived from `new_ref` (arg). + /// - [`ResourceReferenceType::ProcObj`] => Converts the vertices into a `*.eucmdl`, with the name derived from `new_ref` (arg). + fn as_file(&self, new_ref: Option<String>) -> anyhow::Result<PathBuf>; +} + +impl AsFile for ResourceReference { + fn as_file(&self, new_ref: Option<String>) -> anyhow::Result<PathBuf> { + match &self.ref_type { + ResourceReferenceType::None => { + anyhow::bail!("Cannot convert ResourceReferenceType::None to a file") + } + ResourceReferenceType::File(_) => { + let resolved = self.resolve()?; + if let Some(relative) = new_ref { + let root = resources_root_for_write()?; + let out_path = root.join(relative.trim_start_matches('/')); + if let Some(parent) = out_path.parent() { + fs::create_dir_all(parent)?; + } + Ok(out_path) + } else { + Ok(resolved) + } + } + ResourceReferenceType::Bytes(bytes) => { + let hash = hash_value(bytes); + let root = resources_root_for_write()?; + let out_path = root + .join("gen") + .join(format!("{hash:016x}.eucbin")); + + if let Some(parent) = out_path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(&out_path, bytes.as_ref())?; + + Ok(out_path) + } + ResourceReferenceType::ProcObj(obj) => { + let hash = hash_value(obj); + let out_path = if let Some(relative) = new_ref.as_ref() { + let root = resources_root_for_write()?; + root.join(relative.trim_start_matches('/')) + } else { + let root = resources_root_for_write()?; + root.join("gen") + .join(format!("{hash:016x}.eucmdl")) + }; + + let label_stem = out_path + .file_stem() + .and_then(|stem| stem.to_str()) + .unwrap_or("procedural_model") + .to_string(); + + let mut expanded_vertices = Vec::with_capacity(obj.indices.len()); + for &index in &obj.indices { + let vertex = obj + .vertices + .get(index as usize) + .ok_or_else(|| anyhow::anyhow!( + "Procedural object index {} is out of bounds for {} vertices", + index, + obj.vertices.len() + ))? + .clone(); + expanded_vertices.push(vertex); + } + + let model = EucalyptusModel { + label: label_stem.clone(), + meshes: vec![EucalyptusMesh { + name: format!("{label_stem}_mesh"), + num_elements: expanded_vertices.len() as u32, + material: 0, + vertices: expanded_vertices, + morph_deltas_offset: 0, + morph_target_count: 0, + morph_vertex_count: obj.indices.len() as u32, + morph_default_weights: Vec::new(), + }], + materials: vec![EucalyptusMaterial { + name: "procedural_material".to_string(), + diffuse_texture: ResourceReference::default(), + normal_texture: None, + emissive_texture: None, + metallic_roughness_texture: None, + occlusion_texture: None, + tint: [1.0, 1.0, 1.0, 1.0], + emissive_factor: [0.0, 0.0, 0.0], + metallic_factor: 1.0, + roughness_factor: 1.0, + alpha_mode: dropbear_engine::model::AlphaMode::Opaque, + alpha_cutoff: None, + double_sided: false, + occlusion_strength: 1.0, + normal_scale: 1.0, + uv_tiling: [1.0, 1.0], + texture_tag: Some("procedural_material".to_string()), + wrap_mode: dropbear_engine::texture::TextureWrapMode::Repeat, + }], + skins: Vec::new(), + animations: Vec::new(), + nodes: Vec::new(), + morph_deltas: Vec::new(), + }; + + if let Some(parent) = out_path.parent() { + fs::create_dir_all(parent)?; + } + + let serialized = rkyv::to_bytes::<rkyv::rancor::Error>(&model) + .map_err(|e| anyhow::anyhow!("Failed to serialize proc object model as rkyv bytes: {e}"))?; + fs::write(&out_path, serialized.as_ref())?; + + Ok(out_path) + } + } + } +} + +fn hash_value<T: Hash>(value: &T) -> u64 { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + hasher.finish() +} + +fn resources_root_for_write() -> anyhow::Result<PathBuf> { + #[cfg(feature = "editor")] + { + use crate::states::PROJECT; + + let project_path = PROJECT.read().project_path.clone(); + if !project_path.as_os_str().is_empty() { + return Ok(project_path.join("resources")); + } + } + + runtime_resources_dir() +} + fn try_resolve_resource_from_root(relative: &str, root: &Path) -> Option<PathBuf> { let resolved = root.join(relative); resolved.exists().then_some(resolved) @@ -57,6 +57,8 @@ serde.workspace = true bitflags.workspace = true downcast-rs.workspace = true puffin.workspace = true +image.workspace = true +rkyv.workspace = true [target.'cfg(unix)'.dependencies] daemonize = "0.5.0" @@ -3,6 +3,7 @@ use dropbear_engine::model::Model; use dropbear_engine::texture::TextureBuilder; use dropbear_engine::{graphics::NO_TEXTURE, utils::ResourceReference}; use egui_ltreeview::{Action, NodeBuilder, TreeViewBuilder}; +use eucalyptus_core::ser::model::EucalyptusModel; use eucalyptus_core::states::PROJECT; use eucalyptus_core::utils::ResolveReference; use hecs::Entity; @@ -196,6 +197,13 @@ impl<'a> EditorTabViewer<'a> { ); let is_model = Self::is_model_file(&entry.name); let is_texture = Self::is_texture_file(&entry.name); + let ext = entry + .path + .extension() + .and_then(|v| v.to_str()) + .map(|v| v.to_ascii_lowercase()); + let is_eucbin = matches!(ext.as_deref(), Some("eucbin")); + let is_eucmdl = matches!(ext.as_deref(), Some("eucmdl")); let entry_name = entry.name.clone(); let reference_for_menu = reference.clone(); let file_info = AssetNodeInfo { @@ -213,8 +221,8 @@ impl<'a> EditorTabViewer<'a> { self.asset_file_context_menu(cfg, ui, node_id, &file_info); ui.separator(); - if is_model { - if ui.button("Load to memory").clicked() { + if is_model || is_eucmdl { + if ui.button("Load model to memory").clicked() { ui.close(); self.queue_model_load( reference_for_menu.clone(), @@ -225,15 +233,37 @@ impl<'a> EditorTabViewer<'a> { } if is_texture { - if ui.button("Load to memory").clicked() { + if ui.button("Load texture to memory").clicked() { ui.close(); self.queue_texture_load( reference_for_menu.clone(), entry_name.clone(), + false, ); info!("Loading texture {}", entry_name); } } + + if is_eucbin { + if ui.button("Load as model to memory").clicked() { + ui.close(); + self.queue_model_load( + reference_for_menu.clone(), + entry_name.clone(), + ); + info!("Loading eucbin as model {}", entry_name); + } + + if ui.button("Load as texture to memory").clicked() { + ui.close(); + self.queue_texture_load( + reference_for_menu.clone(), + entry_name.clone(), + true, + ); + info!("Loading eucbin as texture {}", entry_name); + } + } }); builder.node(menu); } @@ -1208,20 +1238,39 @@ impl<'a> EditorTabViewer<'a> { queue.push(async move { let path = reference.resolve()?; let buffer = fs::read(&path)?; - let handle = match Model::load_from_memory_raw( - graphics.clone(), - buffer, - Some(reference.clone()), - Some(label.as_str()), - ASSET_REGISTRY.clone(), - ) - .await - { - Ok(v) => v, - Err(e) => { - eucalyptus_core::warn!("Unable to load model {}: {}", reference, e); - return Err(e); + let extension = path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.to_ascii_lowercase()); + + let handle = match extension.as_deref() { + Some("eucmdl") => { + let model = rkyv::from_bytes::<EucalyptusModel, rkyv::rancor::Error>(&buffer) + .map_err(|e| anyhow::anyhow!( + "Unable to deserialize .eucmdl model '{}': {}", + path.display(), + e + ))?; + + let runtime_model = model.load(reference.clone(), graphics.clone()); + let mut registry = ASSET_REGISTRY.write(); + registry.add_model_with_label(label.clone(), runtime_model) } + _ => match Model::load_from_memory_raw( + graphics.clone(), + buffer, + Some(reference.clone()), + Some(label.as_str()), + ASSET_REGISTRY.clone(), + ) + .await + { + Ok(v) => v, + Err(e) => { + eucalyptus_core::warn!("Unable to load model {}: {}", reference, e); + return Err(e); + } + }, }; let mut registry = ASSET_REGISTRY.write(); @@ -1231,7 +1280,7 @@ impl<'a> EditorTabViewer<'a> { }); } - fn queue_texture_load(&self, reference: ResourceReference, label: String) { + fn queue_texture_load(&self, reference: ResourceReference, label: String, strict_image_decode: bool) { if ASSET_REGISTRY .read() .get_texture_handle_by_reference(&reference) @@ -1246,6 +1295,19 @@ impl<'a> EditorTabViewer<'a> { queue.push(async move { let path = reference.resolve()?; let bytes = fs::read(&path)?; + + if strict_image_decode + && let Err(err) = image::load_from_memory(bytes.as_slice()) + { + let error = anyhow::anyhow!( + "'{}' is not a texture-compatible eucbin payload: {}", + path.display(), + err + ); + eucalyptus_core::warn!("{}", error); + return Err(error); + } + let mut texture = TextureBuilder::new(&graphics.device) .from_bytes(graphics.clone(), bytes.as_slice()) .label(label.as_str())