diff options
Diffstat (limited to 'engine')
| -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"); +        } +    } +}  | 
