diff options
-rw-r--r-- | engine/Cargo.toml | 3 | ||||
-rw-r--r-- | engine/src/asset.rs | 777 | ||||
-rw-r--r-- | engine/src/image.rs | 184 | ||||
-rw-r--r-- | engine/src/lib.rs | 14 | ||||
-rw-r--r-- | engine/src/material.rs | 115 | ||||
-rw-r--r-- | engine/src/mesh.rs | 4 | ||||
-rw-r--r-- | engine/src/model.rs | 176 | ||||
-rw-r--r-- | engine/src/opengl/texture.rs | 11 | ||||
-rw-r--r-- | engine/src/renderer/opengl.rs | 129 | ||||
-rw-r--r-- | engine/src/texture.rs | 206 | ||||
-rw-r--r-- | engine/src/util.rs | 1 | ||||
-rw-r--r-- | engine/src/work_queue.rs | 44 |
12 files changed, 1346 insertions, 318 deletions
diff --git a/engine/Cargo.toml b/engine/Cargo.toml index f6cd5cf..a62f458 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -14,7 +14,8 @@ paste = "1.0.14" ecs = { path = "../ecs" } util-macros = { path = "../util-macros" } -[dependencies.image] +[dependencies.image_rs] version = "0.24.7" default-features = false features = ["png", "jpeg"] +package = "image" diff --git a/engine/src/asset.rs b/engine/src/asset.rs new file mode 100644 index 0000000..db4d23c --- /dev/null +++ b/engine/src/asset.rs @@ -0,0 +1,777 @@ +use std::any::{type_name, Any}; +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::HashMap; +use std::convert::Infallible; +use std::ffi::{OsStr, OsString}; +use std::fmt::{Debug, Display}; +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::{ + channel as mpsc_channel, + Receiver as MpscReceiver, + Sender as MpscSender, +}; +use std::sync::Arc; + +use ecs::phase::PRE_UPDATE as PRE_UPDATE_PHASE; +use ecs::sole::Single; +use ecs::Sole; + +use crate::work_queue::{Work, WorkQueue}; + +/// Asset label. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Label<'a> +{ + pub path: Cow<'a, Path>, + pub name: Option<Cow<'a, str>>, +} + +impl Label<'_> +{ + pub fn to_owned(&self) -> LabelOwned + { + LabelOwned { + path: self.path.to_path_buf(), + name: self.name.as_ref().map(|name| name.to_string()), + } + } +} + +impl<'a> From<&'a Path> for Label<'a> +{ + fn from(path: &'a Path) -> Self + { + Self { path: path.into(), name: None } + } +} + +impl From<PathBuf> for Label<'_> +{ + fn from(path: PathBuf) -> Self + { + Self { path: path.into(), name: None } + } +} + +impl<'a> From<&'a LabelOwned> for Label<'a> +{ + fn from(label: &'a LabelOwned) -> Self + { + Self { + path: (&label.path).into(), + name: label.name.as_ref().map(|name| Cow::Borrowed(name.as_str())), + } + } +} + +impl Display for Label<'_> +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + write!(formatter, "{}", self.path.display())?; + + if let Some(name) = &self.name { + formatter.write_str("::")?; + formatter.write_str(&name)?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LabelOwned +{ + pub path: PathBuf, + pub name: Option<String>, +} + +impl LabelOwned +{ + pub fn to_label(&self) -> Label<'_> + { + Label { + path: (&self.path).into(), + name: self.name.as_ref().map(|name| Cow::Borrowed(name.as_str())), + } + } +} + +impl Display for LabelOwned +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + write!(formatter, "{}", self.path.display())?; + + if let Some(name) = &self.name { + formatter.write_str("::")?; + formatter.write_str(&name)?; + } + + Ok(()) + } +} + +#[derive(Debug, Sole)] +pub struct Assets +{ + assets: Vec<StoredAsset>, + asset_lookup: RefCell<HashMap<LabelHash, LookupEntry>>, + importers: Vec<WrappedImporterFn>, + importer_lookup: HashMap<OsString, usize>, + import_work_queue: WorkQueue<ImportWorkUserData>, + import_work_msg_receiver: MpscReceiver<ImportWorkMessage>, + import_work_msg_sender: MpscSender<ImportWorkMessage>, +} + +impl Assets +{ + pub fn with_capacity(capacity: usize) -> Self + { + let (import_work_msg_sender, import_work_msg_receiver) = + mpsc_channel::<ImportWorkMessage>(); + + Self { + assets: Vec::with_capacity(capacity), + asset_lookup: RefCell::new(HashMap::with_capacity(capacity)), + importers: Vec::new(), + importer_lookup: HashMap::new(), + import_work_queue: WorkQueue::new(), + import_work_msg_receiver, + import_work_msg_sender, + } + } + + pub fn set_importer<'file_ext, AssetSettings, Err>( + &mut self, + file_extensions: impl IntoIterator<Item: Into<Cow<'file_ext, str>>>, + func: impl Fn(&mut Submitter<'_>, &Path, Option<&AssetSettings>) -> Result<(), Err>, + ) where + AssetSettings: 'static, + Err: std::error::Error + 'static, + { + self.importers.push(WrappedImporterFn::new(func)); + + let importer_index = self.importers.len() - 1; + + self.importer_lookup + .extend(file_extensions.into_iter().map(|file_ext| { + let file_ext: Cow<str> = file_ext.into(); + + (file_ext.into_owned().into(), importer_index) + })); + } + + #[tracing::instrument(skip_all, fields(asset_type=type_name::<Asset>()))] + pub fn get<'this, 'handle, Asset: 'static + Send + Sync>( + &'this self, + handle: &'handle Handle<Asset>, + ) -> Option<&'handle Asset> + where + 'this: 'handle, + { + let LookupEntry::Occupied(asset_index) = + *self.asset_lookup.borrow().get(&handle.id.label_hash)? + else { + return None; + }; + + let stored_asset = self.assets.get(asset_index).expect("Not possible"); + + let Some(asset) = stored_asset.strong.downcast_ref::<Asset>() else { + tracing::error!("Wrong asset type"); + return None; + }; + + Some(asset) + } + + #[tracing::instrument(skip(self))] + pub fn load<'i, Asset: 'static + Send + Sync>( + &self, + label: impl Into<Label<'i>> + Debug, + ) -> Handle<Asset> + { + let label = label.into(); + + let label_hash = LabelHash::new(&label); + + let mut asset_lookup = self.asset_lookup.borrow_mut(); + + if Self::is_pending(&asset_lookup, &label) { + return Handle::new(label_hash); + } + + let Some(lookup_entry) = asset_lookup.get(&label_hash) else { + self.add_import_work::<Infallible>( + &label, + label_hash, + None, + &mut asset_lookup, + ); + + return Handle::new(label_hash); + }; + + match *lookup_entry { + LookupEntry::Occupied(asset_index) => { + let stored_asset = self.assets.get(asset_index).expect("Not possible"); + + if stored_asset.strong.downcast_ref::<Asset>().is_none() { + tracing::error!("Wrong asset type {}", type_name::<Asset>()); + } + } + LookupEntry::Pending => {} + } + + Handle::new(label_hash) + } + + #[tracing::instrument(skip(self))] + pub fn load_with_settings<'i, Asset, AssetSettings>( + &self, + label: impl Into<Label<'i>> + Debug, + asset_settings: AssetSettings, + ) -> Handle<Asset> + where + Asset: Send + Sync + 'static, + AssetSettings: Send + Sync + Debug + 'static, + { + let label = label.into(); + + let label_hash = LabelHash::new(&label); + + let mut asset_lookup = self.asset_lookup.borrow_mut(); + + if Self::is_pending(&asset_lookup, &label) { + return Handle::new(label_hash); + } + + let Some(lookup_entry) = asset_lookup.get(&label_hash) else { + self.add_import_work::<AssetSettings>( + &label, + label_hash, + Some(asset_settings), + &mut asset_lookup, + ); + + return Handle::new(label_hash); + }; + + match *lookup_entry { + LookupEntry::Occupied(asset_index) => { + let stored_asset = self.assets.get(asset_index).expect("Not possible"); + + if stored_asset.strong.downcast_ref::<Asset>().is_none() { + tracing::error!( + "Wrong asset type {} for asset", + type_name::<Asset>() + ); + } + } + LookupEntry::Pending => {} + } + + Handle::new(label_hash) + } + + pub fn store_with_name<'name, Asset: 'static + Send + Sync>( + &mut self, + name: impl Into<Cow<'name, str>>, + asset: Asset, + ) -> Handle<Asset> + { + self.store_with_label( + Label { + path: Path::new("").into(), + name: Some(name.into()), + }, + asset, + ) + } + + #[tracing::instrument(skip(self, asset), fields(asset_type=type_name::<Asset>()))] + pub fn store_with_label<'i, Asset: 'static + Send + Sync>( + &mut self, + label: impl Into<Label<'i>> + Debug, + asset: Asset, + ) -> Handle<Asset> + { + let label = label.into(); + + let label_hash = LabelHash::new(&label); + + if matches!( + self.asset_lookup.get_mut().get(&label_hash), + Some(LookupEntry::Occupied(_)) + ) { + tracing::error!("Asset already exists"); + + return Handle::new(label_hash); + } + + tracing::debug!("Storing asset"); + + self.assets.push(StoredAsset::new(asset)); + + let index = self.assets.len() - 1; + + self.asset_lookup + .get_mut() + .insert(label_hash, LookupEntry::Occupied(index)); + + if label.name.is_some() { + let parent_asset_label_hash = + LabelHash::new(&Label { path: label.path, name: None }); + + if matches!( + self.asset_lookup.get_mut().get(&parent_asset_label_hash), + Some(LookupEntry::Pending) + ) { + self.asset_lookup.get_mut().remove(&parent_asset_label_hash); + } else if self + .asset_lookup + .get_mut() + .get(&parent_asset_label_hash) + .is_none() + { + self.assets + .push(StoredAsset::new::<Option<Infallible>>(None)); + + self.asset_lookup.get_mut().insert( + parent_asset_label_hash, + LookupEntry::Occupied(self.assets.len() - 1), + ); + } + } + + Handle::new(label_hash) + } + + fn is_pending(asset_lookup: &HashMap<LabelHash, LookupEntry>, label: &Label) -> bool + { + if label.name.is_some() { + if let Some(LookupEntry::Pending) = + asset_lookup.get(&LabelHash::new(&Label { + path: label.path.as_ref().into(), + name: None, + })) + { + return true; + } + } + + if let Some(LookupEntry::Pending) = asset_lookup.get(&LabelHash::new(label)) { + return true; + }; + + false + } + + fn add_import_work<AssetSettings>( + &self, + label: &Label<'_>, + label_hash: LabelHash, + asset_settings: Option<AssetSettings>, + asset_lookup: &mut HashMap<LabelHash, LookupEntry>, + ) where + AssetSettings: Any + Send + Sync, + { + let Some(file_ext) = label.path.extension() else { + tracing::error!("Asset file is missing a file extension"); + return; + }; + + let Some(importer) = self.get_importer(file_ext) else { + tracing::error!( + "No importer exists for asset file extension {}", + file_ext.to_string_lossy() + ); + return; + }; + + self.import_work_queue.add_work(Work { + func: |ImportWorkUserData { + import_work_msg_sender, + asset_path, + asset_settings, + importer, + }| { + if let Err(err) = importer.call( + import_work_msg_sender, + asset_path.as_path(), + asset_settings.as_deref(), + ) { + tracing::error!( + "Failed to load asset {}: {err}", + asset_path.display() + ); + } + }, + user_data: ImportWorkUserData { + import_work_msg_sender: self.import_work_msg_sender.clone(), + asset_path: label.path.to_path_buf(), + asset_settings: asset_settings.map(|asset_settings| { + Box::new(asset_settings) as Box<dyn Any + Send + Sync> + }), + importer: importer.clone(), + }, + }); + + asset_lookup.insert(label_hash, LookupEntry::Pending); + + if label.name.is_some() { + asset_lookup.insert( + LabelHash::new(&Label { + path: label.path.as_ref().into(), + name: None, + }), + LookupEntry::Pending, + ); + } + } + + fn get_importer(&self, file_ext: &OsStr) -> Option<&WrappedImporterFn> + { + let index = *self.importer_lookup.get(file_ext)?; + + Some(self.importers.get(index).expect("Not possible")) + } +} + +impl Default for Assets +{ + fn default() -> Self + { + Self::with_capacity(0) + } +} + +pub struct Submitter<'path> +{ + import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: &'path Path, +} + +impl Submitter<'_> +{ + pub fn submit_load_other<'label, Asset: Send + Sync + 'static>( + &self, + label: impl Into<Label<'label>>, + ) -> Handle<Asset> + { + let label = label.into(); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Load { + do_load: |assets, label, _asset_settings| { + let _ = assets.load::<Asset>(label); + }, + label: label.to_owned(), + asset_settings: None, + }); + + Handle::new(LabelHash::new(&label)) + } + + pub fn submit_load_other_with_settings<'label, Asset, AssetSettings>( + &self, + label: impl Into<Label<'label>>, + asset_settings: AssetSettings, + ) -> Handle<Asset> + where + Asset: Send + Sync + 'static, + AssetSettings: Send + Sync + Debug + 'static, + { + let label = label.into(); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Load { + do_load: |assets, label, asset_settings| { + let asset_settings = *asset_settings + .expect("Not possible") + .downcast::<AssetSettings>() + .expect("Not possible"); + + let _ = assets + .load_with_settings::<Asset, AssetSettings>(label, asset_settings); + }, + label: label.to_owned(), + asset_settings: Some(Box::new(asset_settings)), + }); + + Handle::new(LabelHash::new(&label)) + } + + pub fn submit_store<Asset: Send + Sync + 'static>( + &self, + asset: Asset, + ) -> Handle<Asset> + { + let label = LabelOwned { + path: self.asset_path.into(), + name: None, + }; + + let label_hash = LabelHash::new(&label.to_label()); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Store { + do_store: |assets, label, boxed_asset| { + let Ok(asset) = boxed_asset.downcast::<Asset>() else { + unreachable!(); + }; + + assets.store_with_label::<Asset>(&label, *asset); + }, + label, + asset: Box::new(asset), + }); + + Handle::new(label_hash) + } + + pub fn submit_store_named<Asset: Send + Sync + 'static>( + &self, + name: impl AsRef<str>, + asset: Asset, + ) -> Handle<Asset> + { + let label = LabelOwned { + path: self.asset_path.into(), + name: Some(name.as_ref().into()), + }; + + let label_hash = LabelHash::new(&label.to_label()); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Store { + do_store: |assets, label, boxed_asset| { + let Ok(asset) = boxed_asset.downcast::<Asset>() else { + unreachable!(); + }; + + assets.store_with_label::<Asset>(&label, *asset); + }, + label, + asset: Box::new(asset), + }); + + Handle::new(label_hash) + } +} + +/// Asset handle. +#[derive(Debug)] +pub struct Handle<Asset: 'static> +{ + id: Id, + _pd: PhantomData<Asset>, +} + +impl<Asset: 'static> Handle<Asset> +{ + pub fn id(&self) -> Id + { + self.id + } + + fn new(label_hash: LabelHash) -> Self + { + Self { + id: Id { label_hash }, + _pd: PhantomData, + } + } +} + +impl<Asset: 'static> Clone for Handle<Asset> +{ + fn clone(&self) -> Self + { + Self { id: self.id, _pd: PhantomData } + } +} + +/// Asset ID. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id +{ + label_hash: LabelHash, +} + +#[derive(Debug, thiserror::Error)] +enum ImporterError +{ + #[error("Settings has a incorrect type")] + IncorrectAssetSettingsType(PathBuf), + + #[error(transparent)] + Other(Box<dyn std::error::Error>), +} + +#[derive(Debug, Clone)] +struct WrappedImporterFn +{ + wrapper_func: fn( + MpscSender<ImportWorkMessage>, + &Path, + Option<&(dyn Any + Send + Sync)>, + ) -> Result<(), ImporterError>, +} + +impl WrappedImporterFn +{ + fn new<InnerFunc, AssetSettings, Err>(inner_func_param: InnerFunc) -> Self + where + InnerFunc: + Fn(&mut Submitter<'_>, &Path, Option<&AssetSettings>) -> Result<(), Err>, + AssetSettings: 'static, + Err: std::error::Error + 'static, + { + assert_eq!(size_of::<InnerFunc>(), 0); + + let wrapper_func = + |import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: &Path, + asset_settings: Option<&(dyn Any + Send + Sync)>| { + let inner_func = unsafe { std::mem::zeroed::<InnerFunc>() }; + + let asset_settings = asset_settings + .map(|asset_settings| { + asset_settings + .downcast_ref::<AssetSettings>() + .ok_or_else(|| { + ImporterError::IncorrectAssetSettingsType( + asset_path.to_path_buf(), + ) + }) + }) + .transpose()?; + + inner_func( + &mut Submitter { import_work_msg_sender, asset_path }, + asset_path, + asset_settings, + ) + .map_err(|err| ImporterError::Other(Box::new(err)))?; + + Ok(()) + }; + + std::mem::forget(inner_func_param); + + Self { wrapper_func } + } + + fn call( + &self, + import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: &Path, + asset_settings: Option<&(dyn Any + Send + Sync)>, + ) -> Result<(), ImporterError> + { + (self.wrapper_func)(import_work_msg_sender, asset_path, asset_settings) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct LabelHash(u64); + +impl LabelHash +{ + fn new(label: &Label<'_>) -> Self + { + let mut hasher = DefaultHasher::new(); + + label.hash(&mut hasher); + + Self(hasher.finish()) + } +} + +#[derive(Debug, Default)] +pub(crate) struct Extension +{ + pub assets: Assets, +} + +impl ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: ecs::extension::Collector<'_>) + { + let _ = collector.add_sole(self.assets); + + collector.add_system(*PRE_UPDATE_PHASE, add_received_assets); + } +} + +fn add_received_assets(mut assets: Single<Assets>) +{ + while let Some(import_work_msg) = assets.import_work_msg_receiver.try_recv().ok() { + match import_work_msg { + ImportWorkMessage::Store { do_store, label, asset } => { + do_store(&mut assets, label, asset); + } + ImportWorkMessage::Load { do_load, label, asset_settings } => { + do_load( + &assets, + Label { + path: label.path.as_path().into(), + name: label.name.as_deref().map(|name| name.into()), + }, + asset_settings, + ); + } + } + } +} + +#[derive(Debug)] +struct ImportWorkUserData +{ + import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: PathBuf, + asset_settings: Option<Box<dyn Any + Send + Sync>>, + importer: WrappedImporterFn, +} + +#[derive(Debug)] +enum ImportWorkMessage +{ + Store + { + do_store: fn(&mut Assets, LabelOwned, Box<dyn Any + Send + Sync>), + label: LabelOwned, + asset: Box<dyn Any + Send + Sync>, + }, + + Load + { + do_load: fn(&Assets, Label<'_>, Option<Box<dyn Any + Send + Sync>>), + label: LabelOwned, + asset_settings: Option<Box<dyn Any + Send + Sync>>, + }, +} + +#[derive(Debug, Clone, Copy)] +enum LookupEntry +{ + Occupied(usize), + Pending, +} + +#[derive(Debug)] +struct StoredAsset +{ + strong: Arc<dyn Any + Send + Sync>, +} + +impl StoredAsset +{ + fn new<Asset: Any + Send + Sync>(asset: Asset) -> Self + { + let strong = Arc::new(asset); + + Self { strong } + } +} diff --git a/engine/src/image.rs b/engine/src/image.rs new file mode 100644 index 0000000..a4513ae --- /dev/null +++ b/engine/src/image.rs @@ -0,0 +1,184 @@ +use std::fs::File; +use std::io::BufReader; +use std::path::Path; + +use image_rs::GenericImageView as _; + +use crate::asset::{Assets, Submitter as AssetSubmitter}; +use crate::color::Color; +use crate::data_types::dimens::Dimens; +use crate::util::builder; + +#[derive(Debug)] +pub struct Image +{ + inner: image_rs::DynamicImage, +} + +impl Image +{ + pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> + { + let buffered_reader = + BufReader::new(File::open(&path).map_err(Error::ReadFailed)?); + + let image_reader = image_rs::io::Reader::with_format( + buffered_reader, + image_rs::ImageFormat::from_path(path) + .map_err(|_| Error::UnsupportedFormat)?, + ); + + Ok(Self { + inner: image_reader + .decode() + .map_err(|err| Error::DecodeFailed(DecodeError(err)))?, + }) + } + + pub fn from_color(dimens: impl Into<Dimens<u32>>, color: impl Into<Color<u8>>) + -> Self + { + let dimens: Dimens<u32> = dimens.into(); + + let color: Color<u8> = color.into(); + + Self { + inner: image_rs::RgbImage::from_pixel( + dimens.width, + dimens.height, + image_rs::Rgb([color.red, color.green, color.blue]), + ) + .into(), + } + } + + pub fn dimensions(&self) -> Dimens<u32> + { + self.inner.dimensions().into() + } + + pub fn color_type(&self) -> ColorType + { + self.inner.color().into() + } + + pub fn as_bytes(&self) -> &[u8] + { + self.inner.as_bytes() + } +} + +builder! { +#[builder(name = SettingsBuilder, derives=(Debug, Clone))] +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct Settings { +} +} + +impl Settings +{ + pub fn builder() -> SettingsBuilder + { + SettingsBuilder::default() + } +} + +impl Default for SettingsBuilder +{ + fn default() -> Self + { + Settings::default().into() + } +} + +/// An enumeration over supported color types and bit depths +#[derive(Copy, PartialEq, Eq, Debug, Clone, Hash)] +#[non_exhaustive] +pub enum ColorType +{ + /// Pixel is 8-bit luminance + L8, + + /// Pixel is 8-bit luminance with an alpha channel + La8, + + /// Pixel contains 8-bit R, G and B channels + Rgb8, + + /// Pixel is 8-bit RGB with an alpha channel + Rgba8, + + /// Pixel is 16-bit luminance + L16, + + /// Pixel is 16-bit luminance with an alpha channel + La16, + + /// Pixel is 16-bit RGB + Rgb16, + + /// Pixel is 16-bit RGBA + Rgba16, + + /// Pixel is 32-bit float RGB + Rgb32F, + + /// Pixel is 32-bit float RGBA + Rgba32F, +} + +impl From<image_rs::ColorType> for ColorType +{ + fn from(color_type: image_rs::ColorType) -> Self + { + match color_type { + image_rs::ColorType::L8 => Self::L8, + image_rs::ColorType::La8 => Self::La8, + image_rs::ColorType::Rgb8 => Self::Rgb8, + image_rs::ColorType::Rgba8 => Self::Rgba8, + image_rs::ColorType::L16 => Self::L16, + image_rs::ColorType::La16 => Self::La16, + image_rs::ColorType::Rgb16 => Self::Rgb16, + image_rs::ColorType::Rgba16 => Self::Rgba16, + image_rs::ColorType::Rgb32F => Self::Rgb32F, + image_rs::ColorType::Rgba32F => Self::Rgba32F, + _ => { + panic!("Unrecognized image_rs::ColorType variant"); + } + } + } +} + +pub fn set_asset_importers(assets: &mut Assets) +{ + assets.set_importer::<_, _>(["png", "jpg"], import_asset); +} + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error("Failed to read image file")] + ReadFailed(#[source] std::io::Error), + + #[error("Failed to decode image")] + DecodeFailed(DecodeError), + + #[error("Unsupported image format")] + UnsupportedFormat, +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct DecodeError(image_rs::ImageError); + +fn import_asset( + asset_submitter: &mut AssetSubmitter<'_>, + path: &Path, + _settings: Option<&'_ Settings>, +) -> Result<(), Error> +{ + asset_submitter.submit_store::<Image>(Image::open(path)?); + + Ok(()) +} diff --git a/engine/src/lib.rs b/engine/src/lib.rs index c537e06..6ccba53 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -10,22 +10,27 @@ use ecs::system::{Into, System}; use ecs::uid::Uid; use ecs::{SoleAlreadyExistsError, World}; +use crate::asset::{Assets, Extension as AssetExtension}; use crate::delta_time::{update as update_delta_time, DeltaTime, LastUpdate}; mod opengl; mod util; +mod work_queue; +pub mod asset; pub mod camera; pub mod collision; pub mod data_types; pub mod delta_time; pub mod draw_flags; pub mod file_format; +pub mod image; pub mod input; pub mod lighting; pub mod material; pub mod math; pub mod mesh; +pub mod model; pub mod projection; pub mod renderer; pub mod texture; @@ -37,6 +42,8 @@ pub extern crate ecs; pub(crate) use crate::data_types::matrix; pub use crate::data_types::{color, vector}; +const INITIAL_ASSET_CAPACITY: usize = 128; + #[derive(Debug)] pub struct Engine { @@ -60,6 +67,13 @@ impl Engine .initialize((LastUpdate::default(),)), ); + let mut assets = Assets::with_capacity(INITIAL_ASSET_CAPACITY); + + crate::model::set_asset_importers(&mut assets); + crate::image::set_asset_importers(&mut assets); + + world.add_extension(AssetExtension { assets }); + Self { world } } diff --git a/engine/src/material.rs b/engine/src/material.rs index e368519..fd4aa41 100644 --- a/engine/src/material.rs +++ b/engine/src/material.rs @@ -1,21 +1,19 @@ use ecs::Component; use crate::color::Color; -use crate::data_types::dimens::Dimens; -use crate::texture::{Id as TextureId, Texture}; +use crate::texture::Texture; use crate::util::builder; -#[derive(Debug, Clone, Component)] +#[derive(Debug, Clone)] #[non_exhaustive] pub struct Material { pub ambient: Color<f32>, pub diffuse: Color<f32>, pub specular: Color<f32>, - pub ambient_map: TextureId, - pub diffuse_map: TextureId, - pub specular_map: TextureId, - pub textures: Vec<Texture>, + pub ambient_map: Option<Texture>, + pub diffuse_map: Option<Texture>, + pub specular_map: Option<Texture>, pub shininess: f32, } @@ -27,17 +25,24 @@ impl Material } } +impl Default for Material +{ + fn default() -> Self + { + Self::builder().build() + } +} + /// [`Material`] builder. #[derive(Debug, Clone)] pub struct Builder { - ambient: Option<Color<f32>>, - diffuse: Option<Color<f32>>, - specular: Option<Color<f32>>, - ambient_map: Option<TextureId>, - diffuse_map: Option<TextureId>, - specular_map: Option<TextureId>, - textures: Vec<Texture>, + ambient: Color<f32>, + diffuse: Color<f32>, + specular: Color<f32>, + ambient_map: Option<Texture>, + diffuse_map: Option<Texture>, + specular_map: Option<Texture>, shininess: f32, } @@ -47,13 +52,12 @@ impl Builder pub fn new() -> Self { Self { - ambient: None, - diffuse: None, - specular: None, + ambient: Color::WHITE_F32, + diffuse: Color::WHITE_F32, + specular: Color::WHITE_F32, ambient_map: None, diffuse_map: None, specular_map: None, - textures: Vec::new(), shininess: 32.0, } } @@ -61,7 +65,7 @@ impl Builder #[must_use] pub fn ambient(mut self, ambient: Color<f32>) -> Self { - self.ambient = Some(ambient); + self.ambient = ambient; self } @@ -69,7 +73,7 @@ impl Builder #[must_use] pub fn diffuse(mut self, diffuse: Color<f32>) -> Self { - self.diffuse = Some(diffuse); + self.diffuse = diffuse; self } @@ -77,13 +81,13 @@ impl Builder #[must_use] pub fn specular(mut self, specular: Color<f32>) -> Self { - self.specular = Some(specular); + self.specular = specular; self } #[must_use] - pub fn ambient_map(mut self, ambient_map: TextureId) -> Self + pub fn ambient_map(mut self, ambient_map: Texture) -> Self { self.ambient_map = Some(ambient_map); @@ -91,7 +95,7 @@ impl Builder } #[must_use] - pub fn diffuse_map(mut self, diffuse_map: TextureId) -> Self + pub fn diffuse_map(mut self, diffuse_map: Texture) -> Self { self.diffuse_map = Some(diffuse_map); @@ -99,7 +103,7 @@ impl Builder } #[must_use] - pub fn specular_map(mut self, specular_map: TextureId) -> Self + pub fn specular_map(mut self, specular_map: Texture) -> Self { self.specular_map = Some(specular_map); @@ -107,22 +111,6 @@ impl Builder } #[must_use] - pub fn textures(mut self, textures: impl IntoIterator<Item = Texture>) -> Self - { - self.textures = textures.into_iter().collect(); - - self - } - - #[must_use] - pub fn texture(mut self, texture: Texture) -> Self - { - self.textures.push(texture); - - self - } - - #[must_use] pub fn shininess(mut self, shininess: f32) -> Self { self.shininess = shininess; @@ -135,43 +123,15 @@ impl Builder /// # Panics /// Will panic if no ambient map, diffuse map or specular map is set. #[must_use] - pub fn build(mut self) -> Material + pub fn build(self) -> Material { - let ambient_map = self.ambient_map.unwrap_or_else(|| { - let texture = create_1x1_white_texture(); - let texture_id = texture.id(); - - self.textures.push(texture); - - texture_id - }); - - let diffuse_map = self.diffuse_map.unwrap_or_else(|| { - let texture = create_1x1_white_texture(); - let texture_id = texture.id(); - - self.textures.push(texture); - - texture_id - }); - - let specular_map = self.specular_map.unwrap_or_else(|| { - let texture = create_1x1_white_texture(); - let texture_id = texture.id(); - - self.textures.push(texture); - - texture_id - }); - Material { - ambient: self.ambient.unwrap_or(Color::WHITE_F32), - diffuse: self.diffuse.unwrap_or(Color::WHITE_F32), - specular: self.specular.unwrap_or(Color::WHITE_F32), - ambient_map, - diffuse_map, - specular_map, - textures: self.textures, + ambient: self.ambient, + diffuse: self.diffuse, + specular: self.specular, + ambient_map: self.ambient_map, + diffuse_map: self.diffuse_map, + specular_map: self.specular_map, shininess: self.shininess, } } @@ -206,8 +166,3 @@ impl Flags FlagsBuilder::default() } } - -fn create_1x1_white_texture() -> Texture -{ - Texture::new_from_color(&Dimens { width: 1, height: 1 }, &Color::WHITE_U8) -} diff --git a/engine/src/mesh.rs b/engine/src/mesh.rs index 91d199e..338a0df 100644 --- a/engine/src/mesh.rs +++ b/engine/src/mesh.rs @@ -1,11 +1,9 @@ -use ecs::Component; - use crate::util::builder; use crate::vector::{Vec2, Vec3}; pub mod cube; -#[derive(Debug, Clone, Component)] +#[derive(Debug, Clone, Default)] pub struct Mesh { vertices: Vec<Vertex>, diff --git a/engine/src/model.rs b/engine/src/model.rs new file mode 100644 index 0000000..9f5840c --- /dev/null +++ b/engine/src/model.rs @@ -0,0 +1,176 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::fs::read_to_string; +use std::path::Path; + +use ecs::Component; + +use crate::asset::{Assets, Handle as AssetHandle, Submitter as AssetSubmitter}; +use crate::material::Material; +use crate::mesh::Mesh; +use crate::texture::Texture; + +#[derive(Debug, Clone, Component)] +#[non_exhaustive] +pub struct Model +{ + pub asset_handle: AssetHandle<Data>, +} + +impl Model +{ + pub fn new(asset_handle: AssetHandle<Data>) -> Self + { + Self { asset_handle } + } +} + +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct Data +{ + pub mesh: Mesh, + pub materials: HashMap<String, Material>, +} + +impl Data +{ + pub fn builder() -> DataBuilder + { + DataBuilder::default() + } +} + +#[derive(Debug, Default, Clone)] +pub struct DataBuilder +{ + mesh: Mesh, + materials: HashMap<String, Material>, +} + +impl DataBuilder +{ + pub fn mesh(mut self, mesh: Mesh) -> Self + { + self.mesh = mesh; + + self + } + + pub fn material<'name>( + mut self, + name: impl Into<Cow<'name, str>>, + material: Material, + ) -> Self + { + self.materials.insert(name.into().into_owned(), material); + + self + } + + pub fn build(self) -> Data + { + Data { + mesh: self.mesh, + materials: self.materials, + } + } +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct Settings {} + +#[derive(Debug, thiserror::Error)] +enum Error +{ + #[error("Failed to read model file")] + ReadModelFileFailed(#[source] std::io::Error), + + #[error("Failed to read material file")] + ReadMaterialFileFailed(#[source] std::io::Error), + + #[error("Failed to parse model file")] + ParsingFailed(#[from] ParsingError), +} + +pub fn set_asset_importers(assets: &mut Assets) +{ + assets.set_importer(["obj"], import_wavefront_obj_asset); +} + +#[derive(Debug, thiserror::Error)] +enum ParsingError +{ + #[error(transparent)] + Obj(#[from] crate::file_format::wavefront::obj::Error), + + #[error(transparent)] + Mtl(#[from] crate::file_format::wavefront::mtl::Error), +} + +fn import_wavefront_obj_asset( + asset_submitter: &mut AssetSubmitter<'_>, + path: &Path, + _settings: Option<&'_ Settings>, +) -> Result<(), Error> +{ + let obj = crate::file_format::wavefront::obj::parse( + &read_to_string(path).map_err(Error::ReadModelFileFailed)?, + ) + .map_err(|err| Error::ParsingFailed(ParsingError::Obj(err)))?; + + let mesh = obj + .to_mesh() + .map_err(|err| Error::ParsingFailed(ParsingError::Obj(err)))?; + + let mut materials = + HashMap::<String, Material>::with_capacity(obj.mtl_libs.iter().flatten().count()); + + for mtl_lib_path in obj.mtl_libs.iter().flatten() { + materials.extend(import_mtl(asset_submitter, &mtl_lib_path)?); + } + + asset_submitter.submit_store(Data { mesh, materials }); + + Ok(()) +} + +fn import_mtl<'a>( + asset_submitter: &'a AssetSubmitter<'_>, + path: &Path, +) -> Result<impl Iterator<Item = (String, Material)> + 'a, Error> +{ + let named_materials = crate::file_format::wavefront::mtl::parse( + &read_to_string(path).map_err(Error::ReadMaterialFileFailed)?, + ) + .map_err(|err| Error::ParsingFailed(ParsingError::Mtl(err)))?; + + Ok(named_materials.into_iter().map(|named_material| { + let mut material_builder = Material::builder() + .ambient(named_material.ambient) + .diffuse(named_material.diffuse) + .specular(named_material.specular) + .shininess(named_material.shininess); + + if let Some(ambient_map) = named_material.ambient_map { + material_builder = material_builder.ambient_map(Texture::new( + asset_submitter.submit_load_other(ambient_map.path.as_path()), + )); + } + + if let Some(diffuse_map) = named_material.diffuse_map { + material_builder = material_builder.diffuse_map(Texture::new( + asset_submitter.submit_load_other(diffuse_map.path.as_path()), + )); + } + + if let Some(specular_map) = named_material.specular_map { + material_builder = material_builder.specular_map(Texture::new( + asset_submitter.submit_load_other(specular_map.path.as_path()), + )); + } + + (named_material.name, material_builder.build()) + })) +} diff --git a/engine/src/opengl/texture.rs b/engine/src/opengl/texture.rs index 52c8554..ffc978b 100644 --- a/engine/src/opengl/texture.rs +++ b/engine/src/opengl/texture.rs @@ -223,18 +223,9 @@ macro_rules! texture_unit_enum { )* } } - - pub fn from_num(num: usize) -> Option<Self> { - match num { - #( - N => Some(Self::No~N), - )* - _ => None - } - } } }); }; } -texture_unit_enum!(cnt = 31); +texture_unit_enum!(cnt = 3); diff --git a/engine/src/renderer/opengl.rs b/engine/src/renderer/opengl.rs index 858a899..3be892e 100644 --- a/engine/src/renderer/opengl.rs +++ b/engine/src/renderer/opengl.rs @@ -15,14 +15,17 @@ use ecs::sole::Single; use ecs::system::{Into as _, System}; use ecs::{Component, Query}; +use crate::asset::{Assets, Id as AssetId}; use crate::camera::{Active as ActiveCamera, Camera}; use crate::color::Color; use crate::data_types::dimens::Dimens; use crate::draw_flags::{DrawFlags, NoDraw, PolygonModeConfig}; +use crate::image::{ColorType as ImageColorType, Image}; use crate::lighting::{DirectionalLight, GlobalLight, PointLight}; use crate::material::{Flags as MaterialFlags, Material}; use crate::matrix::Matrix; use crate::mesh::Mesh; +use crate::model::Model; use crate::opengl::buffer::{Buffer, Usage as BufferUsage}; use crate::opengl::debug::{ enable_debug_output, @@ -45,6 +48,7 @@ use crate::opengl::shader::{ }; use crate::opengl::texture::{ set_active_texture_unit, + PixelDataFormat as GlTexturePixelDataFormat, Texture as GlTexture, TextureUnit, }; @@ -64,7 +68,7 @@ use crate::opengl::{ use crate::projection::{ClipVolume, Projection}; use crate::renderer::opengl::vertex::{AttributeComponentType, Vertex}; use crate::renderer::RENDER_PHASE; -use crate::texture::{Id as TextureId, Texture}; +use crate::texture::Properties as TextureProperties; use crate::transform::{Scale, WorldPosition}; use crate::util::{defer, Defer, RefOrValue}; use crate::vector::{Vec2, Vec3}; @@ -72,9 +76,12 @@ use crate::window::Window; mod vertex; +const AMBIENT_MAP_TEXTURE_UNIT: TextureUnit = TextureUnit::No0; +const DIFFUSE_MAP_TEXTURE_UNIT: TextureUnit = TextureUnit::No1; +const SPECULAR_MAP_TEXTURE_UNIT: TextureUnit = TextureUnit::No2; + type RenderableEntity<'a> = ( - &'a Mesh, - &'a Material, + &'a Model, Option<&'a MaterialFlags>, Option<&'a WorldPosition>, Option<&'a Scale>, @@ -134,6 +141,7 @@ fn initialize(window: Single<Window>) enable(Capability::MultiSample); } +#[tracing::instrument(skip_all)] #[allow(clippy::too_many_arguments)] fn render( query: Query<RenderableEntity<'_>, (Without<NoDraw>,)>, @@ -142,6 +150,7 @@ fn render( camera_query: Query<(&Camera, &WorldPosition, &ActiveCamera)>, window: Single<Window>, global_light: Single<GlobalLight>, + assets: Single<Assets>, mut gl_objects: Local<GlobalGlObjects>, mut actions: Actions, ) @@ -156,6 +165,7 @@ fn render( let GlobalGlObjects { shader_program, textures: gl_textures, + default_1x1_texture: default_1x1_gl_texture, } = &mut *gl_objects; let shader_program = @@ -163,18 +173,23 @@ fn render( clear_buffers(BufferClearMask::COLOR | BufferClearMask::DEPTH); - for ( + 'subject_loop: for ( euid, - (mesh, material, material_flags, position, scale, draw_flags, gl_objects), + (model, material_flags, position, scale, draw_flags, gl_objects), ) in query.iter_with_euids() { + let Some(model_data) = assets.get(&model.asset_handle) else { + tracing::trace!("Missing model asset"); + continue; + }; + let material_flags = material_flags .map(|material_flags| material_flags.clone()) .unwrap_or_default(); let gl_objs = match gl_objects.as_deref() { Some(gl_objs) => RefOrValue::Ref(gl_objs), - None => RefOrValue::Value(Some(GlObjects::new(&mesh))), + None => RefOrValue::Value(Some(GlObjects::new(&model_data.mesh))), }; defer!(|gl_objs| { @@ -194,6 +209,22 @@ fn render( window.size().expect("Failed to get window size"), ); + if model_data.materials.len() > 1 { + tracing::warn!(concat!( + "Multiple model materials are not supported ", + "so only the first material will be used" + )); + } + + let material = match model_data.materials.values().next() { + Some(material) => material, + None => { + tracing::warn!("Model has no materials. Using default material"); + + &Material::default() + } + }; + apply_light( &material, &material_flags, @@ -208,12 +239,48 @@ fn render( &camera_world_pos, ); - for (index, texture) in material.textures.iter().enumerate() { - let gl_texture = gl_textures - .entry(texture.id()) - .or_insert_with(|| create_gl_texture(texture)); + let material_texture_maps = [ + (&material.ambient_map, AMBIENT_MAP_TEXTURE_UNIT), + (&material.diffuse_map, DIFFUSE_MAP_TEXTURE_UNIT), + (&material.specular_map, SPECULAR_MAP_TEXTURE_UNIT), + ]; + + for (texture, texture_unit) in material_texture_maps { + let Some(texture) = texture else { + let gl_texture = default_1x1_gl_texture.get_or_insert_with(|| { + create_gl_texture( + &Image::from_color(1, Color::WHITE_U8), + &TextureProperties::default(), + ) + }); + + set_active_texture_unit(texture_unit); - let texture_unit = TextureUnit::from_num(index).expect("Too many textures"); + gl_texture.bind(); + + continue; + }; + + let texture_image_asset_id = texture.asset_handle.id(); + + let gl_texture = match gl_textures.get(&texture_image_asset_id) { + Some(gl_texture) => gl_texture, + None => { + let Some(image) = assets.get::<Image>(&texture.asset_handle) else { + tracing::trace!("Missing texture asset"); + continue 'subject_loop; + }; + + gl_textures.insert( + texture_image_asset_id, + create_gl_texture(image, &texture.properties), + ); + + gl_textures + .get(&texture.asset_handle.id()) + .expect("Not possible") + } + }; set_active_texture_unit(texture_unit); @@ -246,7 +313,8 @@ fn render( struct GlobalGlObjects { shader_program: Option<GlShaderProgram>, - textures: HashMap<TextureId, GlTexture>, + textures: HashMap<AssetId, GlTexture>, + default_1x1_texture: Option<GlTexture>, } fn set_viewport(position: Vec2<u32>, size: Dimens<u32>) @@ -273,17 +341,23 @@ fn draw_mesh(gl_objects: &GlObjects) } } -fn create_gl_texture(texture: &Texture) -> GlTexture +fn create_gl_texture(image: &Image, texture_properties: &TextureProperties) -> GlTexture { let mut gl_texture = GlTexture::new(); gl_texture.generate( - *texture.dimensions(), - texture.image().as_bytes(), - texture.pixel_data_format(), + image.dimensions(), + image.as_bytes(), + match image.color_type() { + ImageColorType::Rgb8 => GlTexturePixelDataFormat::Rgb8, + ImageColorType::Rgba8 => GlTexturePixelDataFormat::Rgba8, + _ => { + unimplemented!(); + } + }, ); - gl_texture.apply_properties(texture.properties()); + gl_texture.apply_properties(texture_properties); gl_texture } @@ -560,29 +634,18 @@ fn apply_light<'point_light>( gl_shader_program .set_uniform(c"material.specular", &Vec3::from(material.specular.clone())); - let texture_map = material - .textures - .iter() - .enumerate() - .map(|(index, texture)| (texture.id(), index)) - .collect::<HashMap<_, _>>(); - #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform( - c"material.ambient_map", - &(*texture_map.get(&material.ambient_map).unwrap() as i32), - ); + gl_shader_program + .set_uniform(c"material.ambient_map", &(AMBIENT_MAP_TEXTURE_UNIT as i32)); #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform( - c"material.diffuse_map", - &(*texture_map.get(&material.diffuse_map).unwrap() as i32), - ); + gl_shader_program + .set_uniform(c"material.diffuse_map", &(DIFFUSE_MAP_TEXTURE_UNIT as i32)); #[allow(clippy::cast_possible_wrap)] gl_shader_program.set_uniform( c"material.specular_map", - &(*texture_map.get(&material.specular_map).unwrap() as i32), + &(SPECULAR_MAP_TEXTURE_UNIT as i32), ); gl_shader_program.set_uniform(c"material.shininess", &material.shininess); diff --git a/engine/src/texture.rs b/engine/src/texture.rs index 4a4fe86..f119ac3 100644 --- a/engine/src/texture.rs +++ b/engine/src/texture.rs @@ -1,17 +1,7 @@ -use std::fmt::Display; -use std::path::Path; -use std::sync::atomic::{AtomicU32, Ordering}; - -use image::io::Reader as ImageReader; -use image::{DynamicImage, ImageError, Rgb, RgbImage}; - -use crate::color::Color; -use crate::data_types::dimens::Dimens; -use crate::opengl::texture::PixelDataFormat; +use crate::asset::Handle as AssetHandle; +use crate::image::Image; use crate::util::builder; -static NEXT_ID: AtomicU32 = AtomicU32::new(0); - mod reexports { pub use crate::opengl::texture::{Filtering, Wrapping}; @@ -20,175 +10,32 @@ mod reexports pub use reexports::*; #[derive(Debug, Clone)] +#[non_exhaustive] pub struct Texture { - id: Id, - image: DynamicImage, - pixel_data_format: PixelDataFormat, - dimensions: Dimens<u32>, - properties: Properties, + pub asset_handle: AssetHandle<Image>, + pub properties: Properties, } impl Texture { - pub fn builder() -> Builder - { - Builder::default() - } - - /// Opens a texture image. - /// - /// # Errors - /// Will return `Err` if: - /// - Opening the image fails - /// - The image data is not 8-bit/color RGB - pub fn open(path: &Path) -> Result<Self, Error> - { - Self::builder().open(path) - } - - #[must_use] - pub fn new_from_color(dimensions: &Dimens<u32>, color: &Color<u8>) -> Self - { - Self::builder().build_with_single_color(dimensions, color) - } - - #[must_use] - pub fn id(&self) -> Id - { - self.id - } - - #[must_use] - pub fn properties(&self) -> &Properties - { - &self.properties - } - - pub fn properties_mut(&mut self) -> &mut Properties - { - &mut self.properties - } - - #[must_use] - pub fn dimensions(&self) -> &Dimens<u32> - { - &self.dimensions - } - - #[must_use] - pub fn pixel_data_format(&self) -> PixelDataFormat - { - self.pixel_data_format - } - - #[must_use] - pub fn image(&self) -> &DynamicImage - { - &self.image - } -} - -impl Drop for Texture -{ - fn drop(&mut self) + pub fn new(asset_handle: AssetHandle<Image>) -> Self { - NEXT_ID.fetch_sub(1, Ordering::Relaxed); - } -} - -/// Texture builder. -#[derive(Debug, Default, Clone)] -pub struct Builder -{ - properties: Properties, -} - -impl Builder -{ - pub fn properties(mut self, properties: Properties) -> Self - { - self.properties = properties; - self - } - - /// Opens a image as a texture. - /// - /// # Errors - /// Will return `Err` if: - /// - Opening the image fails - /// - Decoding the image fails - /// - The image data is in a unsupported format - pub fn open(&self, path: &(impl AsRef<Path> + ?Sized)) -> Result<Texture, Error> - { - let image = ImageReader::open(path) - .map_err(Error::OpenImageFailed)? - .decode() - .map_err(Error::DecodeImageFailed)?; - - let pixel_data_format = match &image { - DynamicImage::ImageRgb8(_) => PixelDataFormat::Rgb8, - DynamicImage::ImageRgba8(_) => PixelDataFormat::Rgba8, - _ => { - return Err(Error::UnsupportedImageDataFormat); - } - }; - - let dimensions = Dimens { - width: image.width(), - height: image.height(), - }; - - let id = NEXT_ID.fetch_add(1, Ordering::Relaxed); - - Ok(Texture { - id: Id::new(id), - image, - pixel_data_format, - dimensions, - properties: self.properties.clone(), - }) + Self { + asset_handle, + properties: Properties::default(), + } } - #[must_use] - pub fn build_with_single_color( - &self, - dimensions: &Dimens<u32>, - color: &Color<u8>, - ) -> Texture + pub fn with_properties( + asset_handle: AssetHandle<Image>, + properties: Properties, + ) -> Self { - let image = RgbImage::from_pixel( - dimensions.width, - dimensions.height, - Rgb([color.red, color.green, color.blue]), - ); - - let id = NEXT_ID.fetch_add(1, Ordering::Relaxed); - - Texture { - id: Id::new(id), - image: image.into(), - pixel_data_format: PixelDataFormat::Rgb8, - dimensions: *dimensions, - properties: self.properties.clone(), - } + Self { asset_handle, properties } } } -/// Texture error. -#[derive(Debug, thiserror::Error)] -pub enum Error -{ - #[error("Failed to open texture image")] - OpenImageFailed(#[source] std::io::Error), - - #[error("Failed to decode texture image")] - DecodeImageFailed(#[source] ImageError), - - #[error("Unsupported image data format")] - UnsupportedImageDataFormat, -} - builder! { /// Texture properties #[builder(name = PropertiesBuilder, derives=(Debug, Clone))] @@ -229,26 +76,3 @@ impl Default for PropertiesBuilder Properties::default().into() } } - -/// Texture ID. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Id -{ - id: u32, -} - -impl Id -{ - fn new(id: u32) -> Self - { - Self { id } - } -} - -impl Display for Id -{ - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result - { - self.id.fmt(formatter) - } -} diff --git a/engine/src/util.rs b/engine/src/util.rs index 0f6c78c..c974c29 100644 --- a/engine/src/util.rs +++ b/engine/src/util.rs @@ -82,6 +82,7 @@ macro_rules! builder { impl From<$name> for $builder_name { + #[allow(unused_variables)] fn from(built: $name) -> Self { Self { diff --git a/engine/src/work_queue.rs b/engine/src/work_queue.rs new file mode 100644 index 0000000..7226c7d --- /dev/null +++ b/engine/src/work_queue.rs @@ -0,0 +1,44 @@ +use std::marker::PhantomData; +use std::sync::mpsc::{channel as mpsc_channel, Sender as MpscSender}; +use std::thread::JoinHandle as ThreadHandle; + +pub struct Work<UserData: Send + Sync + 'static> +{ + pub func: fn(UserData), + pub user_data: UserData, +} + +#[derive(Debug)] +pub struct WorkQueue<UserData: Send + Sync + 'static> +{ + work_sender: MpscSender<Work<UserData>>, + _thread: ThreadHandle<()>, + _pd: PhantomData<UserData>, +} + +impl<UserData: Send + Sync + 'static> WorkQueue<UserData> +{ + pub fn new() -> Self + { + let (work_sender, work_receiver) = mpsc_channel::<Work<UserData>>(); + + Self { + work_sender, + _thread: std::thread::spawn(move || { + let work_receiver = work_receiver; + + while let Ok(work) = work_receiver.recv() { + (work.func)(work.user_data); + } + }), + _pd: PhantomData, + } + } + + pub fn add_work(&self, work: Work<UserData>) + { + if self.work_sender.send(work).is_err() { + tracing::error!("Cannot add work to work queue. Work queue thread is dead"); + } + } +} |