diff options
Diffstat (limited to 'engine/src')
72 files changed, 17596 insertions, 5224 deletions
diff --git a/engine/src/asset.rs b/engine/src/asset.rs new file mode 100644 index 0000000..dd8aa37 --- /dev/null +++ b/engine/src/asset.rs @@ -0,0 +1,921 @@ +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::hint::cold_path; +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::actions::Actions; + +use crate::ecs::pair::ChildOf; +use crate::ecs::phase::{Phase, PRE_UPDATE as PRE_UPDATE_PHASE}; +use crate::ecs::sole::Single; +use crate::ecs::{declare_entity, pair, Sole}; +use crate::work_queue::{Work, WorkQueue}; + +declare_entity! { +pub HANDLE_ASSETS_PHASE: (Phase, pair!(ChildOf, { *PRE_UPDATE_PHASE })); +} + +/// 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>, + events: Events, +} + +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("asset_importing_work_queue"), + import_work_msg_receiver, + import_work_msg_sender, + events: Events::default(), + } + } + + 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 + Send + Sync + '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_label, asset_type = type_name::<Asset>()) + )] + pub fn get<Asset: 'static + Send + Sync>( + &self, + handle: &Handle<Asset>, + ) -> Option<&Asset> + { + let asset_lookup = self.asset_lookup.borrow(); + + let LookupEntry::Occupied(asset_index, asset_label) = + asset_lookup.get(&handle.id.label_hash)? + else { + return None; + }; + + tracing::Span::current() + .record("asset_label", tracing::field::display(&asset_label)); + + 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_all, fields(asset_type=type_name::<Asset>()))] + pub fn get_handle_to_loaded<'label, Asset: 'static + Send + Sync>( + &self, + label: impl Into<Label<'label>>, + ) -> Option<Handle<Asset>> + { + let label = label.into(); + + let label_hash = LabelHash::new(&label); + + let asset_lookup = self.asset_lookup.borrow(); + + let LookupEntry::Occupied(asset_index, _) = asset_lookup.get(&label_hash)? else { + return None; + }; + + 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"); + return None; + }; + + Some(Handle::new(label_hash)) + } + + pub fn is_loaded_and_has_type<Asset: 'static + Send + Sync>( + &self, + handle: &Handle<Asset>, + ) -> bool + { + let asset_lookup = self.asset_lookup.borrow(); + + let Some(LookupEntry::Occupied(asset_index, _)) = + asset_lookup.get(&handle.id.label_hash) + else { + return false; + }; + + let stored_asset = self.assets.get(*asset_index).expect("Not possible"); + + stored_asset.strong.downcast_ref::<Asset>().is_some() + } + + pub fn get_label<Asset: 'static + Send + Sync>( + &self, + handle: &Handle<Asset>, + ) -> Option<LabelOwned> + { + let lookup_entry = self + .asset_lookup + .borrow() + .get(&handle.id.label_hash)? + .clone(); + + let LookupEntry::Occupied(_, label) = lookup_entry else { + return None; + }; + + Some(label) + } + + pub fn get_label_by_id(&self, id: Id) -> Option<LabelOwned> + { + let lookup_entry = self.asset_lookup.borrow().get(&id.label_hash)?.clone(); + + let LookupEntry::Occupied(_, label) = lookup_entry else { + return None; + }; + + Some(label) + } + + #[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, + ) + } + + pub fn store_with_name_with<'name, Asset: 'static + Send + Sync>( + &mut self, + name: impl Into<Cow<'name, str>>, + func: impl FnOnce(&mut Self) -> Asset, + ) -> Handle<Asset> + { + let asset = func(self); + + 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, label.to_owned())); + + self.events + .curr_tick_events + .push(Event::Stored(Id { label_hash }, label.to_owned())); + + Handle::new(label_hash) + } + + pub fn events(&self) -> &Events + { + &self.events + } + + 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 import asset {}: {:#}", + asset_path.display(), + crate::Error::new(err) + ); + } + }, + 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 from_id(id: Id) -> Self + { + Self { id, _pd: PhantomData } + } + + 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, Default)] +pub struct Events +{ + curr_tick_events: Vec<Event>, + last_tick_events: Vec<Event>, +} + +impl Events +{ + pub fn last_tick_events(&self) -> impl Iterator<Item = &Event> + { + self.last_tick_events.iter() + } +} + +/// Asset event. +#[derive(Debug)] +pub enum Event +{ + /// Asset stored. + Stored(Id, LabelOwned), +} + +#[derive(Debug, thiserror::Error)] +enum ImporterError +{ + #[error("Settings has a incorrect type")] + IncorrectAssetSettingsType(PathBuf), + + #[error(transparent)] + Other(Box<dyn std::error::Error + Send + Sync>), +} + +#[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 + Send + Sync + '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 crate::ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: crate::ecs::extension::Collector<'_>) + { + let _ = collector.add_sole(self.assets); + + collector.spawn_declared_entity(&HANDLE_ASSETS_PHASE); + + collector.add_system(*HANDLE_ASSETS_PHASE, add_received_assets); + collector.add_system(*HANDLE_ASSETS_PHASE, check_import_wq_thread_not_panicked); + } +} + +fn add_received_assets(mut assets: Single<Assets>) +{ + let Ok(assets) = assets.get_mut() else { + unreachable!(); + }; + + let Events { curr_tick_events, last_tick_events } = &mut assets.events; + + std::mem::swap(last_tick_events, curr_tick_events); + + curr_tick_events.clear(); + + 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(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, + ); + } + } + } +} + +fn check_import_wq_thread_not_panicked(assets: Single<Assets>, mut actions: Actions<'_>) +{ + let Ok(assets) = assets.get() else { + unreachable!(); + }; + + if assets.import_work_queue.get_thread_panic().is_some() { + cold_path(); + + actions.stop(); + } +} + +#[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)] +enum LookupEntry +{ + Occupied(usize, LabelOwned), + 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/camera.rs b/engine/src/camera.rs index 66150af..51cd54c 100644 --- a/engine/src/camera.rs +++ b/engine/src/camera.rs @@ -1,11 +1,12 @@ -use ecs::Component; - +use crate::ecs::Component; +use crate::matrix::Matrix; use crate::projection::{Perspective, Projection}; +use crate::reflection::Reflection; use crate::vector::Vec3; pub mod fly; -#[derive(Debug, Component)] +#[derive(Debug, Clone, Component, Reflection)] pub struct Camera { pub target: Vec3<f32>, @@ -13,12 +14,24 @@ pub struct Camera pub projection: Projection, } +impl Camera +{ + pub fn to_view_matrix(&self, camera_world_pos: Vec3<f32>) -> Matrix<f32, 4, 4> + { + let mut view = Matrix::new(); + + view.look_at(camera_world_pos, self.target, self.global_up); + + view + } +} + impl Default for Camera { fn default() -> Self { Self { - target: Vec3::default(), + target: Vec3 { x: 0.0, y: 0.0, z: 0.0 }, global_up: Vec3::UP, projection: Projection::Perspective(Perspective::default()), } @@ -26,5 +39,20 @@ impl Default for Camera } /// Marker component for cameras that are active. -#[derive(Debug, Default, Clone, Copy, Component)] +#[derive(Debug, Default, Clone, Copy, Component, Reflection)] pub struct Active; + +/// Cameras that can be controlled have this component. +#[derive(Debug, Clone, Copy, Component, Reflection)] +pub struct Controllable +{ + pub control_enabled: bool, +} + +impl Default for Controllable +{ + fn default() -> Self + { + Self { control_enabled: true } + } +} diff --git a/engine/src/camera/fly.rs b/engine/src/camera/fly.rs index b6ba7aa..5f5ab4b 100644 --- a/engine/src/camera/fly.rs +++ b/engine/src/camera/fly.rs @@ -1,25 +1,28 @@ -use ecs::component::local::Local; -use ecs::sole::Single; -use ecs::system::{Into, System}; -use ecs::{Component, Query}; -use glfw::window::{Key, KeyState}; - -use crate::camera::{Active as ActiveCamera, Camera}; -use crate::delta_time::DeltaTime; -use crate::event::Update as UpdateEvent; -use crate::input::{Cursor, CursorFlags, Keys}; -use crate::transform::Position; -use crate::util::builder; -use crate::vector::{Vec2, Vec3}; +use ecs::time::Time; + +use crate::builder; +use crate::camera::{Active as ActiveCamera, Camera, Controllable as ControllableCamera}; +use crate::ecs::actions::Actions; +use crate::ecs::component::local::Local; +use crate::ecs::phase::UPDATE as UPDATE_PHASE; +use crate::ecs::query::term::With; +use crate::ecs::sole::Single; +use crate::ecs::system::initializable::Initializable; +use crate::ecs::system::Into; +use crate::ecs::{Component, Query}; +use crate::input::keyboard::{Key, Keyboard}; +use crate::input::mouse::Mouse; +use crate::reflection::Reflection; +use crate::transform::Transform; +use crate::vector::{Angles, Vec2, Vec3}; builder! { /// A fly camera. #[builder(name = Builder, derives = (Debug))] -#[derive(Debug, Component)] +#[derive(Debug, Component, Reflection)] #[non_exhaustive] pub struct Fly { - pub current_pitch: f64, - pub current_yaw: f64, + pub angles: Angles<f32>, pub speed: f32, } } @@ -46,8 +49,7 @@ impl Default for Builder fn default() -> Self { Self { - current_yaw: -90.0, - current_pitch: 0.0, + angles: Vec3::BACK.into_deg_angles(), speed: 3.0, } } @@ -56,16 +58,11 @@ impl Default for Builder /// Fly camera extension. pub struct Extension(pub Options); -impl ecs::extension::Extension for Extension +impl crate::ecs::extension::Extension for Extension { - fn collect(self, mut collector: ecs::extension::Collector<'_>) + fn collect(self, mut collector: crate::ecs::extension::Collector<'_>) { - collector.add_system( - UpdateEvent, - update - .into_system() - .initialize((CursorState::default(), self.0)), - ); + collector.add_system(*UPDATE_PHASE, update.into_system().initialize((self.0,))); } } @@ -76,82 +73,81 @@ pub struct Options } fn update( - camera_query: Query<(Camera, Position, Fly, ActiveCamera)>, - keys: Single<Keys>, - cursor: Single<Cursor>, - cursor_flags: Single<CursorFlags>, - delta_time: Single<DeltaTime>, - mut cursor_state: Local<CursorState>, + camera_query: Query<( + &mut Camera, + &mut Transform, + &mut Fly, + Option<&ControllableCamera>, + With<ActiveCamera>, + )>, + keyboard: Single<Keyboard>, + mouse: Single<Mouse>, + time: Single<Time>, options: Local<Options>, -) + mut actions: Actions, +) -> Result<(), crate::Error> { - for (mut camera, mut camera_pos, mut fly_camera, _) in &camera_query { - if cursor.has_moved && cursor_flags.is_first_move.flag { - #[cfg(feature = "debug")] - tracing::debug!("First cursor move"); + let keyboard = keyboard.get()?; + let mouse = mouse.get()?; - cursor_state.last_pos = cursor.position; + for ( + camera_ent_id, + (mut camera, mut camera_transform, mut fly_camera, controllable_camera), + ) in camera_query.iter_with_euids() + { + if let Some(controllable_camera) = controllable_camera { + if !controllable_camera.control_enabled { + continue; + } + } else { + actions.add_components(camera_ent_id, (ControllableCamera::default(),)); } - let delta_time = delta_time.duration; - - let mut x_offset = cursor.position.x - cursor_state.last_pos.x; - let mut y_offset = cursor_state.last_pos.y - cursor.position.y; - - cursor_state.last_pos = cursor.position; + if mouse.curr_tick_position_delta != (Vec2 { x: 0.0, y: 0.0 }) { + let x_offset = + mouse.curr_tick_position_delta.x * f64::from(options.mouse_sensitivity); - x_offset *= f64::from(options.mouse_sensitivity); - y_offset *= f64::from(options.mouse_sensitivity); + let y_offset = (-mouse.curr_tick_position_delta.y) + * f64::from(options.mouse_sensitivity); - fly_camera.current_yaw += x_offset; - fly_camera.current_pitch += y_offset; + fly_camera.angles.yaw += x_offset as f32; + fly_camera.angles.pitch += y_offset as f32; - fly_camera.current_pitch = fly_camera.current_pitch.clamp(-89.0, 89.0); - - // TODO: This casting to a f32 from a f64 is horrible. fix it - #[allow(clippy::cast_possible_truncation)] - let direction = Vec3 { - x: (fly_camera.current_yaw.to_radians().cos() - * fly_camera.current_pitch.to_radians().cos()) as f32, - y: fly_camera.current_pitch.to_radians().sin() as f32, - z: (fly_camera.current_yaw.to_radians().sin() - * fly_camera.current_pitch.to_radians().cos()) as f32, + fly_camera.angles.pitch = fly_camera.angles.pitch.clamp(-89.0, 89.0); } - .normalize(); + + let direction = Vec3::direction_from_deg_angles(fly_camera.angles); let cam_right = direction.cross(&Vec3::UP).normalize(); camera.global_up = cam_right.cross(&direction).normalize(); - if matches!(keys.get_key_state(Key::W), KeyState::Pressed) { - camera_pos.position += - direction * fly_camera.speed * delta_time.as_secs_f32(); + if keyboard.pressed(Key::W) { + camera_transform.position += + direction * fly_camera.speed * time.delta_time.as_secs_f32(); } - if matches!(keys.get_key_state(Key::S), KeyState::Pressed) { - camera_pos.position -= - direction * fly_camera.speed * delta_time.as_secs_f32(); + if keyboard.pressed(Key::S) { + camera_transform.position -= + direction * fly_camera.speed * time.delta_time.as_secs_f32(); } - if matches!(keys.get_key_state(Key::A), KeyState::Pressed) { + if keyboard.pressed(Key::A) { let cam_left = -direction.cross(&Vec3::UP).normalize(); - camera_pos.position += cam_left * fly_camera.speed * delta_time.as_secs_f32(); + camera_transform.position += + cam_left * fly_camera.speed * time.delta_time.as_secs_f32(); } - if matches!(keys.get_key_state(Key::D), KeyState::Pressed) { + if keyboard.pressed(Key::D) { let cam_right = direction.cross(&Vec3::UP).normalize(); - camera_pos.position += - cam_right * fly_camera.speed * delta_time.as_secs_f32(); + camera_transform.position += + cam_right * fly_camera.speed * time.delta_time.as_secs_f32(); } - camera.target = camera_pos.position + direction; + camera.target = camera_transform.position + direction; } -} -#[derive(Debug, Default, Component)] -struct CursorState -{ - last_pos: Vec2<f64>, + Ok(()) } diff --git a/engine/src/collision.rs b/engine/src/collision.rs new file mode 100644 index 0000000..015ab58 --- /dev/null +++ b/engine/src/collision.rs @@ -0,0 +1,142 @@ +use crate::ecs::Component; +use crate::mesh::Mesh; +use crate::reflection::Reflection; +use crate::vector::Vec3; + +pub trait Collider<Other> +{ + fn intersects(&self, other: &Other) -> bool; +} + +#[derive(Debug, Default, Clone, Component, Reflection)] +#[non_exhaustive] +pub struct BoxCollider +{ + pub min: Vec3<f32>, + pub max: Vec3<f32>, +} + +impl BoxCollider +{ + pub fn for_mesh(mesh: &Mesh) -> Self + { + let furthest_dir_points = mesh.find_furthest_vertex_positions(); + + Self { + min: Vec3 { + x: furthest_dir_points.left.x, + y: furthest_dir_points.down.y, + z: furthest_dir_points.back.z, + }, + max: Vec3 { + x: furthest_dir_points.right.x, + y: furthest_dir_points.up.y, + z: furthest_dir_points.front.z, + }, + } + } + + pub fn offset(self, offset: Vec3<f32>) -> Self + { + Self { + min: self.min + offset, + max: self.max + offset, + } + } +} + +impl Collider<BoxCollider> for BoxCollider +{ + fn intersects(&self, other: &BoxCollider) -> bool + { + self.min.x <= other.max.x + && self.max.x >= other.min.x + && self.min.y <= other.max.y + && self.max.y >= other.min.y + && self.min.z <= other.max.z + && self.max.z >= other.min.z + } +} + +impl Collider<SphereCollider> for BoxCollider +{ + fn intersects(&self, other: &SphereCollider) -> bool + { + other.intersects(self) + } +} + +impl Collider<Vec3<f32>> for BoxCollider +{ + fn intersects(&self, other: &Vec3<f32>) -> bool + { + other.x >= self.min.x + && other.y >= self.min.y + && other.z >= self.min.z + && other.x <= self.max.x + && other.y <= self.max.y + && other.z <= self.max.z + } +} + +#[derive(Debug, Default, Clone, Component, Reflection)] +pub struct SphereCollider +{ + pub center: Vec3<f32>, + pub radius: f32, +} + +impl SphereCollider +{ + pub fn offset(self, offset: Vec3<f32>) -> Self + { + Self { + center: self.center + offset, + radius: self.radius, + } + } +} + +impl Collider<SphereCollider> for SphereCollider +{ + fn intersects(&self, other: &SphereCollider) -> bool + { + (self.center - other.center).length() <= self.radius + other.radius + } +} + +impl Collider<BoxCollider> for SphereCollider +{ + fn intersects(&self, other: &BoxCollider) -> bool + { + let mut min_distance = 0.0; + + if self.center.x < other.min.x { + min_distance += (self.center.x - other.min.x).powf(2.0); + } else if self.center.x > other.max.x { + min_distance += (self.center.x - other.max.x).powf(2.0); + } + + if self.center.y < other.min.y { + min_distance += (self.center.y - other.min.y).powf(2.0); + } else if self.center.y > other.max.y { + min_distance += (self.center.y - other.max.y).powf(2.0); + } + + if self.center.z < other.min.z { + min_distance += (self.center.z - other.min.z).powf(2.0); + } else if self.center.z > other.max.z { + min_distance += (self.center.z - other.max.z).powf(2.0); + } + + min_distance <= self.radius.powf(2.0) + } +} + +impl Collider<Vec3<f32>> for SphereCollider +{ + fn intersects(&self, other: &Vec3<f32>) -> bool + { + (self.center - *other).length() <= self.radius + } +} diff --git a/engine/src/data_types/color.rs b/engine/src/data_types/color.rs index cef3b92..d2fd217 100644 --- a/engine/src/data_types/color.rs +++ b/engine/src/data_types/color.rs @@ -1,59 +1,145 @@ -use std::ops::{Add, Div, Mul, Neg, Sub}; +use std::ops::{Add, Div, Mul, Sub}; -#[derive(Debug, Clone, Default)] -#[repr(C)] -pub struct Color<Value> +use crate::reflection::Reflection; + +pub trait Pixel: sealed::Sealed { - pub red: Value, - pub green: Value, - pub blue: Value, + type Component: PixelComponent; } -impl Color<f32> +pub trait PixelComponent: Clone + Copy + sealed::Sealed { - pub const WHITE_F32: Self = Self { red: 1.0, green: 1.0, blue: 1.0 }; + const COLOR_MAX: Self; +} + +macro_rules! gen_color { + ($ident: ident, components=($($component: ident),+)) => { + #[derive(Debug, Clone, Default, Reflection)] + #[reflection(impl_with_generics(<u8>, <u16>, <u32>, <f32>))] + pub struct $ident<Component> + { + $(pub $component: Component),+ + } + + impl<Component: PixelComponent> $ident<Component> + { + pub fn white() -> Self + { + Self { + $($component: Component::COLOR_MAX),* + } + } + } + + impl<Component: PixelComponent> Pixel for $ident<Component> + { + type Component = Component; + } + + impl<Component> sealed::Sealed for $ident<Component> {} + }; } -impl Color<u8> +gen_color!(Rgb, components = (r, g, b)); +gen_color!(Rgba, components = (r, g, b, a)); + +#[derive(Debug, Clone, Reflection)] +#[reflection(impl_with_generics(<u8>, <u16>, <u32>, <f32>))] +pub enum Color<Component> { - pub const WHITE_U8: Self = Self { red: 255, green: 255, blue: 255 }; + Rgb(Rgb<Component>), + Rgba(Rgba<Component>), } -impl<Value: Clone> From<Value> for Color<Value> +impl<Component: PixelComponent> Color<Component> { - fn from(value: Value) -> Self + pub fn to_rgb_lossy(&self) -> Rgb<Component> + { + match self { + Self::Rgb(rgb) => rgb.clone(), + Self::Rgba(rgba) => Rgb { r: rgba.r, g: rgba.g, b: rgba.b }, + } + } + + pub fn to_rgba_lossy(&self) -> Rgba<Component> { - Self { - red: value.clone(), - green: value.clone(), - blue: value, + match self { + Self::Rgb(rgb) => Rgba { + r: rgb.r, + g: rgb.g, + b: rgb.b, + a: Component::COLOR_MAX, + }, + Self::Rgba(rgba) => rgba.clone(), } } } -macro_rules! impl_math_op { - ($math_op_trait: ident, $function: ident) => { - impl<Value> $math_op_trait for Color<Value> +impl<Component> From<Rgb<Component>> for Color<Component> +{ + fn from(rgb: Rgb<Component>) -> Self + { + Self::Rgb(rgb) + } +} + +impl<Component> From<Rgba<Component>> for Color<Component> +{ + fn from(rgba: Rgba<Component>) -> Self + { + Self::Rgba(rgba) + } +} + +impl<Component: Default> Default for Color<Component> +{ + fn default() -> Self + { + Self::Rgb(Rgb::default()) + } +} + +macro_rules! gen_scalar_math_op_impl { + ( + $ident: ident, + $math_op_trait: ident, + $function: ident, + components=($($component: ident),+) + ) => { + impl<Value> $math_op_trait<Value> for $ident<Value> where - Value: $math_op_trait<Output = Value>, + Value: $math_op_trait<Output = Value> + Clone, { type Output = Self; - fn $function(self, rhs: Self) -> Self::Output + fn $function(self, rhs: Value) -> Self::Output { Self { - red: self.red.$function(rhs.red), - green: self.green.$function(rhs.green), - blue: self.blue.$function(rhs.blue), + $($component: self.$component.$function(rhs.clone())),+ } } } }; } -macro_rules! impl_scalar_math_op { - ($math_op_trait: ident, $function: ident) => { - impl<Value> $math_op_trait<Value> for Color<Value> +gen_scalar_math_op_impl!(Rgb, Add, add, components = (r, g, b)); +gen_scalar_math_op_impl!(Rgb, Sub, sub, components = (r, g, b)); +gen_scalar_math_op_impl!(Rgb, Mul, mul, components = (r, g, b)); +gen_scalar_math_op_impl!(Rgb, Div, div, components = (r, g, b)); + +gen_scalar_math_op_impl!(Rgba, Add, add, components = (r, g, b, a)); +gen_scalar_math_op_impl!(Rgba, Sub, sub, components = (r, g, b, a)); +gen_scalar_math_op_impl!(Rgba, Mul, mul, components = (r, g, b, a)); +gen_scalar_math_op_impl!(Rgba, Div, div, components = (r, g, b, a)); + +macro_rules! gen_enum_scalar_math_op_impl { + ( + $ident: ident, + $math_op_trait: ident, + $function: ident, + variants=($($variant: ident),+) + ) => { + impl<Value> $math_op_trait<Value> for $ident<Value> where Value: $math_op_trait<Output = Value> + Clone, { @@ -61,38 +147,44 @@ macro_rules! impl_scalar_math_op { fn $function(self, rhs: Value) -> Self::Output { - Self { - red: self.red.$function(rhs.clone()), - green: self.green.$function(rhs.clone()), - blue: self.blue.$function(rhs), + match self { + $( + Self::$variant(inner) => + Self::$variant(inner.$function(rhs.clone())) + ),+ } } } }; } -impl_math_op!(Add, add); -impl_math_op!(Sub, sub); -impl_math_op!(Mul, mul); -impl_math_op!(Div, div); +gen_enum_scalar_math_op_impl!(Color, Add, add, variants = (Rgb, Rgba)); +gen_enum_scalar_math_op_impl!(Color, Sub, sub, variants = (Rgb, Rgba)); +gen_enum_scalar_math_op_impl!(Color, Mul, mul, variants = (Rgb, Rgba)); +gen_enum_scalar_math_op_impl!(Color, Div, div, variants = (Rgb, Rgba)); -impl_scalar_math_op!(Add, add); -impl_scalar_math_op!(Sub, sub); -impl_scalar_math_op!(Mul, mul); -impl_scalar_math_op!(Div, div); +impl PixelComponent for u8 +{ + const COLOR_MAX: Self = u8::MAX; +} -impl<Value> Neg for Color<Value> -where - Value: Neg<Output = Value>, +impl sealed::Sealed for u8 {} + +impl PixelComponent for u16 { - type Output = Self; + const COLOR_MAX: Self = u16::MAX; +} - fn neg(self) -> Self::Output - { - Self { - red: -self.red, - green: -self.green, - blue: -self.blue, - } - } +impl sealed::Sealed for u16 {} + +impl PixelComponent for f32 +{ + const COLOR_MAX: Self = 1.0; +} + +impl sealed::Sealed for f32 {} + +mod sealed +{ + pub trait Sealed {} } diff --git a/engine/src/data_types/dimens.rs b/engine/src/data_types/dimens.rs index b395627..0621b75 100644 --- a/engine/src/data_types/dimens.rs +++ b/engine/src/data_types/dimens.rs @@ -1,7 +1,80 @@ -/// Dimensions. -#[derive(Debug, Clone, Copy)] +use std::num::NonZeroU32; +use std::ops::Div; + +use crate::reflection::Reflection; + +/// 2D dimensions. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Reflection)] +#[reflection(impl_with_generics(<f32>, <f64>, <u32>, <u16>))] pub struct Dimens<Value> { pub width: Value, pub height: Value, } + +impl<Value: Clone> From<Value> for Dimens<Value> +{ + fn from(value: Value) -> Self + { + Self { width: value.clone(), height: value } + } +} + +impl<Value> From<(Value, Value)> for Dimens<Value> +{ + fn from(value: (Value, Value)) -> Self + { + Self { width: value.0, height: value.1 } + } +} + +impl<Value: Div<Output = Value> + Clone> Div<Value> for Dimens<Value> +{ + type Output = Self; + + fn div(self, rhs: Value) -> Self::Output + { + Self { + width: self.width / rhs.clone(), + height: self.height / rhs, + } + } +} + +impl Dimens<u32> +{ + #[must_use] + pub fn try_into_nonzero(self) -> Option<Dimens<NonZeroU32>> + { + Some(Dimens { + width: NonZeroU32::new(self.width)?, + height: NonZeroU32::new(self.height)?, + }) + } +} + +/// 3D dimensions. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Reflection)] +#[reflection(impl_with_generics(<f32>, <f64>, <u32>, <u16>))] +pub struct Dimens3<Value> +{ + pub width: Value, + pub height: Value, + pub depth: Value, +} + +impl<Value> From<[Value; 3]> for Dimens3<Value> +{ + fn from([width, height, depth]: [Value; 3]) -> Self + { + Self { width, height, depth } + } +} + +impl<Value> From<Dimens3<Value>> for [Value; 3] +{ + fn from(Dimens3 { width, height, depth }: Dimens3<Value>) -> Self + { + [width, height, depth] + } +} diff --git a/engine/src/data_types/matrix.rs b/engine/src/data_types/matrix.rs index 3a29ae2..9ab5bd4 100644 --- a/engine/src/data_types/matrix.rs +++ b/engine/src/data_types/matrix.rs @@ -1,10 +1,12 @@ -use crate::vector::Vec3; +use std::ops::{Add, Index, IndexMut, Mul}; + +use crate::data_types::dimens::Dimens3; +use crate::vector::{Vec3, Vec4}; #[derive(Debug, Clone)] pub struct Matrix<Value, const ROWS: usize, const COLUMNS: usize> { - /// Items must be layed out this way for it to work with OpenGL shaders. - items: [[Value; ROWS]; COLUMNS], + items: [[Value; COLUMNS]; ROWS], } impl<Value, const ROWS: usize, const COLUMNS: usize> Matrix<Value, ROWS, COLUMNS> @@ -20,17 +22,61 @@ impl<Value, const ROWS: usize, const COLUMNS: usize> Matrix<Value, ROWS, COLUMNS } } - /// Sets the value at the specified cell. - pub fn set_cell(&mut self, row: usize, column: usize, value: Value) + pub fn from_columns<Column>(columns: [Column; COLUMNS]) -> Self + where + Column: Into<[Value; ROWS]>, + Value: Default, { - self.items[column][row] = value; + let mut items = + std::array::from_fn(|_| std::array::from_fn(|_| Value::default())); + + for (column_index, column) in columns.into_iter().enumerate() { + for (row_index, item) in column.into().into_iter().enumerate() { + items[row_index][column_index] = item; + } + } + + Self { items } } - /// Returns the internal 2D array as a pointer. - #[must_use] - pub fn as_ptr(&self) -> *const Value + pub fn from_rows<Row>(rows: [Row; ROWS]) -> Self + where + Row: Into<[Value; COLUMNS]>, { - self.items[0].as_ptr() + let items = rows.map(|row| row.into()); + + Self { items } + } + + pub fn get_column_copied(&self, column_index: usize) -> [Value; ROWS] + where + Value: Default + Copy, + { + std::array::from_fn::<Value, ROWS, _>(|row_index| { + self[CellPos { row: row_index, col: column_index }] + }) + } + + pub fn items(&self) -> &[Value] + { + &self.items.as_flattened() + } + + pub fn to_column_major(&self) -> [[Value; ROWS]; COLUMNS] + where + Value: Default + Clone, + { + let mut transposed = std::array::from_fn::<_, COLUMNS, _>(|_| { + std::array::from_fn::<_, ROWS, _>(|_| Value::default()) + }); + + for (row_index, row) in self.items.iter().enumerate() { + for (column_index, item) in row.iter().enumerate() { + transposed[column_index][row_index] = item.clone(); + } + } + + transposed } } @@ -51,19 +97,13 @@ impl<const ROWS_COLS: usize> Matrix<f32, ROWS_COLS, ROWS_COLS> #[must_use] pub fn new_identity() -> Self { - let mut index = 0; - - let items = [(); ROWS_COLS].map(|()| { - let mut columns = [0.0; ROWS_COLS]; - - columns[index] = 1.0; - - index += 1; + let mut identity = Self::new(); - columns - }); + for index in 0..ROWS_COLS { + identity[CellPos { row: index, col: index }] = 1.0; + } - Self { items } + identity } } @@ -71,21 +111,21 @@ impl Matrix<f32, 4, 4> { pub fn translate(&mut self, translation: &Vec3<f32>) { - self.set_cell(0, 3, translation.x); - self.set_cell(1, 3, translation.y); - self.set_cell(2, 3, translation.z); - self.set_cell(3, 3, 1.0); + self[CellPos { row: 0, col: 3 }] = translation.x; + self[CellPos { row: 1, col: 3 }] = translation.y; + self[CellPos { row: 2, col: 3 }] = translation.z; + self[CellPos { row: 3, col: 3 }] = 1.0; } - pub fn scale(&mut self, scaling: &Vec3<f32>) + pub fn scale(&mut self, scaling: &Dimens3<f32>) { - self.set_cell(0, 0, scaling.x); - self.set_cell(1, 1, scaling.y); - self.set_cell(2, 2, scaling.z); - self.set_cell(3, 3, 1.0); + self[CellPos { row: 0, col: 0 }] = scaling.width; + self[CellPos { row: 1, col: 1 }] = scaling.height; + self[CellPos { row: 2, col: 2 }] = scaling.depth; + self[CellPos { row: 3, col: 3 }] = 1.0; } - pub fn look_at(&mut self, eye: &Vec3<f32>, target: &Vec3<f32>, up: &Vec3<f32>) + pub fn look_at(&mut self, eye: Vec3<f32>, target: Vec3<f32>, up: Vec3<f32>) { let rev_target_direction = (eye - target).normalize(); @@ -93,30 +133,388 @@ impl Matrix<f32, 4, 4> let camera_up = rev_target_direction.cross(&camera_right); - self.set_cell(0, 0, camera_right.x); - self.set_cell(0, 1, camera_right.y); - self.set_cell(0, 2, camera_right.z); + self[CellPos { row: 0, col: 0 }] = camera_right.x; + self[CellPos { row: 0, col: 1 }] = camera_right.y; + self[CellPos { row: 0, col: 2 }] = camera_right.z; - self.set_cell(1, 0, camera_up.x); - self.set_cell(1, 1, camera_up.y); - self.set_cell(1, 2, camera_up.z); + self[CellPos { row: 1, col: 0 }] = camera_up.x; + self[CellPos { row: 1, col: 1 }] = camera_up.y; + self[CellPos { row: 1, col: 2 }] = camera_up.z; - self.set_cell(2, 0, rev_target_direction.x); - self.set_cell(2, 1, rev_target_direction.y); - self.set_cell(2, 2, rev_target_direction.z); + self[CellPos { row: 2, col: 0 }] = rev_target_direction.x; + self[CellPos { row: 2, col: 1 }] = rev_target_direction.y; + self[CellPos { row: 2, col: 2 }] = rev_target_direction.z; // The vector is negated since we want the world to be translated in the opposite // direction of where we want the camera to move. let camera_pos = -Vec3 { - x: camera_right.dot(eye), - y: camera_up.dot(eye), - z: rev_target_direction.dot(eye), + x: camera_right.dot(&eye), + y: camera_up.dot(&eye), + z: rev_target_direction.dot(&eye), }; - self.set_cell(0, 3, camera_pos.x); - self.set_cell(1, 3, camera_pos.y); - self.set_cell(2, 3, camera_pos.z); + self[CellPos { row: 0, col: 3 }] = camera_pos.x; + self[CellPos { row: 1, col: 3 }] = camera_pos.y; + self[CellPos { row: 2, col: 3 }] = camera_pos.z; - self.set_cell(3, 3, 1.0); + self[CellPos { row: 3, col: 3 }] = 1.0; } + + pub fn inverse(&self) -> Self + { + let coef_00 = self[CellPos { row: 2, col: 2 }] * self[CellPos { row: 3, col: 3 }] + - self[CellPos { row: 2, col: 3 }] * self[CellPos { row: 3, col: 2 }]; + let coef_02 = self[CellPos { row: 2, col: 1 }] * self[CellPos { row: 3, col: 3 }] + - self[CellPos { row: 2, col: 3 }] * self[CellPos { row: 3, col: 1 }]; + let coef_03 = self[CellPos { row: 2, col: 1 }] * self[CellPos { row: 3, col: 2 }] + - self[CellPos { row: 2, col: 2 }] * self[CellPos { row: 3, col: 1 }]; + + let coef_04 = self[CellPos { row: 1, col: 2 }] * self[CellPos { row: 3, col: 3 }] + - self[CellPos { row: 1, col: 3 }] * self[CellPos { row: 3, col: 2 }]; + let coef_06 = self[CellPos { row: 1, col: 1 }] * self[CellPos { row: 3, col: 3 }] + - self[CellPos { row: 1, col: 3 }] * self[CellPos { row: 3, col: 1 }]; + let coef_07 = self[CellPos { row: 1, col: 1 }] * self[CellPos { row: 3, col: 2 }] + - self[CellPos { row: 1, col: 2 }] * self[CellPos { row: 3, col: 1 }]; + + let coef_08 = self[CellPos { row: 1, col: 2 }] * self[CellPos { row: 2, col: 3 }] + - self[CellPos { row: 1, col: 3 }] * self[CellPos { row: 2, col: 2 }]; + let coef_10 = self[CellPos { row: 1, col: 1 }] * self[CellPos { row: 2, col: 3 }] + - self[CellPos { row: 1, col: 3 }] * self[CellPos { row: 2, col: 1 }]; + let coef_11 = self[CellPos { row: 1, col: 1 }] * self[CellPos { row: 2, col: 2 }] + - self[CellPos { row: 1, col: 2 }] * self[CellPos { row: 2, col: 1 }]; + + let coef_12 = self[CellPos { row: 0, col: 2 }] * self[CellPos { row: 3, col: 3 }] + - self[CellPos { row: 0, col: 3 }] * self[CellPos { row: 3, col: 2 }]; + let coef_14 = self[CellPos { row: 0, col: 1 }] * self[CellPos { row: 3, col: 3 }] + - self[CellPos { row: 0, col: 3 }] * self[CellPos { row: 3, col: 1 }]; + let coef_15 = self[CellPos { row: 0, col: 1 }] * self[CellPos { row: 3, col: 2 }] + - self[CellPos { row: 0, col: 2 }] * self[CellPos { row: 3, col: 1 }]; + + let coef_16 = self[CellPos { row: 0, col: 2 }] * self[CellPos { row: 2, col: 3 }] + - self[CellPos { row: 0, col: 3 }] * self[CellPos { row: 2, col: 2 }]; + let coef_18 = self[CellPos { row: 0, col: 1 }] * self[CellPos { row: 2, col: 3 }] + - self[CellPos { row: 0, col: 3 }] * self[CellPos { row: 2, col: 1 }]; + let coef_19 = self[CellPos { row: 0, col: 1 }] * self[CellPos { row: 2, col: 2 }] + - self[CellPos { row: 0, col: 2 }] * self[CellPos { row: 2, col: 1 }]; + + let coef_20 = self[CellPos { row: 0, col: 2 }] * self[CellPos { row: 1, col: 3 }] + - self[CellPos { row: 0, col: 3 }] * self[CellPos { row: 1, col: 2 }]; + let coef_22 = self[CellPos { row: 0, col: 1 }] * self[CellPos { row: 1, col: 3 }] + - self[CellPos { row: 0, col: 3 }] * self[CellPos { row: 1, col: 1 }]; + let coef_23 = self[CellPos { row: 0, col: 1 }] * self[CellPos { row: 1, col: 2 }] + - self[CellPos { row: 0, col: 2 }] * self[CellPos { row: 1, col: 1 }]; + + let fac_0 = Vec4 { + x: coef_00, + y: coef_00, + z: coef_02, + w: coef_03, + }; + let fac_1 = Vec4 { + x: coef_04, + y: coef_04, + z: coef_06, + w: coef_07, + }; + let fac_2 = Vec4 { + x: coef_08, + y: coef_08, + z: coef_10, + w: coef_11, + }; + let fac_3 = Vec4 { + x: coef_12, + y: coef_12, + z: coef_14, + w: coef_15, + }; + let fac_4 = Vec4 { + x: coef_16, + y: coef_16, + z: coef_18, + w: coef_19, + }; + let fac_5 = Vec4 { + x: coef_20, + y: coef_20, + z: coef_22, + w: coef_23, + }; + + let vec_0 = Vec4 { + x: self[CellPos { row: 0, col: 1 }], + y: self[CellPos { row: 0, col: 0 }], + z: self[CellPos { row: 0, col: 0 }], + w: self[CellPos { row: 0, col: 0 }], + }; + let vec_1 = Vec4 { + x: self[CellPos { row: 1, col: 1 }], + y: self[CellPos { row: 1, col: 0 }], + z: self[CellPos { row: 1, col: 0 }], + w: self[CellPos { row: 1, col: 0 }], + }; + let vec_2 = Vec4 { + x: self[CellPos { row: 2, col: 1 }], + y: self[CellPos { row: 2, col: 0 }], + z: self[CellPos { row: 2, col: 0 }], + w: self[CellPos { row: 2, col: 0 }], + }; + let vec_3 = Vec4 { + x: self[CellPos { row: 3, col: 1 }], + y: self[CellPos { row: 3, col: 0 }], + z: self[CellPos { row: 3, col: 0 }], + w: self[CellPos { row: 3, col: 0 }], + }; + + let inv_0 = vec_1 * fac_0 - vec_2 * fac_1 + vec_3 * fac_2; + let inv_1 = vec_0 * fac_0 - vec_2 * fac_3 + vec_3 * fac_4; + let inv_2 = vec_0 * fac_1 - vec_1 * fac_3 + vec_3 * fac_5; + let inv_3 = vec_0 * fac_2 - vec_1 * fac_4 + vec_2 * fac_5; + + let sign_a = Vec4 { x: 1.0, y: -1.0, z: 1.0, w: -1.0 }; + let sign_b = Vec4 { x: -1.0, y: 1.0, z: -1.0, w: 1.0 }; + + let inverse = Self::from_columns([ + inv_0 * sign_a, + inv_1 * sign_b, + inv_2 * sign_a, + inv_3 * sign_b, + ]); + + let row_0 = Vec4 { + x: inverse[CellPos { row: 0, col: 0 }], + y: inverse[CellPos { row: 0, col: 1 }], + z: inverse[CellPos { row: 0, col: 2 }], + w: inverse[CellPos { row: 0, col: 3 }], + }; + + let dot_0 = Vec4::<f32>::from(self.get_column_copied(0)) * row_0; + + let dot_1 = (dot_0.x + dot_0.y) + (dot_0.z + dot_0.w); + + let one_over_determinant = 1.0 / dot_1; + + inverse * one_over_determinant + } +} + +impl Mul<f32> for Matrix<f32, 4, 4> +{ + type Output = Self; + + fn mul(self, scalar: f32) -> Self::Output + { + Self { + items: self.items.map(|row| (Vec4::from(row) * scalar).into()), + } + } +} + +impl<Value, const ROWS: usize, const COLUMNS: usize, const RHS_COLUMNS: usize> + Mul<Matrix<Value, ROWS, RHS_COLUMNS>> for Matrix<Value, ROWS, COLUMNS> +where + Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy, +{ + type Output = Matrix<Value, ROWS, RHS_COLUMNS>; + + fn mul(self, rhs: Matrix<Value, ROWS, RHS_COLUMNS>) -> Self::Output + { + &self * &rhs + } +} + +impl<Value, const ROWS: usize, const COLUMNS: usize, const RHS_COLUMNS: usize> + Mul<&Matrix<Value, ROWS, RHS_COLUMNS>> for Matrix<Value, ROWS, COLUMNS> +where + Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy, +{ + type Output = Matrix<Value, ROWS, RHS_COLUMNS>; + + fn mul(self, rhs: &Matrix<Value, ROWS, RHS_COLUMNS>) -> Self::Output + { + &self * rhs + } +} + +impl<Value, const ROWS: usize, const COLUMNS: usize, const RHS_COLUMNS: usize> + Mul<Matrix<Value, ROWS, RHS_COLUMNS>> for &Matrix<Value, ROWS, COLUMNS> +where + Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy, +{ + type Output = Matrix<Value, ROWS, RHS_COLUMNS>; + + fn mul(self, rhs: Matrix<Value, ROWS, RHS_COLUMNS>) -> Self::Output + { + self * &rhs + } +} + +impl<Value, const ROWS: usize, const COLUMNS: usize, const RHS_COLUMNS: usize> + Mul<&Matrix<Value, ROWS, RHS_COLUMNS>> for &Matrix<Value, ROWS, COLUMNS> +where + Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy, +{ + type Output = Matrix<Value, ROWS, RHS_COLUMNS>; + + fn mul(self, rhs: &Matrix<Value, ROWS, RHS_COLUMNS>) -> Self::Output + { + // https://en.wikipedia.org/wiki/Matrix_multiplication + + let mut out = Self::Output::new(); + + for (row_index, row) in self.items.iter().enumerate() { + for (column, item) in row.iter().enumerate() { + let rhs_items = &rhs.items[column]; + + for (rhs_item_index, rhs_item) in rhs_items.iter().enumerate() { + let prev = out[CellPos { row: row_index, col: rhs_item_index }]; + + out[CellPos { row: row_index, col: rhs_item_index }] = + prev + (*item * *rhs_item); + } + } + } + + out + } +} + +impl<Value, const COLUMNS: usize> Mul<Vec3<Value>> for Matrix<Value, 3, COLUMNS> +where + Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy, +{ + type Output = Vec3<Value>; + + fn mul(self, rhs: Vec3<Value>) -> Self::Output + { + &self * &rhs + } +} + +impl<Value, const COLUMNS: usize> Mul<&Vec3<Value>> for Matrix<Value, 3, COLUMNS> +where + Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy, +{ + type Output = Vec3<Value>; + + fn mul(self, rhs: &Vec3<Value>) -> Self::Output + { + &self * rhs + } +} + +impl<Value, const COLUMNS: usize> Mul<Vec3<Value>> for &Matrix<Value, 3, COLUMNS> +where + Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy, +{ + type Output = Vec3<Value>; + + fn mul(self, rhs: Vec3<Value>) -> Self::Output + { + self * &rhs + } +} + +impl<Value, const COLUMNS: usize> Mul<&Vec3<Value>> for &Matrix<Value, 3, COLUMNS> +where + Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy, +{ + type Output = Vec3<Value>; + + fn mul(self, rhs: &Vec3<Value>) -> Self::Output + { + let rhs_mat = Matrix::<Value, 3, 1> { items: [[rhs.x], [rhs.y], [rhs.z]] }; + + let Matrix { items: [[out_x], [out_y], [out_z]] } = self * &rhs_mat; + + Vec3 { x: out_x, y: out_y, z: out_z } + } +} + +impl<Value, const COLUMNS: usize> Mul<Vec4<Value>> for Matrix<Value, 4, COLUMNS> +where + Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy, +{ + type Output = Vec4<Value>; + + fn mul(self, rhs: Vec4<Value>) -> Self::Output + { + &self * &rhs + } +} + +impl<Value, const COLUMNS: usize> Mul<&Vec4<Value>> for Matrix<Value, 4, COLUMNS> +where + Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy, +{ + type Output = Vec4<Value>; + + fn mul(self, rhs: &Vec4<Value>) -> Self::Output + { + &self * rhs + } +} + +impl<Value, const COLUMNS: usize> Mul<Vec4<Value>> for &Matrix<Value, 4, COLUMNS> +where + Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy, +{ + type Output = Vec4<Value>; + + fn mul(self, rhs: Vec4<Value>) -> Self::Output + { + self * &rhs + } +} + +impl<Value, const COLUMNS: usize> Mul<&Vec4<Value>> for &Matrix<Value, 4, COLUMNS> +where + Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy, +{ + type Output = Vec4<Value>; + + fn mul(self, rhs: &Vec4<Value>) -> Self::Output + { + let rhs_mat = Matrix::<Value, 4, 1> { + items: [[rhs.x], [rhs.y], [rhs.z], [rhs.w]], + }; + + let Matrix { + items: [[out_x], [out_y], [out_z], [out_w]], + } = self * &rhs_mat; + + Vec4 { + x: out_x, + y: out_y, + z: out_z, + w: out_w, + } + } +} + +impl<Value, const ROWS: usize, const COLUMNS: usize> Index<CellPos> + for Matrix<Value, ROWS, COLUMNS> +{ + type Output = Value; + + fn index(&self, cell_pos: CellPos) -> &Self::Output + { + &self.items[cell_pos.row][cell_pos.col] + } +} + +impl<Value, const ROWS: usize, const COLUMNS: usize> IndexMut<CellPos> + for Matrix<Value, ROWS, COLUMNS> +{ + fn index_mut(&mut self, cell_pos: CellPos) -> &mut Self::Output + { + &mut self.items[cell_pos.row][cell_pos.col] + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CellPos +{ + pub row: usize, + pub col: usize, } diff --git a/engine/src/data_types/vector.rs b/engine/src/data_types/vector.rs index 17953f4..915599a 100644 --- a/engine/src/data_types/vector.rs +++ b/engine/src/data_types/vector.rs @@ -1,82 +1,125 @@ -use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign}; - -use crate::color::Color; +use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +use crate::reflection::Reflection; + +macro_rules! impl_math_op_traits { + (impl $op_trait: ident for $vector: ident, ($($vec_field: ident),+)) => { + paste::paste! { + impl<Value> $op_trait for $vector<Value> + where + Value: $op_trait<Output = Value>, + { + type Output = Self; + + fn [<$op_trait:lower>](self, rhs: Self) -> Self::Output + { + Self::Output {$( + $vec_field: self.$vec_field.[<$op_trait:lower>](rhs.$vec_field) + ),+} + } + } + + impl<Value> [<$op_trait Assign>] for $vector<Value> + where + Value: [<$op_trait Assign>], + { + fn [<$op_trait:lower _assign>](&mut self, rhs: Self) + { + $( + self.$vec_field.[<$op_trait:lower _assign>](rhs.$vec_field); + )+ + } + } + } + }; + + (impl $op_trait: ident <Value> for $vector: ident, ($($vec_field: ident),+)) => { + paste::paste! { + impl<Value> $op_trait<Value> for $vector<Value> + where + Value: $op_trait<Output = Value> + Clone, + { + type Output = Self; + + fn [<$op_trait:lower>](self, rhs: Value) -> Self::Output + { + Self {$( + $vec_field: self.$vec_field.[<$op_trait:lower>](rhs.clone()) + ),+} + } + } + + impl<Value> [<$op_trait Assign>]<Value> for $vector<Value> + where + Value: [<$op_trait Assign>] + Clone, + { + fn [<$op_trait:lower _assign>](&mut self, rhs: Value) + { + $( + self.$vec_field.[<$op_trait:lower _assign>](rhs.clone()); + )+ + } + } + } + }; +} -#[derive(Debug, Default, Clone, Copy)] -pub struct Vec2<Value> +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Reflection)] +#[reflection(impl_with_generics(<f32>, <f64>, <u32>, <u16>))] +pub struct Vec2<Value: 'static> { pub x: Value, pub y: Value, } +impl<Value> Vec2<Value> +{ + pub fn into_array(self) -> [Value; 2] + { + self.into() + } +} + impl Vec2<u32> { pub const ZERO: Self = Self { x: 0, y: 0 }; } -impl<Value> Add<Value> for Vec2<Value> -where - Value: Add<Output = Value> + Clone, -{ - type Output = Self; +impl_math_op_traits!(impl Add for Vec2, (x, y)); +impl_math_op_traits!(impl Sub for Vec2, (x, y)); - fn add(self, rhs: Value) -> Self::Output - { - Self { - x: self.x + rhs.clone(), - y: self.y + rhs, - } - } -} +impl_math_op_traits!(impl Add<Value> for Vec2, (x, y)); +impl_math_op_traits!(impl Sub<Value> for Vec2, (x, y)); +impl_math_op_traits!(impl Mul<Value> for Vec2, (x, y)); +impl_math_op_traits!(impl Div<Value> for Vec2, (x, y)); -impl<Value> Sub<Value> for Vec2<Value> -where - Value: Sub<Output = Value> + Clone, +impl<Value> From<[Value; 2]> for Vec2<Value> { - type Output = Self; - - fn sub(self, rhs: Value) -> Self::Output + fn from([x, y]: [Value; 2]) -> Self { - Self { - x: self.x - rhs.clone(), - y: self.y - rhs, - } + Self { x, y } } } -impl<Value> Mul<Value> for Vec2<Value> -where - Value: Mul<Output = Value> + Clone, +impl<Value> From<(Value, Value)> for Vec2<Value> { - type Output = Self; - - fn mul(self, rhs: Value) -> Self::Output + fn from((x, y): (Value, Value)) -> Self { - Self { - x: self.x * rhs.clone(), - y: self.y * rhs, - } + Self { x, y } } } -impl<Value> Div<Value> for Vec2<Value> -where - Value: Div<Output = Value> + Clone, +impl<Value> From<Vec2<Value>> for [Value; 2] { - type Output = Self; - - fn div(self, rhs: Value) -> Self::Output + fn from(vec: Vec2<Value>) -> Self { - Self { - x: self.x / rhs.clone(), - y: self.y / rhs, - } + [vec.x, vec.y] } } -#[derive(Debug, Default, Clone, Copy)] -#[repr(C)] -pub struct Vec3<Value> +#[derive(Debug, Default, Clone, Copy, PartialEq, Reflection)] +#[reflection(impl_with_generics(<f32>, <f64>, <u32>, <u16>))] +pub struct Vec3<Value: 'static> { pub x: Value, pub y: Value, @@ -85,6 +128,11 @@ pub struct Vec3<Value> impl Vec3<f32> { + pub const BACK: Self = Self { x: 0.0, y: 0.0, z: -1.0 }; + pub const DOWN: Self = Self { x: 0.0, y: -1.0, z: 0.0 }; + pub const FRONT: Self = Self { x: 0.0, y: 0.0, z: 1.0 }; + pub const LEFT: Self = Self { x: -1.0, y: 0.0, z: 0.0 }; + pub const RIGHT: Self = Self { x: 1.0, y: 0.0, z: 0.0 }; pub const UP: Self = Self { x: 0.0, y: 1.0, z: 0.0 }; /// Returns the length of the vector. @@ -95,16 +143,18 @@ impl Vec3<f32> } /// Normalizes the vector, returning a unit vector. + /// + /// // Panics + /// When debug assertions are enabled, if the length is zero or not finite, + /// this functions panics. #[must_use] pub fn normalize(&self) -> Self { - let length = self.length(); + let length_recip = 1.0 / self.length(); - Self { - x: self.x / length, - y: self.y / length, - z: self.z / length, - } + debug_assert!(length_recip.is_finite()); + + *self * length_recip } /// Returns the cross product of this and another vector. @@ -125,89 +175,54 @@ impl Vec3<f32> (self.x * rhs.x) + (self.y * rhs.y) + (self.z * rhs.z) } - /// Returns a direction vector from the specified angle (in degrees). + /// Returns a normalized direction vector from the specified angle (in degrees). #[must_use] - pub fn direction_from_angle(pitch_degs: f32, yaw_degs: f32) -> Self + pub fn direction_from_deg_angles(angles: Angles<f32>) -> Self { Self { - x: yaw_degs.to_radians().cos() * pitch_degs.to_radians().cos(), - y: pitch_degs.to_radians().sin(), - z: yaw_degs.to_radians().sin() * pitch_degs.to_radians().cos(), + x: angles.yaw.to_radians().cos() * angles.pitch.to_radians().cos(), + y: angles.pitch.to_radians().sin(), + z: angles.yaw.to_radians().sin() * angles.pitch.to_radians().cos(), } + .normalize() } -} -impl<Value> Vec3<Value> -{ - pub fn as_ptr(&self) -> *const Value + pub fn into_deg_angles(self) -> Angles<f32> { - &self.x - } -} - -impl<Value> Sub for Vec3<Value> -where - Value: Sub<Value, Output = Value>, -{ - type Output = Self; + let Self { x, y, z } = self.normalize(); - fn sub(self, rhs: Self) -> Self::Output - { - Self::Output { - x: self.x - rhs.x, - y: self.y - rhs.y, - z: self.z - rhs.z, + Angles { + yaw: z.atan2(x).to_degrees(), + pitch: y.asin().to_degrees(), + roll: 0.0, } } } -impl<Value> Sub for &Vec3<Value> -where - for<'a, 'b> &'a Value: Sub<&'b Value, Output = Value>, +impl<Value> Vec3<Value> { - type Output = Vec3<Value>; - - fn sub(self, rhs: Self) -> Self::Output + pub fn into_array(self) -> [Value; 3] { - Self::Output { - x: &self.x - &rhs.x, - y: &self.y - &rhs.y, - z: &self.z - &rhs.z, - } + self.into() } -} -impl<Value> Add for Vec3<Value> -where - Value: Add<Value, Output = Value>, -{ - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output + pub fn from_vec4(source: Vec4<Value>) -> Self { - Self::Output { - x: self.x + rhs.x, - y: self.y + rhs.y, - z: self.z + rhs.z, + Self { + x: source.x, + y: source.y, + z: source.z, } } } -impl<Value> Add for &Vec3<Value> -where - for<'a, 'b> &'a Value: Add<&'b Value, Output = Value>, -{ - type Output = Vec3<Value>; +impl_math_op_traits!(impl Add for Vec3, (x, y, z)); +impl_math_op_traits!(impl Sub for Vec3, (x, y, z)); - fn add(self, rhs: Self) -> Self::Output - { - Self::Output { - x: &self.x + &rhs.x, - y: &self.y + &rhs.y, - z: &self.z + &rhs.z, - } - } -} +impl_math_op_traits!(impl Add<Value> for Vec3, (x, y, z)); +impl_math_op_traits!(impl Sub<Value> for Vec3, (x, y, z)); +impl_math_op_traits!(impl Mul<Value> for Vec3, (x, y, z)); +impl_math_op_traits!(impl Div<Value> for Vec3, (x, y, z)); impl<Value> Neg for Vec3<Value> where @@ -225,100 +240,85 @@ where } } -impl<Value> Add<Value> for Vec3<Value> -where - Value: Add<Value, Output = Value> + Clone, +impl From<f32> for Vec3<f32> { - type Output = Self; - - fn add(mut self, rhs: Value) -> Self::Output + fn from(value: f32) -> Self { - self.x = self.x + rhs.clone(); - self.y = self.y + rhs.clone(); - self.z = self.z + rhs.clone(); - - self + Self { x: value, y: value, z: value } } } -impl<Value> Sub<Value> for Vec3<Value> -where - Value: Sub<Value, Output = Value> + Clone, +impl<Value> From<[Value; 3]> for Vec3<Value> { - type Output = Self; - - fn sub(mut self, rhs: Value) -> Self::Output + fn from([x, y, z]: [Value; 3]) -> Self { - self.x = self.x - rhs.clone(); - self.y = self.y - rhs.clone(); - self.z = self.z - rhs.clone(); - - self + Self { x, y, z } } } -impl<Value> Mul<Value> for Vec3<Value> -where - Value: Mul<Value, Output = Value> + Clone, +impl<Value> From<Vec3<Value>> for [Value; 3] { - type Output = Self; - - fn mul(mut self, rhs: Value) -> Self::Output + fn from(vec: Vec3<Value>) -> Self { - self.x = self.x * rhs.clone(); - self.y = self.y * rhs.clone(); - self.z = self.z * rhs.clone(); - - self + [vec.x, vec.y, vec.z] } } -impl<Value> AddAssign for Vec3<Value> -where - Value: AddAssign<Value>, +#[derive(Debug, Default, Clone, Copy, PartialEq, Reflection)] +#[reflection(impl_with_generics(<f32>, <f64>, <u32>, <u16>))] +pub struct Vec4<Value: 'static> { - fn add_assign(&mut self, rhs: Self) - { - self.x += rhs.x; - self.y += rhs.y; - self.z += rhs.z; - } + pub x: Value, + pub y: Value, + pub z: Value, + pub w: Value, } -impl<Value> SubAssign for Vec3<Value> -where - Value: SubAssign<Value>, -{ - fn sub_assign(&mut self, rhs: Self) - { - self.x -= rhs.x; - self.y -= rhs.y; - self.z -= rhs.z; - } -} +impl_math_op_traits!(impl Add for Vec4, (x, y, z, w)); +impl_math_op_traits!(impl Sub for Vec4, (x, y, z, w)); +impl_math_op_traits!(impl Mul for Vec4, (x, y, z, w)); -impl<Value> From<Value> for Vec3<Value> -where - Value: Clone, +impl_math_op_traits!(impl Add<Value> for Vec4, (x, y, z, w)); +impl_math_op_traits!(impl Sub<Value> for Vec4, (x, y, z, w)); +impl_math_op_traits!(impl Mul<Value> for Vec4, (x, y, z, w)); +impl_math_op_traits!(impl Div<Value> for Vec4, (x, y, z, w)); + +impl<Value: Clone> From<Value> for Vec4<Value> { fn from(value: Value) -> Self { Self { x: value.clone(), y: value.clone(), - z: value, + z: value.clone(), + w: value, } } } -impl<Value> From<Color<Value>> for Vec3<Value> +impl<Value> From<[Value; 4]> for Vec4<Value> { - fn from(color: Color<Value>) -> Self + fn from(values: [Value; 4]) -> Self { - Self { - x: color.red, - y: color.green, - z: color.blue, - } + let [x, y, z, w] = values; + + Self { x, y, z, w } + } +} + +impl<Value> From<Vec4<Value>> for [Value; 4] +{ + fn from(vec: Vec4<Value>) -> Self + { + [vec.x, vec.y, vec.z, vec.w] } } + +#[derive(Debug, Clone, Copy, Reflection)] +#[reflection(impl_with_generics(<f32>, <f64>))] +pub struct Angles<Value> +{ + pub yaw: Value, + pub pitch: Value, + pub roll: Value, +} diff --git a/engine/src/delta_time.rs b/engine/src/delta_time.rs deleted file mode 100644 index 33a2fc8..0000000 --- a/engine/src/delta_time.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::time::{Duration, Instant}; - -use ecs::component::local::Local; -use ecs::sole::Single; -use ecs::{Component, Sole}; - -#[derive(Debug, Clone, Default, Sole)] -pub struct DeltaTime -{ - pub duration: Duration, -} - -#[derive(Debug, Clone, Default, Component)] -pub struct LastUpdate -{ - pub time: Option<Instant>, -} - -/// Updates the current delta time. -/// -/// # Panics -/// Will panic if no delta time component exists. -pub fn update(mut delta_time: Single<DeltaTime>, mut last_update: Local<LastUpdate>) -{ - let current_time = Instant::now(); - - if let Some(last_update_time) = last_update.time { - delta_time.duration = current_time.duration_since(last_update_time); - } - - last_update.time = Some(current_time); -} diff --git a/engine/src/draw_flags.rs b/engine/src/draw_flags.rs index df5eed1..b32bb14 100644 --- a/engine/src/draw_flags.rs +++ b/engine/src/draw_flags.rs @@ -1,11 +1,11 @@ -use ecs::Component; - -use crate::util::builder; +use crate::builder; +use crate::ecs::Component; +use crate::reflection::Reflection; builder! { /// Flags for how a object should be drawn. #[builder(name = Builder, derives = (Debug, Default, Clone))] -#[derive(Debug, Default, Clone, Component)] +#[derive(Debug, Default, Clone, Component, Reflection)] #[non_exhaustive] pub struct DrawFlags { @@ -22,14 +22,16 @@ impl DrawFlags } } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Reflection)] pub struct PolygonModeConfig { pub face: PolygonModeFace, pub mode: PolygonMode, } -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Reflection, +)] pub enum PolygonMode { Point, @@ -39,7 +41,9 @@ pub enum PolygonMode Fill, } -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Reflection, +)] pub enum PolygonModeFace { Front, @@ -50,5 +54,5 @@ pub enum PolygonModeFace } /// Component that makes a object not be drawn. -#[derive(Debug, Clone, Copy, Component)] +#[derive(Debug, Default, Clone, Copy, Component, Reflection)] pub struct NoDraw; diff --git a/engine/src/event.rs b/engine/src/event.rs deleted file mode 100644 index e5ae486..0000000 --- a/engine/src/event.rs +++ /dev/null @@ -1,27 +0,0 @@ -pub use ecs::event::start::Start; -use ecs::event::Event; - -#[derive(Debug)] -pub struct Update; - -impl Event for Update {} - -#[derive(Debug)] -pub struct PreUpdate; - -impl Event for PreUpdate {} - -#[derive(Debug)] -pub struct Present; - -impl Event for Present {} - -#[derive(Debug)] -pub struct PostPresent; - -impl Event for PostPresent {} - -#[derive(Debug)] -pub struct Conclude; - -impl Event for Conclude {} diff --git a/engine/src/file_format/wavefront/common.rs b/engine/src/file_format/wavefront/common.rs index 8cdfb34..83fd8af 100644 --- a/engine/src/file_format/wavefront/common.rs +++ b/engine/src/file_format/wavefront/common.rs @@ -1,5 +1,7 @@ use std::num::{ParseFloatError, ParseIntError}; +use engine_ecs::util::array_vec::ArrayVec; + macro_rules! keyword { ( $(#[$attr: meta])* @@ -68,9 +70,11 @@ where let mut parts = line.split(' '); let keyword = KeywordT::from_str(parts.next().unwrap(), line_no)?; - let arguments = parts - .map(|part| Value::parse::<KeywordT>(part, line_no)) - .collect::<Result<Vec<_>, _>>()?; + let mut arguments = ArrayVec::<Value, 16>::default(); + + for part in parts { + arguments.push(Value::parse::<KeywordT>(part, line_no)?); + } Ok(Some(Statement { keyword, arguments })) } @@ -265,7 +269,7 @@ impl Value pub struct Statement<KeywordT: Keyword> { pub keyword: KeywordT, - pub arguments: Vec<Value>, + pub arguments: ArrayVec<Value, 16>, } impl<KeywordT: Keyword> Statement<KeywordT> diff --git a/engine/src/file_format/wavefront/mtl.rs b/engine/src/file_format/wavefront/mtl.rs index ef6e894..24387b9 100644 --- a/engine/src/file_format/wavefront/mtl.rs +++ b/engine/src/file_format/wavefront/mtl.rs @@ -2,17 +2,15 @@ //! //! File format documentation: <https://paulbourke.net/dataformats/mtl> -use std::path::Path; +use std::path::{Path, PathBuf}; -use crate::color::Color; +use crate::color::{Color, Rgb}; use crate::file_format::wavefront::common::{ keyword, parse_statement_line, ParsingError, Statement, }; -use crate::material::{Builder as MaterialBuilder, Material}; -use crate::texture::{Error as TextureError, Texture}; /// Parses the content of a Wavefront `.mtl`. /// @@ -44,25 +42,47 @@ pub fn parse(obj_content: &str) -> Result<Vec<NamedMaterial>, Error> .filter(|(_, statement)| matches!(statement.keyword, Keyword::Newmtl)) .count(); - #[cfg(feature = "debug")] tracing::debug!("Material count: {material_cnt}"); statements_to_materials(statements, material_cnt) } #[derive(Debug, Clone)] +#[non_exhaustive] pub struct NamedMaterial { pub name: String, - pub material: Material, + pub ambient: Color<f32>, + pub diffuse: Color<f32>, + pub specular: Color<f32>, + pub ambient_map: Option<TextureMap>, + pub diffuse_map: Option<TextureMap>, + pub specular_map: Option<TextureMap>, + pub shininess: f32, +} + +impl Default for NamedMaterial +{ + fn default() -> Self + { + Self { + name: String::new(), + ambient: Color::Rgb(Rgb::<f32>::white()), + diffuse: Color::Rgb(Rgb::<f32>::white()), + specular: Color::Rgb(Rgb::<f32>::white()), + ambient_map: None, + diffuse_map: None, + specular_map: None, + shininess: 0.0, + } + } } #[derive(Debug, Clone)] -pub struct UnfinishedNamedMaterial +#[non_exhaustive] +pub struct TextureMap { - name: String, - material_builder: MaterialBuilder, - ready: bool, + pub path: PathBuf, } #[derive(Debug, thiserror::Error)] @@ -71,8 +91,14 @@ pub enum Error #[error(transparent)] ParsingError(#[from] ParsingError), - #[error("Failed to open texture")] - TextureError(#[from] TextureError), + #[error( + "A material start statement (newmtl) is expected before statement at line {}", + line_no + )] + ExpectedMaterialStartStmtBeforeStmt + { + line_no: usize + }, #[error( "Unsupported number of arguments ({arg_count}) to {keyword} at line {line_no}" @@ -93,7 +119,7 @@ pub enum Error }, } -#[cfg_attr(feature = "debug", tracing::instrument(skip_all))] +#[tracing::instrument(skip_all)] fn statements_to_materials( statements: impl IntoIterator<Item = (usize, Statement<Keyword>)>, material_cnt: usize, @@ -101,63 +127,52 @@ fn statements_to_materials( { let mut materials = Vec::<NamedMaterial>::with_capacity(material_cnt); - let mut curr_material = UnfinishedNamedMaterial { - name: String::new(), - material_builder: MaterialBuilder::new(), - ready: false, - }; - for (line_no, statement) in statements { if statement.keyword == Keyword::Newmtl { - if curr_material.ready { - #[cfg(feature = "debug")] - tracing::debug!("Building material"); - - let material = curr_material.material_builder.clone().build(); - - materials.push(NamedMaterial { name: curr_material.name, material }); - } - let name = statement.get_text_arg(0, line_no)?; - curr_material.name = name.to_string(); - curr_material.ready = true; + materials.push(NamedMaterial { + name: name.to_string(), + ..Default::default() + }); continue; } - if !curr_material.ready { - // Discard statements not belonging to a material - continue; + let Some(curr_material) = materials.last_mut() else { + return Err(Error::ExpectedMaterialStartStmtBeforeStmt { line_no }); }; match statement.keyword { Keyword::Ka => { let color = get_color_from_statement(&statement, line_no)?; - #[cfg(feature = "debug")] - tracing::debug!("Adding ambient color"); + tracing::debug!( + "Adding ambient color {color:?} to material {}", + curr_material.name + ); - curr_material.material_builder = - curr_material.material_builder.ambient(color); + curr_material.ambient = color; } Keyword::Kd => { let color = get_color_from_statement(&statement, line_no)?; - #[cfg(feature = "debug")] - tracing::debug!("Adding diffuse color"); + tracing::debug!( + "Adding diffuse color {color:?} to material {}", + curr_material.name + ); - curr_material.material_builder = - curr_material.material_builder.diffuse(color); + curr_material.diffuse = color; } Keyword::Ks => { let color = get_color_from_statement(&statement, line_no)?; - #[cfg(feature = "debug")] - tracing::debug!("Adding specular color"); + tracing::debug!( + "Adding specular color {color:?} to material {}", + curr_material.name + ); - curr_material.material_builder = - curr_material.material_builder.specular(color); + curr_material.specular = color; } Keyword::MapKa => { if statement.arguments.len() > 1 { @@ -170,55 +185,75 @@ fn statements_to_materials( let texture_file_path = statement.get_text_arg(0, line_no)?; - let texture = Texture::open(Path::new(texture_file_path))?; - - #[cfg(feature = "debug")] - tracing::debug!("Adding ambient map"); + tracing::debug!( + "Adding ambient map {texture_file_path} to material {}", + curr_material.name + ); - let texture_id = texture.id(); - - curr_material.material_builder = curr_material - .material_builder - .texture(texture) - .ambient_map(texture_id); + curr_material.ambient_map = Some(TextureMap { + path: Path::new(texture_file_path).to_path_buf(), + }); } Keyword::MapKd => { - let texture = get_map_from_texture(&statement, line_no)?; + if statement.arguments.len() > 1 { + return Err(Error::UnsupportedArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no, + }); + } - #[cfg(feature = "debug")] - tracing::debug!("Adding diffuse map"); + let texture_file_path = statement.get_text_arg(0, line_no)?; - let texture_id = texture.id(); + tracing::debug!( + "Adding diffuse map {texture_file_path} to material {}", + curr_material.name + ); - curr_material.material_builder = curr_material - .material_builder - .texture(texture) - .diffuse_map(texture_id); + curr_material.diffuse_map = Some(TextureMap { + path: Path::new(texture_file_path).to_path_buf(), + }); } Keyword::MapKs => { - let texture = get_map_from_texture(&statement, line_no)?; + if statement.arguments.len() > 1 { + return Err(Error::UnsupportedArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no, + }); + } - #[cfg(feature = "debug")] - tracing::debug!("Adding specular map"); + let texture_file_path = statement.get_text_arg(0, line_no)?; - let texture_id = texture.id(); + tracing::debug!( + "Adding specular map {texture_file_path} to material {}", + curr_material.name + ); - curr_material.material_builder = curr_material - .material_builder - .texture(texture) - .specular_map(texture_id); + curr_material.specular_map = Some(TextureMap { + path: Path::new(texture_file_path).to_path_buf(), + }); } - Keyword::Newmtl => {} - } - } + Keyword::Ns => { + if statement.arguments.len() != 1 { + return Err(Error::UnsupportedArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no, + }); + } - if curr_material.ready { - #[cfg(feature = "debug")] - tracing::debug!("Building last material"); + let shininess = statement.get_float_arg(0, line_no)?; - let material = curr_material.material_builder.build(); + tracing::debug!( + "Adding shininess {shininess} to material {}", + curr_material.name + ); - materials.push(NamedMaterial { name: curr_material.name, material }); + curr_material.shininess = shininess; + } + Keyword::Newmtl => {} + } } Ok(materials) @@ -241,25 +276,7 @@ fn get_color_from_statement( let green = statement.get_float_arg(1, line_no)?; let blue = statement.get_float_arg(2, line_no)?; - Ok(Color { red, green, blue }) -} - -fn get_map_from_texture( - statement: &Statement<Keyword>, - line_no: usize, -) -> Result<Texture, Error> -{ - if statement.arguments.len() > 1 { - return Err(Error::UnsupportedArgumentCount { - keyword: statement.keyword.to_string(), - arg_count: statement.arguments.len(), - line_no, - }); - } - - let texture_file_path = statement.get_text_arg(0, line_no)?; - - Ok(Texture::open(Path::new(texture_file_path))?) + Ok(Color::Rgb(Rgb { r: red, g: green, b: blue })) } keyword! { @@ -280,5 +297,7 @@ keyword! { #[keyword(rename = "map_Ks")] MapKs, + + Ns, } } diff --git a/engine/src/file_format/wavefront/obj.rs b/engine/src/file_format/wavefront/obj.rs index 88e6580..dbcee33 100644 --- a/engine/src/file_format/wavefront/obj.rs +++ b/engine/src/file_format/wavefront/obj.rs @@ -2,20 +2,28 @@ //! //! File format documentation: <https://paulbourke.net/dataformats/obj> +use std::collections::HashMap; use std::fs::read_to_string; use std::path::PathBuf; +use engine_ecs::util::Either; + use crate::file_format::wavefront::common::{ keyword, parse_statement_line, + Keyword as _, ParsingError, Statement, Triplet, }; -use crate::mesh::Mesh; -use crate::util::try_option; +use crate::mesh::vertex_buffer::{ + NamedVertexAttr, + VertexAttrInfo, + VertexBuffer as MeshVertexBuffer, + VertexLabel, +}; +use crate::mesh::{Mesh, VertexAttrType}; use crate::vector::{Vec2, Vec3}; -use crate::vertex::{Builder as VertexBuilder, Vertex}; /// Parses the content of a Wavefront `.obj`. /// @@ -28,34 +36,37 @@ pub fn parse(obj_content: &str) -> Result<Obj, Error> .enumerate() .map(|(line_index, line)| (line_index + 1, line)); - let statements = lines - .map(|(line_no, line)| (line_no, parse_statement_line::<Keyword>(line, line_no))) - .filter_map(|(line_no, result)| { - let opt_statement = match result { - Ok(opt_statement) => opt_statement, - Err(err) => { - return Some(Err(err)); - } - }; - - Some(Ok((line_no, opt_statement?))) - }) - .collect::<Result<Vec<_>, _>>()?; - - let vertex_positions = get_vertex_positions_from_statements(&statements)?; - let texture_positions = get_texture_positions_from_statements(&statements)?; - let vertex_normals = get_vertex_normals_from_statements(&statements)?; - let material_specs = get_material_specs_from_statements(&statements)?; - let faces = get_faces_from_statements(&statements, &material_specs)?; - let mtl_libs = get_mtl_libs_from_statements(&statements)?; - - Ok(Obj { - vertex_positions, - vertex_normals, - texture_positions, - faces, - mtl_libs, - }) + let mut item_counts = ItemCounts::default(); + + for (line_index, line) in obj_content.lines().enumerate() { + if line.is_empty() || line.starts_with('#') { + continue; + } + + let Some((keyword, _)) = line.split_once(" ") else { + continue; + }; + + let Ok(keyword) = Keyword::from_str(keyword, line_index + 1) else { + continue; + }; + + item_counts.increment_item_cnt_by_keyword(keyword); + } + + let mut obj = Obj::with_capacities_from_item_cnts(item_counts); + + let mut parsing_state = ParsingState::default(); + + for (line_no, line) in lines { + let Some(statement) = parse_statement_line::<Keyword>(line, line_no)? else { + continue; + }; + + obj.handle_statement(line_no, statement, &mut parsing_state)?; + } + + Ok(obj) } /// The data of a Wavefront object file. @@ -67,7 +78,8 @@ pub struct Obj pub vertex_normals: Vec<Vec3<f32>>, pub texture_positions: Vec<Vec2<f32>>, pub faces: Vec<Face>, - pub mtl_libs: Vec<Vec<PathBuf>>, + pub mtl_libs: Vec<PathBuf>, + pub unique_used_material_names: Vec<Box<str>>, } impl Obj @@ -80,28 +92,124 @@ impl Obj /// - A face's texture position cannot be found /// - A face's vertex normal cannot be found /// - A face index does not fit in a [`u32`] - pub fn to_mesh(&self) -> Result<Mesh, Error> + pub fn to_mesh(&self, options: ToMeshOptions) -> Result<Mesh, Error> { - let vertices = self - .faces - .iter() - .flat_map(|face| face.vertices.clone()) - .map(|face_vertex| face_vertex.to_vertex(self)) - .collect::<Result<Vec<_>, Error>>()?; - - Ok(Mesh::new( - vertices, - Some( - self.faces - .iter() - .flat_map(|face| face.vertices.clone()) - .enumerate() - .map(|(index, _)| { - u32::try_from(index).map_err(|_| Error::FaceIndexTooBig(index)) - }) - .collect::<Result<Vec<_>, _>>()?, - ), - )) + let mut vertex_buf = MeshVertexBuffer::with_capacity( + &[ + VertexAttrInfo { + label: VertexLabel::Position, + ty: VertexAttrType::Float32Array { length: 3 }, + }, + VertexAttrInfo { + label: VertexLabel::UvFromTopLeft, + ty: VertexAttrType::Float32Array { length: 2 }, + }, + VertexAttrInfo { + label: VertexLabel::Normal, + ty: VertexAttrType::Float32Array { length: 3 }, + }, + ], + self.faces.len() * 3, + ); + + let mut indices = Vec::<u32>::with_capacity(self.faces.len() * 3); + + let mut added_face_vertices = + HashMap::<FaceVertex, u32>::with_capacity(self.faces.len() * 3); + + for face in &self.faces { + for face_vertex in &face.vertices { + if let Some(index) = added_face_vertices.get(&face_vertex) { + indices.push(*index); + + continue; + } + + let pos = self + .vertex_positions + .get(face_vertex.position as usize - 1) + .ok_or(Error::FaceVertexPositionNotFound { + vertex_pos_index: face_vertex.position, + })? + .clone(); + + let texture_pos = face_vertex.texture.map_or_else( + || { + if !self.texture_positions.is_empty() { + tracing::warn!(concat!( + "Wavefront OBJ has texture coordinates ", + "but face vertex does not specify one" + )); + } + + Ok(Vec2::default()) + }, + |face_vertex_texture| { + self.texture_positions + .get(face_vertex_texture as usize - 1) + .ok_or(Error::FaceTexturePositionNotFound { + texture_pos_index: face_vertex_texture, + }) + .cloned() + }, + )?; + + let texture_pos = options + .y_flip_uvs + .then(|| Vec2 { x: texture_pos.x, y: -texture_pos.y }) + .unwrap_or(texture_pos); + + let normal = face_vertex.normal.map_or_else( + || { + if !self.vertex_normals.is_empty() { + tracing::warn!(concat!( + "Wavefront OBJ has normals ", + "but face vertex does not specify one" + )); + } + + Ok(Vec3::default()) + }, + |face_vertex_normal| { + self.vertex_normals + .get(face_vertex_normal as usize - 1) + .ok_or(Error::FaceVertexNormalNotFound { + vertex_normal_index: face_vertex_normal, + }) + .cloned() + }, + )?; + + vertex_buf.push(( + NamedVertexAttr { + label: VertexLabel::Position, + value: pos.into_array(), + }, + NamedVertexAttr { + label: VertexLabel::UvFromTopLeft, + value: texture_pos.into_array(), + }, + NamedVertexAttr { + label: VertexLabel::Normal, + value: normal.into_array(), + }, + )); + + let vertex_index = vertex_buf.len() - 1; + + let vertex_index = u32::try_from(vertex_index) + .map_err(|_| Error::FaceIndexTooBig(vertex_index))?; + + indices.push(vertex_index); + + added_face_vertices.insert(face_vertex.clone(), vertex_index); + } + } + + Ok(Mesh::builder() + .vertices(vertex_buf) + .indices(indices) + .build()) } /// Reads and parses the material libraries of this `Obj`. @@ -117,7 +225,6 @@ impl Obj { self.mtl_libs .iter() - .flatten() .map(|mtl_lib| { Ok(parse_material_lib(&read_to_string(mtl_lib).map_err( |err| MaterialLibsError::ReadingMaterialLibFailed { @@ -132,69 +239,257 @@ impl Obj }) .collect::<Result<Vec<_>, _>>() } -} -#[derive(Debug)] -#[non_exhaustive] -pub struct Face -{ - pub vertices: [FaceVertex; 3], - pub material_name: Option<String>, -} + fn with_capacities_from_item_cnts(item_counts: ItemCounts) -> Self + { + Self { + vertex_positions: Vec::with_capacity(item_counts.pos_cnt), + vertex_normals: Vec::with_capacity(item_counts.normal_cnt), + texture_positions: Vec::with_capacity(item_counts.uv_cnt), + faces: Vec::with_capacity(item_counts.face_cnt), + mtl_libs: Vec::with_capacity(item_counts.mtl_lib_cnt), + unique_used_material_names: Vec::with_capacity( + item_counts.material_usage_cnt, + ), + } + } -#[derive(Debug, Clone)] -pub struct FaceVertex -{ - pub position: u32, - pub texture: Option<u32>, - pub normal: Option<u32>, -} + fn handle_statement( + &mut self, + line_no: usize, + statement: Statement<Keyword>, + parsing_state: &mut ParsingState, + ) -> Result<(), Error> + { + match statement.keyword { + Keyword::V => self.handle_position_statement(line_no, statement), + Keyword::Vn => self.handle_normal_statement(line_no, statement), + Keyword::Vt => self.handle_uv_statement(line_no, statement), + Keyword::F => self.handle_face_statement(line_no, statement, parsing_state), + Keyword::Mtllib => self.handle_material_lib_statement(line_no, statement), + Keyword::Usemtl => { + self.handle_material_usage_statement(line_no, statement, parsing_state) + } + Keyword::O | Keyword::S => Ok(()), + } + } -impl FaceVertex -{ - /// Tries to convert this face vertex into a [`Vertex`]. - /// - /// # Errors - /// Returns `Err` if: - /// - The face's vertex position cannot be found in the given [`Obj`] - /// - The face's texture position cannot be found in the given [`Obj`] - /// - The face's vertex normal cannot be found in the given [`Obj`] - pub fn to_vertex(&self, obj: &Obj) -> Result<Vertex, Error> + fn handle_position_statement( + &mut self, + line_no: usize, + statement: Statement<Keyword>, + ) -> Result<(), Error> { - let mut vertex_builder = VertexBuilder::default(); + debug_assert_eq!(statement.keyword, Keyword::V); + + if statement.arguments.len() == 4 { + return Err(Error::UnsupportedArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no: line_no, + }); + } - let vertex_pos = *obj.vertex_positions.get(self.position as usize - 1).ok_or( - Error::FaceVertexPositionNotFound { vertex_pos_index: self.position }, - )?; + if statement.arguments.len() > 4 { + return Err(Error::InvalidArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no: line_no, + }); + } + + let x = statement.get_float_arg(0, line_no)?; + let y = statement.get_float_arg(1, line_no)?; + let z = statement.get_float_arg(2, line_no)?; - vertex_builder = vertex_builder.pos(vertex_pos); + self.vertex_positions.push(Vec3 { x, y, z }); + + Ok(()) + } - if let Some(face_vertex_texture) = self.texture { - let texture_pos = obj - .texture_positions - .get(face_vertex_texture as usize - 1) - .ok_or(Error::FaceTexturePositionNotFound { - texture_pos_index: face_vertex_texture, - })?; + fn handle_uv_statement( + &mut self, + line_no: usize, + statement: Statement<Keyword>, + ) -> Result<(), Error> + { + debug_assert_eq!(statement.keyword, Keyword::Vt); + + if statement.arguments.len() == 3 { + return Err(Error::UnsupportedArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no: line_no, + }); + } - vertex_builder = vertex_builder.texture_coords(*texture_pos); + if statement.arguments.len() > 3 { + return Err(Error::InvalidArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no: line_no, + }); } - if let Some(face_vertex_normal) = self.normal { - let vertex_normal = *obj - .vertex_normals - .get(face_vertex_normal as usize - 1) - .ok_or(Error::FaceVertexNormalNotFound { - vertex_normal_index: face_vertex_normal, - })?; + let u = statement.get_float_arg(0, line_no)?; + let v = statement.get_float_arg(1, line_no)?; - vertex_builder = vertex_builder.normal(vertex_normal); + self.texture_positions.push(Vec2 { x: u, y: v }); + + Ok(()) + } + + fn handle_normal_statement( + &mut self, + line_no: usize, + statement: Statement<Keyword>, + ) -> Result<(), Error> + { + debug_assert_eq!(statement.keyword, Keyword::Vn); + + if statement.arguments.len() > 3 { + return Err(Error::InvalidArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no: line_no, + }); } - Ok(vertex_builder.build()) + let i = statement.get_float_arg(0, line_no)?; + let j = statement.get_float_arg(1, line_no)?; + let k = statement.get_float_arg(2, line_no)?; + + self.vertex_normals.push(Vec3 { x: i, y: j, z: k }); + + Ok(()) + } + + fn handle_material_usage_statement( + &mut self, + line_no: usize, + statement: Statement<Keyword>, + parsing_state: &mut ParsingState, + ) -> Result<(), Error> + { + debug_assert_eq!(statement.keyword, Keyword::Usemtl); + + if statement.arguments.len() > 1 { + return Err(Error::InvalidArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no: line_no, + }); + } + + let material_name = statement + .get_text_arg(0, line_no)? + .to_owned() + .into_boxed_str(); + + if !self.unique_used_material_names.contains(&material_name) { + self.unique_used_material_names.push(material_name.clone()); + } + + parsing_state.current_material_name = Some(material_name); + + Ok(()) + } + + fn handle_face_statement( + &mut self, + line_no: usize, + statement: Statement<Keyword>, + parsing_state: &mut ParsingState, + ) -> Result<(), Error> + { + debug_assert_eq!(statement.keyword, Keyword::F); + + if statement.arguments.len() > 3 { + return Err(Error::UnsupportedArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no: line_no, + }); + } + + let vertex_a = statement.get_triplet_arg(0, line_no)?; + let vertex_b = statement.get_triplet_arg(1, line_no)?; + let vertex_c = statement.get_triplet_arg(2, line_no)?; + + self.faces.push(Face { + vertices: [vertex_a.into(), vertex_b.into(), vertex_c.into()], + material_name: parsing_state.current_material_name.clone(), + }); + + Ok(()) + } + + fn handle_material_lib_statement( + &mut self, + line_no: usize, + statement: Statement<Keyword>, + ) -> Result<(), Error> + { + debug_assert_eq!(statement.keyword, Keyword::Mtllib); + + for (index, value) in statement.arguments.iter().enumerate() { + let mtl_lib = PathBuf::from(value.to_text(index, line_no)?); + + self.mtl_libs.push(mtl_lib); + } + + Ok(()) } } +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct ToMeshOptions +{ + /// Whether all UVs should be flipped on the Y axis. This is necessary if the UVs + /// have their origin at the bottom left corner of the image + /// + /// The default is `true`. + pub y_flip_uvs: bool, +} + +impl ToMeshOptions +{ + /// Whether all UVs should be flipped on the Y axis. This is necessary if the UVs have + /// their origin at the bottom left corner of the image + /// + /// The default is `true`. + pub fn y_flip_uvs(mut self, y_flip_uvs: bool) -> Self + { + self.y_flip_uvs = y_flip_uvs; + self + } +} + +impl Default for ToMeshOptions +{ + fn default() -> Self + { + Self { y_flip_uvs: true } + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub struct Face +{ + pub vertices: [FaceVertex; 3], + pub material_name: Option<Box<str>>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct FaceVertex +{ + pub position: u32, + pub texture: Option<u32>, + pub normal: Option<u32>, +} + impl From<Triplet> for FaceVertex { fn from(triplet: Triplet) -> Self @@ -274,6 +569,39 @@ pub enum MaterialLibsError<MaterialLibParseError> }, } +#[derive(Debug, Default)] +struct ParsingState +{ + current_material_name: Option<Box<str>>, +} + +#[derive(Debug, Default)] +struct ItemCounts +{ + pos_cnt: usize, + uv_cnt: usize, + normal_cnt: usize, + face_cnt: usize, + mtl_lib_cnt: usize, + material_usage_cnt: usize, +} + +impl ItemCounts +{ + fn increment_item_cnt_by_keyword(&mut self, keyword: Keyword) + { + match keyword { + Keyword::V => self.pos_cnt += 1, + Keyword::Vn => self.normal_cnt += 1, + Keyword::Vt => self.uv_cnt += 1, + Keyword::F => self.face_cnt += 1, + Keyword::Mtllib => self.mtl_lib_cnt += 1, + Keyword::Usemtl => self.material_usage_cnt += 1, + Keyword::O | Keyword::S => {} + } + } +} + keyword! { #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum Keyword { @@ -303,233 +631,6 @@ keyword! { } } -#[derive(Debug)] -struct MaterialSpecifier -{ - material_name: String, - line_no: usize, -} - -fn find_material_specifier_for_line_no( - material_specifiers: &[MaterialSpecifier], - line_no: usize, -) -> Option<&MaterialSpecifier> -{ - material_specifiers - .iter() - .rev() - .find(|material_specifier| material_specifier.line_no < line_no) -} - -enum Either<ValA, ValB> -{ - A(ValA), - B(ValB), -} - -impl<ValA, ValB, Item> Iterator for Either<ValA, ValB> -where - ValA: Iterator<Item = Item>, - ValB: Iterator<Item = Item>, -{ - type Item = Item; - - fn next(&mut self) -> Option<Self::Item> - { - match self { - Self::A(iter_a) => iter_a.next(), - Self::B(iter_b) => iter_b.next(), - } - } -} - -fn get_vertex_positions_from_statements( - statements: &[(usize, Statement<Keyword>)], -) -> Result<Vec<Vec3<f32>>, Error> -{ - statements - .iter() - .filter_map(|(line_no, statement)| { - if statement.keyword != Keyword::V { - return None; - } - - if statement.arguments.len() == 4 { - return Some(Err(Error::UnsupportedArgumentCount { - keyword: statement.keyword.to_string(), - arg_count: statement.arguments.len(), - line_no: *line_no, - })); - } - - if statement.arguments.len() > 4 { - return Some(Err(Error::InvalidArgumentCount { - keyword: statement.keyword.to_string(), - arg_count: statement.arguments.len(), - line_no: *line_no, - })); - } - - let x = try_option!(statement.get_float_arg(0, *line_no)); - let y = try_option!(statement.get_float_arg(1, *line_no)); - let z = try_option!(statement.get_float_arg(2, *line_no)); - - Some(Ok(Vec3 { x, y, z })) - }) - .collect::<Result<Vec<_>, Error>>() -} - -fn get_texture_positions_from_statements( - statements: &[(usize, Statement<Keyword>)], -) -> Result<Vec<Vec2<f32>>, Error> -{ - statements - .iter() - .filter_map(|(line_no, statement)| { - if statement.keyword != Keyword::Vt { - return None; - } - - if statement.arguments.len() == 3 { - return Some(Err(Error::UnsupportedArgumentCount { - keyword: statement.keyword.to_string(), - arg_count: statement.arguments.len(), - line_no: *line_no, - })); - } - - if statement.arguments.len() > 3 { - return Some(Err(Error::InvalidArgumentCount { - keyword: statement.keyword.to_string(), - arg_count: statement.arguments.len(), - line_no: *line_no, - })); - } - - let u = try_option!(statement.get_float_arg(0, *line_no)); - let v = try_option!(statement.get_float_arg(1, *line_no)); - - Some(Ok(Vec2 { x: u, y: v })) - }) - .collect::<Result<Vec<_>, Error>>() -} - -fn get_vertex_normals_from_statements( - statements: &[(usize, Statement<Keyword>)], -) -> Result<Vec<Vec3<f32>>, Error> -{ - statements - .iter() - .filter_map(|(line_no, statement)| { - if statement.keyword != Keyword::Vn { - return None; - } - - if statement.arguments.len() > 3 { - return Some(Err(Error::InvalidArgumentCount { - keyword: statement.keyword.to_string(), - arg_count: statement.arguments.len(), - line_no: *line_no, - })); - } - - let i = try_option!(statement.get_float_arg(0, *line_no)); - let j = try_option!(statement.get_float_arg(1, *line_no)); - let k = try_option!(statement.get_float_arg(2, *line_no)); - - Some(Ok(Vec3 { x: i, y: j, z: k })) - }) - .collect::<Result<Vec<_>, Error>>() -} - -fn get_material_specs_from_statements( - statements: &[(usize, Statement<Keyword>)], -) -> Result<Vec<MaterialSpecifier>, Error> -{ - statements - .iter() - .filter_map(|(line_no, statement)| { - if statement.keyword != Keyword::Usemtl { - return None; - } - - if statement.arguments.len() > 1 { - return Some(Err(Error::InvalidArgumentCount { - keyword: statement.keyword.to_string(), - arg_count: statement.arguments.len(), - line_no: *line_no, - })); - } - - let material_name = try_option!(statement.get_text_arg(0, *line_no)); - - Some(Ok(MaterialSpecifier { - material_name: material_name.to_string(), - line_no: *line_no, - })) - }) - .collect::<Result<Vec<_>, Error>>() -} - -fn get_faces_from_statements( - statements: &[(usize, Statement<Keyword>)], - material_specifiers: &[MaterialSpecifier], -) -> Result<Vec<Face>, Error> -{ - statements - .iter() - .filter_map(|(line_no, statement)| { - if statement.keyword != Keyword::F { - return None; - } - - if statement.arguments.len() > 3 { - return Some(Err(Error::UnsupportedArgumentCount { - keyword: statement.keyword.to_string(), - arg_count: statement.arguments.len(), - line_no: *line_no, - })); - } - - let vertex_a = try_option!(statement.get_triplet_arg(0, *line_no)).into(); - let vertex_b = try_option!(statement.get_triplet_arg(1, *line_no)).into(); - let vertex_c = try_option!(statement.get_triplet_arg(2, *line_no)).into(); - - let material_name = - find_material_specifier_for_line_no(material_specifiers, *line_no) - .map(|material_specifier| material_specifier.material_name.clone()); - - Some(Ok(Face { - vertices: [vertex_a, vertex_b, vertex_c], - material_name, - })) - }) - .collect::<Result<Vec<Face>, Error>>() -} - -fn get_mtl_libs_from_statements( - statements: &[(usize, Statement<Keyword>)], -) -> Result<Vec<Vec<PathBuf>>, Error> -{ - statements - .iter() - .filter_map(|(line_no, statement)| { - if statement.keyword != Keyword::Mtllib { - return None; - } - - let mtl_lib_paths = try_option!(statement - .arguments - .iter() - .enumerate() - .map(|(index, value)| Ok(PathBuf::from(value.to_text(index, *line_no)?))) - .collect::<Result<Vec<_>, ParsingError>>()); - - Some(Ok(mtl_lib_paths)) - }) - .collect::<Result<Vec<_>, Error>>() -} - #[cfg(test)] mod tests { @@ -550,17 +651,26 @@ mod tests assert_eq!(obj.faces.len(), 3); assert_eq!( - obj.faces[0].material_name.as_ref().expect("Expected Some"), + obj.faces[0] + .material_name + .as_deref() + .expect("Expected Some"), "dark-green" ); assert_eq!( - obj.faces[1].material_name.as_ref().expect("Expected Some"), + obj.faces[1] + .material_name + .as_deref() + .expect("Expected Some"), "dark-green" ); assert_eq!( - obj.faces[2].material_name.as_ref().expect("Expected Some"), + obj.faces[2] + .material_name + .as_deref() + .expect("Expected Some"), "light-pink" ); } diff --git a/engine/src/image.rs b/engine/src/image.rs new file mode 100644 index 0000000..b01ea53 --- /dev/null +++ b/engine/src/image.rs @@ -0,0 +1,363 @@ +use std::fs::File; +use std::io::{BufRead, BufReader, Seek}; +use std::marker::PhantomData; +use std::ops::Deref; +use std::path::Path; + +use image_rs::GenericImageView as _; + +use crate::color::{Pixel as ColorPixel, Rgb, Rgba}; +use crate::data_types::dimens::Dimens; +use crate::vector::Vec2; + +#[derive(Debug, Clone)] +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::Io)?); + + Self::from_reader( + buffered_reader, + Format::from_image_rs( + image_rs::ImageFormat::from_path(path) + .map_err(|_| Error::UnsupportedFormat)?, + ) + .ok_or(Error::UnsupportedFormat)?, + ) + } + + pub fn from_reader(reader: impl BufRead + Seek, format: Format) + -> Result<Self, Error> + { + let image_reader = + image_rs::ImageReader::with_format(reader, format.into_image_rs()); + + let image = match image_reader + .decode() + .map_err(|err| Error::DecodeFailed(DecodeError(err)))? + { + image @ (image_rs::DynamicImage::ImageLuma8(_) + | image_rs::DynamicImage::ImageLumaA8(_) + | image_rs::DynamicImage::ImageLuma16(_) + | image_rs::DynamicImage::ImageLumaA16(_)) => image.into_rgba32f().into(), + image => image, + }; + + Ok(Self { inner: image }) + } + + pub fn from_pixels<PixelT, Buf>( + pixels: PixelBuffer<PixelT, Buf>, + ) -> Result<Self, PixelBufferLenIncorrectForSize> + where + PixelT: ColorPixel, + Buf: Deref<Target = [PixelT::Component]>, + Self: TryFrom<PixelBuffer<PixelT, Buf>, Error = PixelBufferLenIncorrectForSize>, + { + Self::try_from(pixels) + } + + pub fn from_color<PixelT: ColorPixel>(color: PixelT, size: Dimens<u32>) -> Self + where + Self: From<(PixelT, Dimens<u32>)>, + { + Self::from((color, size)) + } + + pub fn dimensions(&self) -> Dimens<u32> + { + self.inner.dimensions().into() + } + + pub fn color_type(&self) -> ColorType + { + self.inner.color().into() + } + + pub fn color_space_is_srgb(&self) -> bool + { + match self.inner.color_space().primaries { + image_rs::metadata::CicpColorPrimaries::SRgb => true, + _ => false, + } + } + + /// Returns a immutable view of a subsection of this image. + /// The `offset` argument sets the position of the top left corner of the view. + /// + /// # Panics + /// Panics if the view would be out of bounds + pub fn sub_view(&self, offset: Vec2<u32>, size: Dimens<u32>) -> SubView<'_> + { + assert!(offset.x + size.width <= self.dimensions().width); + assert!(offset.y + size.height <= self.dimensions().height); + + SubView { image: &self, offset, size } + } + + pub fn to_rgba8(&self) -> Self + { + Self { inner: self.inner.to_rgba8().into() } + } + + /// Consumes the image and returns a RGBA8 image. If it already is RGBA8, the image is + /// returned as is. If not, the image is converted to RGBA8. + pub fn into_rgba8(self) -> Self + { + Self { + inner: self.inner.into_rgba8().into(), + } + } + + pub fn as_bytes(&self) -> &[u8] + { + self.inner.as_bytes() + } + + pub fn into_bytes(self) -> Vec<u8> + { + self.inner.into_bytes() + } +} + +impl From<(Rgb<u8>, Dimens<u32>)> for Image +{ + fn from((color, size): (Rgb<u8>, Dimens<u32>)) -> Self + { + Self { + inner: image_rs::RgbImage::from_pixel( + size.width, + size.height, + image_rs::Rgb([color.r, color.g, color.b]), + ) + .into(), + } + } +} + +impl From<(Rgba<u8>, Dimens<u32>)> for Image +{ + fn from((color, size): (Rgba<u8>, Dimens<u32>)) -> Self + { + Self { + inner: image_rs::RgbaImage::from_pixel( + size.width, + size.height, + image_rs::Rgba([color.r, color.g, color.b, color.a]), + ) + .into(), + } + } +} + +macro_rules! gen_try_from_pixel_buffer_impl { + ($pixel: ident<$component: ty>) => { + impl<Buf> TryFrom<PixelBuffer<$pixel<$component>, Buf>> for Image + where + Buf: Deref<Target = [$component]>, + { + type Error = PixelBufferLenIncorrectForSize; + + fn try_from( + pixels: PixelBuffer<$pixel<$component>, Buf>, + ) -> Result<Self, Self::Error> + { + Ok( + Image { + inner: image_rs::ImageBuffer::< + image_rs::$pixel<$component>, + Vec<_>, + >::from_raw( + pixels.size.width, + pixels.size.height, + pixels.buf.deref().to_vec(), + ) + .ok_or(PixelBufferLenIncorrectForSize)? + .into(), + }, + ) + } + } + }; +} + +gen_try_from_pixel_buffer_impl!(Rgb<u8>); +gen_try_from_pixel_buffer_impl!(Rgb<u16>); +gen_try_from_pixel_buffer_impl!(Rgb<f32>); + +gen_try_from_pixel_buffer_impl!(Rgba<u8>); +gen_try_from_pixel_buffer_impl!(Rgba<u16>); +gen_try_from_pixel_buffer_impl!(Rgba<f32>); + +#[derive(Debug)] +pub struct PixelBuffer<PixelT, Buf> +where + PixelT: ColorPixel, + Buf: Deref<Target = [PixelT::Component]>, +{ + buf: Buf, + size: Dimens<u32>, + _pd: PhantomData<PixelT>, +} + +impl<PixelT, Buf> PixelBuffer<PixelT, Buf> +where + PixelT: ColorPixel, + Buf: Deref<Target = [PixelT::Component]>, +{ + pub fn new(buf: Buf, size: Dimens<u32>) -> Self + { + Self { buf, size, _pd: PhantomData } + } +} + +/// An enumeration over supported color types and bit depths +#[derive(Copy, PartialEq, Eq, Debug, Clone, Hash)] +#[non_exhaustive] +pub enum ColorType +{ + /// Pixel contains 8-bit R, G and B channels + Rgb8, + + /// Pixel is 8-bit RGB with an alpha channel + Rgba8, + + /// 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::Rgb8 => Self::Rgb8, + image_rs::ColorType::Rgba8 => Self::Rgba8, + image_rs::ColorType::Rgb16 => Self::Rgb16, + image_rs::ColorType::Rgba16 => Self::Rgba16, + image_rs::ColorType::Rgb32F => Self::Rgb32F, + image_rs::ColorType::Rgba32F => Self::Rgba32F, + image_rs::ColorType::L8 + | image_rs::ColorType::La8 + | image_rs::ColorType::L16 + | image_rs::ColorType::La16 => unimplemented!(), + _ => { + panic!("Unrecognized image_rs::ColorType variant"); + } + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +#[non_exhaustive] +pub enum Format +{ + Png, + Jpeg, + Ico, +} + +impl Format +{ + fn into_image_rs(self) -> image_rs::ImageFormat + { + match self { + Self::Png => image_rs::ImageFormat::Png, + Self::Jpeg => image_rs::ImageFormat::Jpeg, + Self::Ico => image_rs::ImageFormat::Ico, + } + } + + fn from_image_rs(format: image_rs::ImageFormat) -> Option<Self> + { + match format { + image_rs::ImageFormat::Png => Some(Self::Png), + image_rs::ImageFormat::Jpeg => Some(Self::Jpeg), + image_rs::ImageFormat::Ico => Some(Self::Ico), + _ => None, + } + } +} + +pub struct SubView<'image> +{ + image: &'image Image, + offset: Vec2<u32>, + size: Dimens<u32>, +} + +impl SubView<'_> +{ + pub fn to_image(&self) -> Image + { + fn create_sub_image<Image>( + image: Image, + sub_view: &SubView<'_>, + ) -> image_rs::SubImage<Image> + { + let SubView { image: _, offset, size } = sub_view; + + image_rs::SubImage::new(image, offset.x, offset.y, size.width, size.height) + } + + Image { + inner: match &self.image.inner { + image_rs::DynamicImage::ImageRgb8(image) => { + create_sub_image(image, self).to_image().into() + } + image_rs::DynamicImage::ImageRgba8(image) => { + create_sub_image(image, self).to_image().into() + } + image_rs::DynamicImage::ImageRgb16(image) => { + create_sub_image(image, self).to_image().into() + } + image_rs::DynamicImage::ImageRgba16(image) => { + create_sub_image(image, self).to_image().into() + } + image_rs::DynamicImage::ImageRgb32F(image) => { + create_sub_image(image, self).to_image().into() + } + image_rs::DynamicImage::ImageRgba32F(image) => { + create_sub_image(image, self).to_image().into() + } + _ => unimplemented!(), + }, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error("I/O error")] + Io(#[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); + +#[derive(Debug, thiserror::Error)] +#[error("Length of pixel buffer is incorrect for the given image size")] +pub struct PixelBufferLenIncorrectForSize; diff --git a/engine/src/input.rs b/engine/src/input.rs index f4166f6..c5e6d09 100644 --- a/engine/src/input.rs +++ b/engine/src/input.rs @@ -1,242 +1,26 @@ -use std::collections::HashMap; +use crate::ecs::extension::Collector as ExtensionCollector; +use crate::ecs::pair::ChildOf; +use crate::ecs::phase::Phase; +use crate::ecs::{declare_entity, pair}; +use crate::windowing::PHASE as WINDOWING_PHASE; -use ecs::extension::Collector as ExtensionCollector; -use ecs::sole::Single; -use ecs::Sole; +pub mod keyboard; +pub mod mouse; -use crate::event::{ - PostPresent as PostPresentEvent, - PreUpdate as PreUpdateEvent, - Start as StartEvent, -}; -use crate::vector::Vec2; -use crate::window::Window; - -mod reexports -{ - pub use crate::window::{Key, KeyState}; -} - -pub use reexports::*; - -#[derive(Debug, Sole)] -pub struct Keys -{ - map: HashMap<Key, KeyData>, -} - -impl Keys -{ - #[must_use] - pub fn new() -> Self - { - Self { - map: Key::KEYS - .iter() - .map(|key| { - ( - *key, - KeyData { - state: KeyState::Released, - prev_tick_state: KeyState::Released, - }, - ) - }) - .collect(), - } - } - - #[must_use] - pub fn get_key_state(&self, key: Key) -> KeyState - { - let Some(key_data) = self.map.get(&key) else { - unreachable!(); - }; - - key_data.state - } - - #[must_use] - pub fn get_prev_key_state(&self, key: Key) -> KeyState - { - let Some(key_data) = self.map.get(&key) else { - unreachable!(); - }; - - key_data.prev_tick_state - } - - pub fn set_key_state(&mut self, key: Key, new_key_state: KeyState) - { - if matches!(new_key_state, KeyState::Repeat) { - return; - } - - let Some(key_data) = self.map.get_mut(&key) else { - unreachable!(); - }; - - key_data.state = new_key_state; - } - - #[must_use] - pub fn is_anything_pressed(&self) -> bool - { - self.map - .values() - .any(|key_data| matches!(key_data.state, KeyState::Pressed)) - } -} - -impl Default for Keys -{ - fn default() -> Self - { - Self::new() - } -} - -#[derive(Debug, Default, Clone, Sole)] -pub struct Cursor -{ - pub position: Vec2<f64>, - pub has_moved: bool, -} - -#[derive(Debug, Clone, Sole)] -pub struct CursorFlags -{ - /// This flag is set in two situations: - /// A: The window has just started - /// B: The window has gained focus again after losing focus. - /// - /// This flag only lasts a single tick then it is cleared (at the beginning of the - /// next tick). - pub is_first_move: CursorFlag, -} - -impl Default for CursorFlags -{ - fn default() -> Self - { - Self { - is_first_move: CursorFlag { flag: true, ..Default::default() }, - } - } -} - -#[derive(Debug, Default, Clone)] -pub struct CursorFlag -{ - pub flag: bool, - pub pending_clear: bool, -} - -impl CursorFlag -{ - pub fn clear(&mut self) - { - self.flag = false; - self.pending_clear = false; - } +declare_entity! { +pub PHASE: (Phase, pair!(ChildOf, { *WINDOWING_PHASE })); } /// Input extension. #[derive(Debug, Default)] pub struct Extension {} -impl ecs::extension::Extension for Extension +impl crate::ecs::extension::Extension for Extension { fn collect(self, mut collector: ExtensionCollector<'_>) { - collector.add_system(StartEvent, initialize); - collector.add_system(PreUpdateEvent, maybe_clear_cursor_is_first_move); - collector.add_system(PostPresentEvent, set_keys_prev_tick_state); + collector.spawn_declared_entity(&PHASE); - collector.add_sole(Keys::default()).ok(); - collector.add_sole(Cursor::default()).ok(); - collector.add_sole(CursorFlags::default()).ok(); + // TODO: Add input mapping } } - -fn initialize( - keys: Single<Keys>, - cursor: Single<Cursor>, - cursor_flags: Single<CursorFlags>, - window: Single<Window>, -) -{ - let keys_weak_ref = keys.to_weak_ref(); - - window.set_key_callback(move |key, _scancode, key_state, _modifiers| { - let keys_ref = keys_weak_ref.access().expect("No world"); - - let mut keys = keys_ref.to_single(); - - keys.set_key_state(key, key_state); - }); - - let cursor_weak_ref = cursor.to_weak_ref(); - - window.set_cursor_pos_callback(move |cursor_position| { - let cursor_ref = cursor_weak_ref.access().expect("No world"); - - let mut cursor = cursor_ref.to_single(); - - cursor.position = Vec2 { - x: cursor_position.x, - y: cursor_position.y, - }; - - cursor.has_moved = true; - }); - - let cursor_flags_weak_ref = cursor_flags.to_weak_ref(); - - window.set_focus_callback(move |is_focused| { - #[cfg(feature = "debug")] - tracing::trace!("Window is focused: {is_focused}"); - - let cursor_flags_ref = cursor_flags_weak_ref.access().expect("No world"); - - cursor_flags_ref.to_single().is_first_move.flag = is_focused; - }); -} - -fn maybe_clear_cursor_is_first_move( - cursor: Single<Cursor>, - mut cursor_flags: Single<CursorFlags>, -) -{ - if cursor_flags.is_first_move.pending_clear { - #[cfg(feature = "debug")] - tracing::trace!("Clearing is_first_move"); - - // This flag was set for the whole previous tick so it can be cleared now - cursor_flags.is_first_move.clear(); - - return; - } - - if cursor.has_moved && cursor_flags.is_first_move.flag { - #[cfg(feature = "debug")] - tracing::trace!("Setting flag to clear is_first_move next tick"); - - // Make this system clear is_first_move the next time it runs - cursor_flags.is_first_move.pending_clear = true; - } -} - -fn set_keys_prev_tick_state(mut keys: Single<Keys>) -{ - for key_data in keys.map.values_mut() { - key_data.prev_tick_state = key_data.state; - } -} - -#[derive(Debug)] -struct KeyData -{ - state: KeyState, - prev_tick_state: KeyState, -} diff --git a/engine/src/input/keyboard.rs b/engine/src/input/keyboard.rs new file mode 100644 index 0000000..d226df0 --- /dev/null +++ b/engine/src/input/keyboard.rs @@ -0,0 +1,6 @@ +mod reexports +{ + pub use crate::windowing::keyboard::{Key, KeyState, Keyboard}; +} + +pub use reexports::*; diff --git a/engine/src/input/mouse.rs b/engine/src/input/mouse.rs new file mode 100644 index 0000000..c10aaf2 --- /dev/null +++ b/engine/src/input/mouse.rs @@ -0,0 +1,6 @@ +mod reexports +{ + pub use crate::windowing::mouse::{Button, ButtonState, Buttons, Mouse}; +} + +pub use reexports::*; diff --git a/engine/src/lib.rs b/engine/src/lib.rs index abf26f5..36affff 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -1,61 +1,45 @@ #![deny(clippy::all, clippy::pedantic)] #![allow(clippy::needless_pass_by_value)] -use ecs::component::Sequence as ComponentSequence; -use ecs::event::component::TypeTransformComponentsToAddedEvents; -use ecs::event::{Event, Sequence as EventSequence}; -use ecs::extension::Extension; -use ecs::sole::Sole; -use ecs::system::{Into, System}; -use ecs::tuple::Reduce as TupleReduce; -use ecs::uid::Uid; -use ecs::{SoleAlreadyExistsError, World}; - -use crate::delta_time::{update as update_delta_time, DeltaTime, LastUpdate}; -use crate::event::{ - Conclude as ConcludeEvent, - PostPresent as PostPresentEvent, - PreUpdate as PreUpdateEvent, - Present as PresentEvent, - Update as UpdateEvent, -}; - -mod opengl; -mod shader_preprocessor; +use crate::asset::{Assets, Extension as AssetExtension}; +use crate::ecs::error::HandlerFn as ErrorHandlerFn; +use crate::ecs::extension::Extension; +use crate::ecs::World; + 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 event; pub mod file_format; +pub mod image; pub mod input; pub mod lighting; pub mod material; pub mod math; pub mod mesh; -pub mod performance; +pub mod model; pub mod projection; -pub mod renderer; -pub mod shader; +pub mod reflection; +pub mod rendering; +pub mod scene; +pub mod sky_box; pub mod texture; pub mod transform; -pub mod vertex; -pub mod window; +pub mod ui; +pub mod windowing; + +pub extern crate engine_ecs as ecs; -pub extern crate ecs; +pub use ecs::error::Error; pub(crate) use crate::data_types::matrix; pub use crate::data_types::{color, vector}; -type EventOrder = ( - PreUpdateEvent, - UpdateEvent, - PresentEvent, - PostPresentEvent, - ConcludeEvent, -); +const INITIAL_ASSET_CAPACITY: usize = 128; #[derive(Debug)] pub struct Engine @@ -71,53 +55,34 @@ impl Engine { let mut world = World::new(); - world.add_sole(DeltaTime::default()).ok(); + let mut assets = Assets::with_capacity(INITIAL_ASSET_CAPACITY); - world.register_system( - PreUpdateEvent, - update_delta_time - .into_system() - .initialize((LastUpdate::default(),)), - ); + crate::model::asset::add_importers(&mut assets); + crate::material::asset::add_importers(&mut assets); - Self { world } - } + crate::texture::initialize(&mut assets); - pub fn spawn<Comps>(&mut self, components: Comps) -> Uid - where - Comps: ComponentSequence + TupleReduce<TypeTransformComponentsToAddedEvents>, - Comps::Out: EventSequence, - { - self.world.create_entity(components) - } + world.add_extension(AssetExtension { assets }); - pub fn register_system<'this, SystemImpl>( - &'this mut self, - event: impl Event, - system: impl System<'this, SystemImpl>, - ) - { - self.world.register_system(event, system); + Self { world } } - /// Adds a globally shared singleton value. - /// - /// # Errors - /// Returns `Err` if this [`Sole`] has already been added. - pub fn add_sole(&mut self, sole: impl Sole) -> Result<(), SoleAlreadyExistsError> + pub fn with_error_handler(mut self, error_handler: ErrorHandlerFn) -> Self { - self.world.add_sole(sole) + self.world.set_err_handler(error_handler); + self } - pub fn add_extension(&mut self, extension: impl Extension) + pub fn with_extension(mut self, extension: impl Extension) -> Self { self.world.add_extension(extension); + self } /// Runs the event loop. - pub fn start(&self) + pub fn start(&mut self) { - self.world.event_loop::<EventOrder>(); + self.world.start_loop(); } } @@ -128,3 +93,10 @@ impl Default for Engine Self::new() } } + +#[macro_export] +macro_rules! error { + ($($tt: tt)*) => { + $crate::ecs::error!($($tt)*) + }; +} diff --git a/engine/src/lighting.rs b/engine/src/lighting.rs index 48adb0e..5314e58 100644 --- a/engine/src/lighting.rs +++ b/engine/src/lighting.rs @@ -1,16 +1,17 @@ -use ecs::{Component, Sole}; - -use crate::color::Color; +use crate::builder; +use crate::color::{Color, Rgb}; use crate::data_types::vector::Vec3; -use crate::util::builder; +use crate::ecs::Component; +use crate::reflection::Reflection; builder! { #[builder(name = PointLightBuilder, derives = (Debug, Clone))] -#[derive(Debug, Clone, Component)] +#[derive(Debug, Clone, Component, Reflection)] #[non_exhaustive] pub struct PointLight { - pub position: Vec3<f32>, + /// Position in local space. + pub local_position: Vec3<f32>, pub diffuse: Color<f32>, pub specular: Color<f32>, pub attenuation_params: AttenuationParams, @@ -31,9 +32,9 @@ impl Default for PointLight fn default() -> Self { Self { - position: Vec3::default(), - diffuse: Color { red: 0.5, green: 0.5, blue: 0.5 }, - specular: Color { red: 1.0, green: 1.0, blue: 1.0 }, + local_position: Vec3::default(), + diffuse: Color::Rgb(Rgb { r: 0.5, g: 0.5, b: 0.5 }), + specular: Color::Rgb(Rgb::<f32>::white()), attenuation_params: AttenuationParams::default(), } } @@ -48,7 +49,7 @@ impl Default for PointLightBuilder } /// Parameters for light [attenuation](https://en.wikipedia.org/wiki/Attenuation). -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Reflection)] pub struct AttenuationParams { pub constant: f32, @@ -58,7 +59,6 @@ pub struct AttenuationParams impl Default for AttenuationParams { - #[must_use] fn default() -> Self { Self { @@ -70,8 +70,8 @@ impl Default for AttenuationParams } builder! { -#[builder(name = DirectionalLightBuilder, derives = (Debug, Default, Clone))] -#[derive(Debug, Default, Clone, Component)] +#[builder(name = DirectionalLightBuilder, derives = (Debug, Clone))] +#[derive(Debug, Clone, Component, Reflection)] #[non_exhaustive] pub struct DirectionalLight { @@ -90,22 +90,58 @@ impl DirectionalLight } } +impl Default for DirectionalLight +{ + fn default() -> Self + { + Self::builder().build() + } +} + +impl Default for DirectionalLightBuilder +{ + fn default() -> Self + { + Self { + diffuse: Color::Rgb(Rgb { r: 0.5, g: 0.5, b: 0.5 }), + specular: Color::Rgb(Rgb::<f32>::white()), + direction: Vec3::LEFT, + } + } +} + builder! { -/// Global light properties. -#[builder(name = GlobalLightBuilder, derives = (Debug, Clone, Default))] -#[derive(Debug, Clone, Default, Sole)] +#[builder(name = EnvironmentalBuilder, derives = (Debug, Clone))] +#[derive(Debug, Clone, Component, Reflection)] #[non_exhaustive] -pub struct GlobalLight +pub struct Environmental { - pub ambient: Color<f32>, + pub ambient_color: Color<f32>, } } -impl GlobalLight +impl Environmental { - #[must_use] - pub fn builder() -> GlobalLightBuilder + pub fn builder() -> EnvironmentalBuilder + { + EnvironmentalBuilder::default() + } +} + +impl Default for Environmental +{ + fn default() -> Self { - GlobalLightBuilder::default() + Self::builder().build() + } +} + +impl Default for EnvironmentalBuilder +{ + fn default() -> Self + { + Self { + ambient_color: Color::Rgb(Rgb { r: 0.2, g: 0.2, b: 0.2 }), + } } } diff --git a/engine/src/material.rs b/engine/src/material.rs index aae6003..af982f4 100644 --- a/engine/src/material.rs +++ b/engine/src/material.rs @@ -1,35 +1,58 @@ -use ecs::Component; +use crate::asset::Handle as AssetHandle; +use crate::builder; +use crate::color::{Color, Rgb}; +use crate::ecs::Component; +use crate::reflection::Reflection; +use crate::texture::Texture; -use crate::color::Color; -use crate::data_types::dimens::Dimens; -use crate::texture::{Id as TextureId, Texture}; -use crate::util::builder; +pub mod asset; -#[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<AssetHandle<Texture>>, + pub diffuse_map: Option<AssetHandle<Texture>>, + pub specular_map: Option<AssetHandle<Texture>>, pub shininess: f32, } +impl Material +{ + pub fn builder() -> Builder + { + Builder::new() + } + + pub fn textures(&self) -> impl Iterator<Item = &AssetHandle<Texture>> + { + [&self.ambient_map, &self.diffuse_map, &self.specular_map] + .into_iter() + .flatten() + } +} + +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<AssetHandle<Texture>>, + diffuse_map: Option<AssetHandle<Texture>>, + specular_map: Option<AssetHandle<Texture>>, shininess: f32, } @@ -39,13 +62,12 @@ impl Builder pub fn new() -> Self { Self { - ambient: None, - diffuse: None, - specular: None, + ambient: Color::Rgb(Rgb::<f32>::white()), + diffuse: Color::Rgb(Rgb::<f32>::white()), + specular: Color::Rgb(Rgb::<f32>::white()), ambient_map: None, diffuse_map: None, specular_map: None, - textures: Vec::new(), shininess: 32.0, } } @@ -53,7 +75,7 @@ impl Builder #[must_use] pub fn ambient(mut self, ambient: Color<f32>) -> Self { - self.ambient = Some(ambient); + self.ambient = ambient; self } @@ -61,7 +83,7 @@ impl Builder #[must_use] pub fn diffuse(mut self, diffuse: Color<f32>) -> Self { - self.diffuse = Some(diffuse); + self.diffuse = diffuse; self } @@ -69,13 +91,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: AssetHandle<Texture>) -> Self { self.ambient_map = Some(ambient_map); @@ -83,7 +105,7 @@ impl Builder } #[must_use] - pub fn diffuse_map(mut self, diffuse_map: TextureId) -> Self + pub fn diffuse_map(mut self, diffuse_map: AssetHandle<Texture>) -> Self { self.diffuse_map = Some(diffuse_map); @@ -91,7 +113,7 @@ impl Builder } #[must_use] - pub fn specular_map(mut self, specular_map: TextureId) -> Self + pub fn specular_map(mut self, specular_map: AssetHandle<Texture>) -> Self { self.specular_map = Some(specular_map); @@ -99,22 +121,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; @@ -127,43 +133,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 const 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, } } @@ -179,8 +157,8 @@ impl Default for Builder builder! { /// Material flags. -#[builder(name = FlagsBuilder, derives = (Debug, Default, Clone))] -#[derive(Debug, Default, Clone, Component)] +#[builder(name = FlagsBuilder, derives = (Debug, Clone))] +#[derive(Debug, Clone, Component, Reflection)] #[non_exhaustive] pub struct Flags { @@ -193,13 +171,32 @@ pub struct Flags impl Flags { #[must_use] - pub fn builder() -> FlagsBuilder + pub const fn builder() -> FlagsBuilder { - FlagsBuilder::default() + FlagsBuilder::new() } } -fn create_1x1_white_texture() -> Texture +impl Default for Flags { - Texture::new_from_color(&Dimens { width: 1, height: 1 }, &Color::WHITE_U8) + fn default() -> Self + { + Self::builder().build() + } +} + +impl FlagsBuilder +{ + pub const fn new() -> Self + { + Self { use_ambient_color: false } + } +} + +impl Default for FlagsBuilder +{ + fn default() -> Self + { + Self::new() + } } diff --git a/engine/src/material/asset.rs b/engine/src/material/asset.rs new file mode 100644 index 0000000..f87b768 --- /dev/null +++ b/engine/src/material/asset.rs @@ -0,0 +1,82 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::fs::read_to_string; +use std::path::{Path, PathBuf}; + +use crate::asset::{Assets, Handle as AssetHandle, Submitter as AssetSubmitter}; +use crate::material::Material; + +#[derive(Debug, Clone)] +pub struct Map +{ + pub assets: HashMap<Cow<'static, str>, AssetHandle<Material>>, +} + +/// Material asset import settings. +#[derive(Debug)] +#[non_exhaustive] +pub struct Settings {} + +pub fn add_importers(assets: &mut Assets) +{ + assets.set_importer(["mtl"], import_wavefront_mtl_asset); +} + +fn import_wavefront_mtl_asset( + asset_submitter: &mut AssetSubmitter<'_>, + path: &Path, + _settings: Option<&'_ Settings>, +) -> Result<(), Error> +{ + let parent_path = path + .parent() + .ok_or_else(|| Error::InvalidPath(path.to_path_buf()))?; + + let named_materials = crate::file_format::wavefront::mtl::parse( + &read_to_string(path) + .map_err(|err| Error::ReadFailed(err, path.to_path_buf()))?, + )?; + + for material in named_materials { + let mut material_builder = Material::builder() + .ambient(material.ambient) + .diffuse(material.diffuse) + .specular(material.specular) + .shininess(material.shininess); + + if let Some(ambient_map) = material.ambient_map { + material_builder = material_builder.ambient_map( + asset_submitter.submit_load_other(parent_path.join(&ambient_map.path)), + ); + } + + if let Some(diffuse_map) = material.diffuse_map { + material_builder = material_builder.diffuse_map( + asset_submitter.submit_load_other(parent_path.join(&diffuse_map.path)), + ); + } + + if let Some(specular_map) = material.specular_map { + material_builder = material_builder.specular_map( + asset_submitter.submit_load_other(parent_path.join(&specular_map.path)), + ); + } + + asset_submitter.submit_store_named(material.name, material_builder.build()); + } + + Ok(()) +} + +#[derive(Debug, thiserror::Error)] +enum Error +{ + #[error("Invalid path '{}'", .0.display())] + InvalidPath(PathBuf), + + #[error("Failed to read file {}", .1.display())] + ReadFailed(#[source] std::io::Error, PathBuf), + + #[error(transparent)] + Other(#[from] crate::file_format::wavefront::mtl::Error), +} diff --git a/engine/src/math.rs b/engine/src/math.rs index b86e760..eeed61e 100644 --- a/engine/src/math.rs +++ b/engine/src/math.rs @@ -5,13 +5,13 @@ use crate::vector::Vec3; /// Calculates the surface normal of a triangle. #[must_use] pub fn calc_triangle_surface_normal( - egde_a: &Vec3<f32>, - edge_b: &Vec3<f32>, - edge_c: &Vec3<f32>, + egde_a: Vec3<f32>, + edge_b: Vec3<f32>, + edge_c: Vec3<f32>, ) -> Vec3<f32> { let v1 = edge_b - egde_a; let v2 = edge_c - egde_a; - v1.cross(&v2) + v1.cross(&v2).normalize() } diff --git a/engine/src/mesh.rs b/engine/src/mesh.rs index de0af70..c1c0f77 100644 --- a/engine/src/mesh.rs +++ b/engine/src/mesh.rs @@ -1,28 +1,253 @@ -use ecs::Component; +use std::alloc::Layout; -use crate::vertex::Vertex; +use zerocopy::IntoBytes; -pub mod cube; +use crate::data_types::dimens::Dimens3; +use crate::mesh::vertex_buffer::{VertexAttrProperties, VertexLabel}; +use crate::vector::{Vec2, Vec3}; -#[derive(Debug, Clone, Component)] +pub mod vertex_buffer; + +#[derive(Debug, Clone)] pub struct Mesh { - vertices: Vec<Vertex>, + vertex_buf: vertex_buffer::VertexBuffer, indices: Option<Vec<u32>>, } impl Mesh { - #[must_use] - pub fn new(vertices: Vec<Vertex>, indices: Option<Vec<u32>>) -> Self + pub fn cube(size: Dimens3<f32>) -> Self { - Self { vertices, indices } + Self::cube_with_options(CubeOptions::default().size(size)) } - #[must_use] - pub fn vertices(&self) -> &[Vertex] + pub fn cube_with_options(options: CubeOptions) -> Self + { + let CubeOptions { + size, + top_face: + CubeFaceOptions { + uv_upper_left: top_uv_upper_left, + uv_lower_right: top_uv_lower_right, + }, + bottom_face: + CubeFaceOptions { + uv_upper_left: bot_uv_upper_left, + uv_lower_right: bot_uv_lower_right, + }, + left_face: + CubeFaceOptions { + uv_upper_left: left_uv_upper_left, + uv_lower_right: left_uv_lower_right, + }, + right_face: + CubeFaceOptions { + uv_upper_left: right_uv_upper_left, + uv_lower_right: right_uv_lower_right, + }, + back_face: + CubeFaceOptions { + uv_upper_left: back_uv_upper_left, + uv_lower_right: back_uv_lower_right, + }, + front_face: + CubeFaceOptions { + uv_upper_left: front_uv_upper_left, + uv_lower_right: front_uv_lower_right, + }, + } = options; + + let half_w = size.width / 2.0; + let half_h = size.height / 2.0; + let half_d = size.depth / 2.0; + + #[inline(always)] + fn vertex(pos: [f32; 3], uv: [f32; 2], normal: Vec3<f32>) -> [f32; 8] + { + let [pos_x, pos_y, pos_z] = pos; + let [uv_x, uv_y] = uv; + + [ + pos_x, pos_y, pos_z, uv_x, uv_y, normal.x, normal.y, normal.z, + ] + } + + let top_vertices = [ + ( + [half_w, half_h, half_d], + [top_uv_upper_left.x, top_uv_upper_left.y], + ), + ( + [-half_w, half_h, half_d], + [top_uv_lower_right.x, top_uv_upper_left.y], + ), + ( + [half_w, half_h, -half_d], + [top_uv_upper_left.x, top_uv_lower_right.y], + ), + ( + [-half_w, half_h, -half_d], + [top_uv_lower_right.x, top_uv_lower_right.y], + ), + ] + .map(|(pos, uv)| vertex(pos, uv, Vec3::UP)); + + let bottom_vertices = [ + ( + [half_w, -half_h, half_d], + [bot_uv_upper_left.x, bot_uv_upper_left.y], + ), + ( + [-half_w, -half_h, half_d], + [bot_uv_lower_right.x, bot_uv_upper_left.y], + ), + ( + [half_w, -half_h, -half_d], + [bot_uv_upper_left.x, bot_uv_lower_right.y], + ), + ( + [-half_w, -half_h, -half_d], + [bot_uv_lower_right.x, bot_uv_lower_right.y], + ), + ] + .map(|(pos, uv)| vertex(pos, uv, Vec3::DOWN)); + + let left_vertices = [ + ( + [-half_w, half_h, half_d], + [left_uv_upper_left.x, left_uv_upper_left.y], + ), + ( + [-half_w, half_h, -half_d], + [left_uv_lower_right.x, left_uv_upper_left.y], + ), + ( + [-half_w, -half_h, half_d], + [left_uv_upper_left.x, left_uv_lower_right.y], + ), + ( + [-half_w, -half_h, -half_d], + [left_uv_lower_right.x, left_uv_lower_right.y], + ), + ] + .map(|(pos, uv)| vertex(pos, uv, Vec3::LEFT)); + + let right_vertices = [ + ( + [half_w, half_h, half_d], + [right_uv_upper_left.x, right_uv_upper_left.y], + ), + ( + [half_w, half_h, -half_d], + [right_uv_lower_right.x, right_uv_upper_left.y], + ), + ( + [half_w, -half_h, half_d], + [right_uv_upper_left.x, right_uv_lower_right.y], + ), + ( + [half_w, -half_h, -half_d], + [right_uv_lower_right.x, right_uv_lower_right.y], + ), + ] + .map(|(pos, uv)| vertex(pos, uv, Vec3::RIGHT)); + + let back_vertices = [ + ( + [half_w, half_h, -half_d], + [back_uv_upper_left.x, back_uv_upper_left.y], + ), + ( + [-half_w, half_h, -half_d], + [back_uv_lower_right.x, back_uv_upper_left.y], + ), + ( + [half_w, -half_h, -half_d], + [back_uv_upper_left.x, back_uv_lower_right.y], + ), + ( + [-half_w, -half_h, -half_d], + [back_uv_lower_right.x, back_uv_lower_right.y], + ), + ] + .map(|(pos, uv)| vertex(pos, uv, Vec3::BACK)); + + let front_vertices = [ + ( + [half_w, half_h, half_d], + [front_uv_upper_left.x, front_uv_upper_left.y], + ), + ( + [-half_w, half_h, half_d], + [front_uv_lower_right.x, front_uv_upper_left.y], + ), + ( + [half_w, -half_h, half_d], + [front_uv_upper_left.x, front_uv_lower_right.y], + ), + ( + [-half_w, -half_h, half_d], + [front_uv_lower_right.x, front_uv_lower_right.y], + ), + ] + .map(|(pos, uv)| vertex(pos, uv, Vec3::FRONT)); + + Mesh::builder() + .vertices(unsafe { + vertex_buffer::VertexBuffer::from_raw( + &[ + VertexAttrProperties { + label: VertexLabel::Position, + ty: VertexAttrType::Float32Array { length: 3 }, + layout: Layout::new::<[f32; 3]>(), + byte_offset: 0, + }, + VertexAttrProperties { + label: VertexLabel::UvFromTopLeft, + ty: VertexAttrType::Float32Array { length: 2 }, + layout: Layout::new::<[f32; 2]>(), + byte_offset: size_of::<[f32; 3]>(), + }, + VertexAttrProperties { + label: VertexLabel::Normal, + ty: VertexAttrType::Float32Array { length: 3 }, + layout: Layout::new::<[f32; 3]>(), + byte_offset: size_of::<[f32; 3]>() + size_of::<[f32; 2]>(), + }, + ], + [ + top_vertices, + bottom_vertices, + left_vertices, + right_vertices, + back_vertices, + front_vertices, + ] + .as_bytes() + .to_vec(), + ) + }) + .indices([ + 0, 1, 2, 1, 3, 2, 4, 5, 6, 5, 7, 6, 8, 9, 10, 9, 11, 10, 12, 13, 14, 13, + 15, 14, 16, 17, 18, 17, 19, 18, 20, 21, 22, 21, 23, 22, + ]) + .build() + } + + pub fn builder() -> Builder + { + Builder::default() + } + + pub fn vertex_buf(&self) -> &vertex_buffer::VertexBuffer { - &self.vertices + &self.vertex_buf + } + + pub fn vertex_buf_mut(&mut self) -> &mut vertex_buffer::VertexBuffer + { + &mut self.vertex_buf } #[must_use] @@ -30,4 +255,311 @@ impl Mesh { self.indices.as_deref() } + + #[must_use] + pub fn indices_mut(&mut self) -> Option<&mut [u32]> + { + self.indices.as_deref_mut() + } + + pub fn set_indices(&mut self, indices: impl IntoIterator<Item = u32>) + { + let curr_indices = self.indices.get_or_insert_with(|| Vec::new()); + + curr_indices.clear(); + + curr_indices.extend(indices.into_iter()); + } + + /// Finds the vertex positions that are furthest in every 3D direction. Keep in mind + /// that this can be quite time-expensive if the mesh has many vertices. + pub fn find_furthest_vertex_positions(&self) -> DirectionPositions + { + let mut pos_iter = self + .vertex_buf() + .iter::<[f32; 3]>(VertexLabel::Position) + .map(|vertex_pos| Vec3::from(*vertex_pos)) + .into_iter(); + + let first_pos = pos_iter.next().unwrap(); + + pos_iter + .fold( + FurthestPosAcc { + up: FurthestPos::new(first_pos, &Vec3::UP), + down: FurthestPos::new(first_pos, &Vec3::DOWN), + left: FurthestPos::new(first_pos, &Vec3::LEFT), + right: FurthestPos::new(first_pos, &Vec3::RIGHT), + back: FurthestPos::new(first_pos, &Vec3::BACK), + front: FurthestPos::new(first_pos, &Vec3::FRONT), + }, + |mut furthest_pos_acc, pos| { + furthest_pos_acc.up.update_if_further(pos); + furthest_pos_acc.down.update_if_further(pos); + furthest_pos_acc.left.update_if_further(pos); + furthest_pos_acc.right.update_if_further(pos); + furthest_pos_acc.back.update_if_further(pos); + furthest_pos_acc.front.update_if_further(pos); + + furthest_pos_acc + }, + ) + .into() + } +} + +/// Mesh builder +#[derive(Debug, Clone, Default)] +pub struct Builder +{ + vertex_buf: vertex_buffer::VertexBuffer, + indices: Option<Vec<u32>>, +} + +impl Builder +{ + pub fn new() -> Self + { + Self::default() + } + + pub fn vertices(mut self, vertices_bytes: vertex_buffer::VertexBuffer) -> Self + { + self.vertex_buf = vertices_bytes; + self + } + + pub fn indices(mut self, indices: impl IntoIterator<Item = u32>) -> Self + { + self.indices = Some(indices.into_iter().collect()); + self + } + + pub fn build(self) -> Mesh + { + Mesh { + vertex_buf: self.vertex_buf, + indices: self.indices, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum VertexAttrType +{ + Float32, + Float32Array + { + length: usize, + }, +} + +impl VertexAttrType +{ + pub fn layout(&self) -> Layout + { + match self { + Self::Float32 => Layout::new::<f32>(), + Self::Float32Array { length } => Layout::array::<f32>(*length).unwrap(), + } + } +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct CubeOptions +{ + pub size: Dimens3<f32>, + pub top_face: CubeFaceOptions, + pub bottom_face: CubeFaceOptions, + pub front_face: CubeFaceOptions, + pub back_face: CubeFaceOptions, + pub left_face: CubeFaceOptions, + pub right_face: CubeFaceOptions, +} + +impl CubeOptions +{ + #[must_use] + pub fn size(mut self, size: Dimens3<f32>) -> Self + { + self.size = size; + self + } + + #[must_use] + pub fn top_face(mut self, top_face: CubeFaceOptions) -> Self + { + self.top_face = top_face; + self + } + + #[must_use] + pub fn bottom_face(mut self, bottom_face: CubeFaceOptions) -> Self + { + self.bottom_face = bottom_face; + self + } + + #[must_use] + pub fn front_face(mut self, front_face: CubeFaceOptions) -> Self + { + self.front_face = front_face; + self + } + + #[must_use] + pub fn back_face(mut self, back_face: CubeFaceOptions) -> Self + { + self.back_face = back_face; + self + } + + #[must_use] + pub fn left_face(mut self, left_face: CubeFaceOptions) -> Self + { + self.left_face = left_face; + self + } + + #[must_use] + pub fn right_face(mut self, right_face: CubeFaceOptions) -> Self + { + self.right_face = right_face; + self + } +} + +impl Default for CubeOptions +{ + fn default() -> Self + { + Self { + size: Dimens3 { width: 1.0, height: 1.0, depth: 1.0 }, + top_face: CubeFaceOptions::default(), + bottom_face: CubeFaceOptions::default(), + front_face: CubeFaceOptions::default(), + back_face: CubeFaceOptions::default(), + left_face: CubeFaceOptions::default(), + right_face: CubeFaceOptions::default(), + } + } +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct CubeFaceOptions +{ + /// Seeing the face head on, this is the texture coordinate for the corner that + /// would be on the upper left of the face. + /// + /// The default is `(0, 0)` + pub uv_upper_left: Vec2<f32>, + + /// Seeing the face head on, this is the texture coordinate for the corner that + /// would be on the lower right of the face. + /// + /// The default is `(1, 1)` + pub uv_lower_right: Vec2<f32>, +} + +impl CubeFaceOptions +{ + /// Seeing the face head on, this is the texture coordinate for the corner that + /// would be on the upper left of the face. + /// + /// The default is `(0, 0)` + pub fn uv_upper_left(mut self, uv_upper_left: Vec2<f32>) -> Self + { + self.uv_upper_left = uv_upper_left; + self + } + + /// Seeing the face head on, this is the texture coordinate for the corner that + /// would be on the lower right of the face. + /// + /// The default is `(1, 1)` + pub fn uv_lower_right(mut self, uv_lower_right: Vec2<f32>) -> Self + { + self.uv_lower_right = uv_lower_right; + self + } +} + +impl Default for CubeFaceOptions +{ + fn default() -> Self + { + Self { + uv_upper_left: Vec2 { x: 0.0, y: 0.0 }, + uv_lower_right: Vec2 { x: 1.0, y: 1.0 }, + } + } +} + +#[derive(Debug, Clone)] +pub struct DirectionPositions +{ + pub up: Vec3<f32>, + pub down: Vec3<f32>, + pub left: Vec3<f32>, + pub right: Vec3<f32>, + pub back: Vec3<f32>, + pub front: Vec3<f32>, +} + +impl<'mesh> From<FurthestPosAcc<'mesh>> for DirectionPositions +{ + fn from(acc: FurthestPosAcc<'mesh>) -> Self + { + Self { + up: acc.up.pos, + down: acc.down.pos, + left: acc.left.pos, + right: acc.right.pos, + back: acc.back.pos, + front: acc.front.pos, + } + } +} + +#[derive(Debug)] +struct FurthestPosAcc<'mesh> +{ + up: FurthestPos<'mesh>, + down: FurthestPos<'mesh>, + left: FurthestPos<'mesh>, + right: FurthestPos<'mesh>, + back: FurthestPos<'mesh>, + front: FurthestPos<'mesh>, +} + +#[derive(Debug)] +struct FurthestPos<'mesh> +{ + pos: Vec3<f32>, + dot_prod: f32, + direction: &'mesh Vec3<f32>, +} + +impl<'mesh> FurthestPos<'mesh> +{ + fn new(pos: Vec3<f32>, direction: &'mesh Vec3<f32>) -> Self + { + Self { + pos, + dot_prod: direction.dot(&pos), + direction, + } + } + + fn update_if_further(&mut self, point: Vec3<f32>) + { + let point_dot_prod = self.direction.dot(&point); + + if point_dot_prod > self.dot_prod { + self.pos = point; + self.dot_prod = point_dot_prod; + } + } } diff --git a/engine/src/mesh/cube.rs b/engine/src/mesh/cube.rs deleted file mode 100644 index 7cdf885..0000000 --- a/engine/src/mesh/cube.rs +++ /dev/null @@ -1,702 +0,0 @@ -use crate::math::calc_triangle_surface_normal; -use crate::mesh::Mesh; -use crate::util::builder; -use crate::vector::Vec3; -use crate::vertex::{Builder as VertexBuilder, Vertex}; - -builder! { -/// Cube mesh creation specification. -#[builder(name = CreationSpecBuilder, derives = (Debug, Default))] -#[derive(Debug, Default)] -#[non_exhaustive] -pub struct CreationSpec -{ - pub width: f32, - pub height: f32, - pub depth: f32, -} -} - -impl CreationSpec -{ - /// Returns a new `CreationSpec` builder. - #[must_use] - pub fn builder() -> CreationSpecBuilder - { - CreationSpecBuilder::default() - } -} - -#[derive(Debug)] -pub enum Side -{ - Front, - Back, - Left, - Right, - Top, - Bottom, -} - -#[derive(Debug)] -pub enum Corner -{ - TopRight, - TopLeft, - BottomRight, - BottomLeft, -} - -/// Creates a cube mesh. -pub fn create( - creation_spec: CreationSpec, - vertex_builder_cb: impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) -> Mesh -{ - let mut vertices = [const { None }; VertexIndex::VARIANT_CNT]; - - create_front(&creation_spec, &mut vertices, &vertex_builder_cb); - create_back(&creation_spec, &mut vertices, &vertex_builder_cb); - create_right(&creation_spec, &mut vertices, &vertex_builder_cb); - create_left(&creation_spec, &mut vertices, &vertex_builder_cb); - create_top(&creation_spec, &mut vertices, &vertex_builder_cb); - create_bottom(&creation_spec, &mut vertices, &vertex_builder_cb); - - Mesh::new( - vertices.map(Option::unwrap).to_vec(), - Some( - VERTEX_INDICES - .into_iter() - .flatten() - .map(|index| index as u32) - .collect(), - ), - ) -} - -macro_rules! one { - ($tt: tt) => { - 1 - }; -} - -macro_rules! enum_with_variant_cnt { - ( - $(#[$attr: meta])* - enum $name: ident { - $($variant: ident,)* - } - ) => { - $(#[$attr])* - enum $name { - $($variant,)* - } - - impl $name { - const VARIANT_CNT: usize = 0 $(+ one!($variant))*; - } - }; -} - -enum_with_variant_cnt! { -#[repr(u32)] -enum VertexIndex -{ - FrontTopRight, - FrontBottomRight, - FrontBottomLeft, - FrontTopLeft, - - BackTopRight, - BackBottomRight, - BackBottomLeft, - BackTopLeft, - - RightBackTop, - RightBackBottom, - RightFrontTop, - RightFrontBottom, - - LeftBackTop, - LeftBackBottom, - LeftFrontTop, - LeftFrontBottom, - - TopBackRight, - TopBackLeft, - TopFrontRight, - TopFrontLeft, - - BottomBackRight, - BottomBackLeft, - BottomFrontRight, - BottomFrontLeft, -} -} - -fn create_front( - creation_spec: &CreationSpec, - vertices: &mut [Option<Vertex>], - vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) -{ - let front_top_right_pos = Vec3 { - x: creation_spec.width / 2.0, - y: creation_spec.height / 2.0, - z: -(creation_spec.depth / 2.0), - }; - - let front_bottom_right_pos = Vec3 { - x: creation_spec.width / 2.0, - y: -(creation_spec.height / 2.0), - z: -(creation_spec.depth / 2.0), - }; - - let front_bottom_left_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: -(creation_spec.height / 2.0), - z: -(creation_spec.depth / 2.0), - }; - - let front_top_left_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: creation_spec.height / 2.0, - z: -(creation_spec.depth / 2.0), - }; - - let front_normal = calc_triangle_surface_normal( - &front_top_right_pos, - &front_bottom_right_pos, - &front_top_left_pos, - ); - - vertices[VertexIndex::FrontTopRight as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(front_top_right_pos) - .normal(front_normal), - Side::Front, - Corner::TopRight, - ) - .build(), - ); - - vertices[VertexIndex::FrontBottomRight as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(front_bottom_right_pos) - .normal(front_normal), - Side::Front, - Corner::BottomRight, - ) - .build(), - ); - - vertices[VertexIndex::FrontBottomLeft as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(front_bottom_left_pos) - .normal(front_normal), - Side::Front, - Corner::BottomLeft, - ) - .build(), - ); - - vertices[VertexIndex::FrontTopLeft as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(front_top_left_pos) - .normal(front_normal), - Side::Front, - Corner::TopLeft, - ) - .build(), - ); -} - -fn create_back( - creation_spec: &CreationSpec, - vertices: &mut [Option<Vertex>], - vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) -{ - let back_top_right_pos = Vec3 { - x: creation_spec.width / 2.0, - y: creation_spec.height / 2.0, - z: creation_spec.depth / 2.0, - }; - - let back_bottom_right_pos = Vec3 { - x: creation_spec.width / 2.0, - y: -(creation_spec.height / 2.0), - z: creation_spec.depth / 2.0, - }; - - let back_bottom_left_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: -(creation_spec.height / 2.0), - z: creation_spec.depth / 2.0, - }; - - let back_top_left_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: creation_spec.height / 2.0, - z: creation_spec.depth / 2.0, - }; - - let back_normal = -calc_triangle_surface_normal( - &back_top_right_pos, - &back_bottom_right_pos, - &back_top_left_pos, - ); - - vertices[VertexIndex::BackTopRight as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(back_top_right_pos) - .normal(back_normal), - Side::Back, - Corner::TopRight, - ) - .build(), - ); - - vertices[VertexIndex::BackBottomRight as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(back_bottom_right_pos) - .normal(back_normal), - Side::Back, - Corner::BottomRight, - ) - .build(), - ); - - vertices[VertexIndex::BackBottomLeft as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(back_bottom_left_pos) - .normal(back_normal), - Side::Back, - Corner::BottomLeft, - ) - .build(), - ); - - vertices[VertexIndex::BackTopLeft as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(back_top_left_pos) - .normal(back_normal), - Side::Back, - Corner::TopLeft, - ) - .build(), - ); -} - -fn create_right( - creation_spec: &CreationSpec, - vertices: &mut [Option<Vertex>], - vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) -{ - let right_back_top_pos = Vec3 { - x: creation_spec.width / 2.0, - y: creation_spec.height / 2.0, - z: creation_spec.depth / 2.0, - }; - - let right_back_bottom_pos = Vec3 { - x: creation_spec.width / 2.0, - y: -(creation_spec.height / 2.0), - z: creation_spec.depth / 2.0, - }; - - let right_front_top_pos = Vec3 { - x: creation_spec.width / 2.0, - y: creation_spec.height / 2.0, - z: -(creation_spec.depth / 2.0), - }; - - let right_front_bottom_pos = Vec3 { - x: creation_spec.width / 2.0, - y: -(creation_spec.height / 2.0), - z: -(creation_spec.depth / 2.0), - }; - - let right_normal = calc_triangle_surface_normal( - &right_back_top_pos, - &right_back_bottom_pos, - &right_front_top_pos, - ); - - vertices[VertexIndex::RightBackTop as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(right_back_top_pos) - .normal(right_normal), - Side::Right, - Corner::TopLeft, - ) - .build(), - ); - - vertices[VertexIndex::RightBackBottom as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(right_back_bottom_pos) - .normal(right_normal), - Side::Right, - Corner::BottomLeft, - ) - .build(), - ); - - vertices[VertexIndex::RightFrontTop as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(right_front_top_pos) - .normal(right_normal), - Side::Right, - Corner::TopRight, - ) - .build(), - ); - - vertices[VertexIndex::RightFrontBottom as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(right_front_bottom_pos) - .normal(right_normal), - Side::Right, - Corner::BottomRight, - ) - .build(), - ); -} - -fn create_left( - creation_spec: &CreationSpec, - vertices: &mut [Option<Vertex>], - vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) -{ - let left_back_top_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: creation_spec.height / 2.0, - z: creation_spec.depth / 2.0, - }; - - let left_back_bottom_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: -(creation_spec.height / 2.0), - z: creation_spec.depth / 2.0, - }; - - let left_front_top_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: creation_spec.height / 2.0, - z: -(creation_spec.depth / 2.0), - }; - - let left_front_bottom_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: -(creation_spec.height / 2.0), - z: -(creation_spec.depth / 2.0), - }; - - let left_normal = -calc_triangle_surface_normal( - &left_back_top_pos, - &left_back_bottom_pos, - &left_front_top_pos, - ); - - vertices[VertexIndex::LeftBackTop as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(left_back_top_pos) - .normal(left_normal), - Side::Left, - Corner::TopRight, - ) - .build(), - ); - - vertices[VertexIndex::LeftBackBottom as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(left_back_bottom_pos) - .normal(left_normal), - Side::Left, - Corner::BottomRight, - ) - .build(), - ); - - vertices[VertexIndex::LeftFrontTop as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(left_front_top_pos) - .normal(left_normal), - Side::Left, - Corner::TopLeft, - ) - .build(), - ); - - vertices[VertexIndex::LeftFrontBottom as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(left_front_bottom_pos) - .normal(left_normal), - Side::Left, - Corner::BottomLeft, - ) - .build(), - ); -} - -fn create_top( - creation_spec: &CreationSpec, - vertices: &mut [Option<Vertex>], - vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) -{ - let top_back_right_pos = Vec3 { - x: creation_spec.width / 2.0, - y: creation_spec.height / 2.0, - z: creation_spec.depth / 2.0, - }; - - let top_back_left_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: creation_spec.height / 2.0, - z: creation_spec.depth / 2.0, - }; - - let top_front_left_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: creation_spec.height / 2.0, - z: -(creation_spec.depth / 2.0), - }; - - let top_front_right_pos = Vec3 { - x: creation_spec.width / 2.0, - y: creation_spec.height / 2.0, - z: -(creation_spec.depth / 2.0), - }; - - let top_normal = -calc_triangle_surface_normal( - &top_back_right_pos, - &top_back_left_pos, - &top_front_right_pos, - ); - - vertices[VertexIndex::TopBackRight as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(top_back_right_pos) - .normal(top_normal), - Side::Top, - Corner::TopRight, - ) - .build(), - ); - - vertices[VertexIndex::TopBackLeft as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(top_back_left_pos) - .normal(top_normal), - Side::Top, - Corner::TopLeft, - ) - .build(), - ); - - vertices[VertexIndex::TopFrontLeft as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(top_front_left_pos) - .normal(top_normal), - Side::Top, - Corner::BottomLeft, - ) - .build(), - ); - - vertices[VertexIndex::TopFrontRight as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(top_front_right_pos) - .normal(top_normal), - Side::Top, - Corner::BottomRight, - ) - .build(), - ); -} - -fn create_bottom( - creation_spec: &CreationSpec, - vertices: &mut [Option<Vertex>], - vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) -{ - let bottom_back_right_pos = Vec3 { - x: creation_spec.width / 2.0, - y: -(creation_spec.height / 2.0), - z: (creation_spec.depth / 2.0), - }; - - let bottom_back_left_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: -(creation_spec.height / 2.0), - z: creation_spec.depth / 2.0, - }; - - let bottom_front_right_pos = Vec3 { - x: creation_spec.width / 2.0, - y: -(creation_spec.height / 2.0), - z: -(creation_spec.depth / 2.0), - }; - - let bottom_front_left_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: -(creation_spec.height / 2.0), - z: -(creation_spec.depth / 2.0), - }; - - let bottom_normal = calc_triangle_surface_normal( - &bottom_back_right_pos, - &bottom_back_left_pos, - &bottom_front_right_pos, - ); - - vertices[VertexIndex::BottomBackRight as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(bottom_back_right_pos) - .normal(bottom_normal), - Side::Bottom, - Corner::BottomRight, - ) - .build(), - ); - - vertices[VertexIndex::BottomBackLeft as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(bottom_back_left_pos) - .normal(bottom_normal), - Side::Bottom, - Corner::BottomLeft, - ) - .build(), - ); - - vertices[VertexIndex::BottomFrontRight as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(bottom_front_right_pos) - .normal(bottom_normal), - Side::Bottom, - Corner::TopRight, - ) - .build(), - ); - - vertices[VertexIndex::BottomFrontLeft as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(bottom_front_left_pos) - .normal(bottom_normal), - Side::Bottom, - Corner::TopLeft, - ) - .build(), - ); -} - -const VERTEX_INDICES_FRONT: [VertexIndex; 6] = [ - // 🮝 - VertexIndex::FrontTopRight, - VertexIndex::FrontBottomRight, - VertexIndex::FrontTopLeft, - // - // 🮟 - VertexIndex::FrontBottomRight, - VertexIndex::FrontBottomLeft, - VertexIndex::FrontTopLeft, -]; - -const VERTEX_INDICES_BACK: [VertexIndex; 6] = [ - // 🮝 - VertexIndex::BackTopRight, - VertexIndex::BackBottomRight, - VertexIndex::BackTopLeft, - // - // 🮟 - VertexIndex::BackBottomRight, - VertexIndex::BackBottomLeft, - VertexIndex::BackTopLeft, -]; - -const VERTEX_INDICES_RIGHT: [VertexIndex; 6] = [ - // 🮝 - VertexIndex::RightBackTop, - VertexIndex::RightBackBottom, - VertexIndex::RightFrontTop, - // - // 🮟 - VertexIndex::RightBackBottom, - VertexIndex::RightFrontBottom, - VertexIndex::RightFrontTop, -]; - -const VERTEX_INDICES_LEFT: [VertexIndex; 6] = [ - // 🮝 - VertexIndex::LeftBackTop, - VertexIndex::LeftBackBottom, - VertexIndex::LeftFrontTop, - // - // 🮟 - VertexIndex::LeftBackBottom, - VertexIndex::LeftFrontBottom, - VertexIndex::LeftFrontTop, -]; - -const VERTEX_INDICES_TOP: [VertexIndex; 6] = [ - // 🮝 - VertexIndex::TopBackRight, - VertexIndex::TopBackLeft, - VertexIndex::TopFrontRight, - // - // 🮟 - VertexIndex::TopBackLeft, - VertexIndex::TopFrontLeft, - VertexIndex::TopFrontRight, -]; - -const VERTEX_INDICES_BOTTOM: [VertexIndex; 6] = [ - // 🮝 - VertexIndex::BottomBackRight, - VertexIndex::BottomBackLeft, - VertexIndex::BottomFrontRight, - // - // 🮟 - VertexIndex::BottomBackLeft, - VertexIndex::BottomFrontLeft, - VertexIndex::BottomFrontRight, -]; - -const VERTEX_INDICES: [[VertexIndex; 6]; 6] = [ - VERTEX_INDICES_FRONT, - VERTEX_INDICES_BACK, - VERTEX_INDICES_RIGHT, - VERTEX_INDICES_LEFT, - VERTEX_INDICES_TOP, - VERTEX_INDICES_BOTTOM, -]; diff --git a/engine/src/mesh/vertex_buffer.rs b/engine/src/mesh/vertex_buffer.rs new file mode 100644 index 0000000..5c03b45 --- /dev/null +++ b/engine/src/mesh/vertex_buffer.rs @@ -0,0 +1,281 @@ +use std::alloc::Layout; +use std::borrow::Cow; +use std::marker::PhantomData; + +use seq_macro::seq; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +use crate::mesh::VertexAttrType; + +pub trait VertexAttrValue: + IntoBytes + FromBytes + KnownLayout + Immutable + 'static +{ + fn ty() -> VertexAttrType; +} + +impl VertexAttrValue for f32 +{ + fn ty() -> VertexAttrType + { + VertexAttrType::Float32 + } +} + +impl<const LEN: usize> VertexAttrValue for [f32; LEN] +{ + fn ty() -> VertexAttrType + { + VertexAttrType::Float32Array { length: LEN } + } +} + +#[derive(Debug)] +pub struct NamedVertexAttr<Value: VertexAttrValue> +{ + pub label: VertexLabel, + pub value: Value, +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct VertexAttrProperties +{ + pub label: VertexLabel, + pub ty: VertexAttrType, + pub layout: Layout, + pub byte_offset: usize, +} + +#[derive(Debug)] +pub struct VertexAttrInfo +{ + pub label: VertexLabel, + pub ty: VertexAttrType, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum VertexLabel +{ + Position, + Normal, + + /// Texture coordinate with (0, 0) being at the top left. + UvFromTopLeft, + Color, + Other(Cow<'static, str>), +} + +#[derive(Debug, Clone, Default)] +pub struct VertexBuffer +{ + buf: Vec<u8>, + vertex_size: usize, + vertex_attr_props: Vec<VertexAttrProperties>, +} + +impl VertexBuffer +{ + pub unsafe fn from_raw( + vertex_attr_props: &[VertexAttrProperties], + buffer: Vec<u8>, + ) -> Self + { + let last_vertex_attr_props = vertex_attr_props + .iter() + .max_by_key(|props| props.byte_offset) + .expect("No vertex attribute properties are given"); + + let vertex_size = + last_vertex_attr_props.byte_offset + last_vertex_attr_props.layout.size(); + + Self { + buf: buffer, + vertex_size, + vertex_attr_props: vertex_attr_props.to_vec(), + } + } + + pub fn with_capacity(vertex_attrs: &[VertexAttrInfo], capacity: usize) -> Self + { + let mut vertex_attr_props = vertex_attrs + .iter() + .map(|VertexAttrInfo { label, ty }| VertexAttrProperties { + label: label.clone(), + ty: ty.clone(), + layout: ty.layout(), + byte_offset: 0, + }) + .collect::<Vec<_>>(); + + let mut vertex_layout = Layout::new::<()>(); + + for VertexAttrProperties { + layout: vertex_attr_layout, + byte_offset: vertex_attr_byte_offset, + .. + } in &mut vertex_attr_props + { + let (new_struct_layout, byte_offset) = + vertex_layout.extend(*vertex_attr_layout).unwrap(); + + *vertex_attr_byte_offset = byte_offset; + vertex_layout = new_struct_layout; + } + + let vertex_layout = vertex_layout.pad_to_align(); + + Self { + buf: Vec::with_capacity(vertex_layout.size() * capacity), + vertex_size: vertex_layout.size(), + vertex_attr_props, + } + } + + pub fn push<VertexAttrs: NamedVertexAttrs>(&mut self, vertex: VertexAttrs) + { + assert_eq!( + self.vertex_attr_props.len(), + vertex.vertex_attr_cnt(), + "Vertex has incorrect amount of attributes" + ); + + if self.buf.spare_capacity_mut().len() < self.vertex_size { + self.buf.reserve_exact(self.vertex_size * (self.len() / 2)); + } + + let spare_capacity = self.buf.spare_capacity_mut(); + + let vertex_attrs = vertex.vertex_attrs(); + + for (vertex_label, vertex_attr_bytes, vertex_attr_ty) in vertex_attrs { + let vertex_attr_props = self + .vertex_attr_props + .iter() + .find(|vertex_attr_props| &vertex_attr_props.label == vertex_label) + .unwrap(); + + assert_eq!(vertex_attr_ty, vertex_attr_props.ty); + + let start_offset = vertex_attr_props.byte_offset; + + let end_offset = start_offset + vertex_attr_props.layout.size(); + + spare_capacity[start_offset..end_offset] + .write_copy_of_slice(vertex_attr_bytes); + } + + unsafe { + self.buf.set_len(self.buf.len() + self.vertex_size); + } + } + + pub fn vertex_attr_props(&self) -> &[VertexAttrProperties] + { + &self.vertex_attr_props + } + + pub fn len(&self) -> usize + { + assert_eq!(self.buf.len() % self.vertex_size, 0, "Invalid length"); + + self.buf.len() / self.vertex_size + } + + pub fn vertex_size(&self) -> usize + { + self.vertex_size + } + + pub fn as_bytes(&self) -> &[u8] + { + &self.buf + } + + pub fn clear(&mut self) + { + self.buf.clear(); + } + + pub fn iter<VertexAttr: VertexAttrValue>( + &self, + vertex_label: VertexLabel, + ) -> Iter<'_, VertexAttr> + { + let vertex_attr_props = self + .vertex_attr_props + .iter() + .find(|vertex_attr_props| vertex_attr_props.label == vertex_label) + .unwrap(); + + assert_eq!(VertexAttr::ty(), vertex_attr_props.ty); + + Iter { + buf: self, + vertex_attr_props, + curr_index: 0, + _pd: PhantomData, + } + } +} + +pub struct Iter<'a, VertexAttr: VertexAttrValue> +{ + buf: &'a VertexBuffer, + vertex_attr_props: &'a VertexAttrProperties, + curr_index: usize, + _pd: PhantomData<VertexAttr>, +} + +impl<'a, VertexAttr: VertexAttrValue> Iterator for Iter<'a, VertexAttr> +{ + type Item = &'a VertexAttr; + + fn next(&mut self) -> Option<Self::Item> + { + let start_offset = + (self.buf.vertex_size * self.curr_index) + self.vertex_attr_props.byte_offset; + + let end_offset = start_offset + self.vertex_attr_props.layout.size(); + + let bytes = self.buf.buf.get(start_offset..end_offset)?; + + self.curr_index += 1; + + Some(VertexAttr::ref_from_bytes(bytes).unwrap()) + } +} + +pub trait NamedVertexAttrs +{ + fn vertex_attr_cnt(&self) -> usize; + + fn vertex_attrs(&self) + -> impl Iterator<Item = (&VertexLabel, &[u8], VertexAttrType)>; +} + +macro_rules! impl_named_vertex_attrs { + ($cnt: tt) => { + seq!(I in 0..$cnt { + impl<#(VertexAttr~I: VertexAttrValue,)*> + NamedVertexAttrs for (#(NamedVertexAttr<VertexAttr~I>,)*) + { + fn vertex_attr_cnt(&self) -> usize + { + $cnt + } + + fn vertex_attrs(&self) + -> impl Iterator<Item = (&VertexLabel, &[u8], VertexAttrType)> + { + [#( + (&self.I.label, self.I.value.as_bytes(), VertexAttr~I::ty()), + )*].into_iter() + } + } + }); + }; +} + +seq!(I in 0..16 { + impl_named_vertex_attrs!(I); +}); diff --git a/engine/src/model.rs b/engine/src/model.rs new file mode 100644 index 0000000..1269afd --- /dev/null +++ b/engine/src/model.rs @@ -0,0 +1,112 @@ +use crate::asset::{Assets, Handle as AssetHandle}; +use crate::ecs::Component; +use crate::material::Material; +use crate::mesh::Mesh; + +pub mod asset; + +#[derive(Debug, Clone, Component)] +#[non_exhaustive] +pub struct Model +{ + pub spec_asset: AssetHandle<Spec>, +} + +impl Model +{ + pub fn new(asset_handle: AssetHandle<Spec>) -> Self + { + Self { spec_asset: asset_handle } + } +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct Spec +{ + pub mesh_asset: Option<AssetHandle<Mesh>>, + pub materials: Vec<MaterialDescription>, +} + +impl Spec +{ + pub fn builder() -> SpecBuilder + { + SpecBuilder::default() + } + + pub fn find_first_material<'assets>( + &'assets self, + assets: &'assets Assets, + ) -> MaterialSearchResult<'assets> + { + let Some(material_desc) = self.materials.first() else { + return MaterialSearchResult::NoMaterials; + }; + + if assets.get(&material_desc.asset).is_none() { + tracing::trace!("Missing material asset"); + return MaterialSearchResult::NotFound; + } + + MaterialSearchResult::Found(&material_desc.asset) + } +} + +#[derive(Debug, Default, Clone)] +pub struct SpecBuilder +{ + mesh_asset: Option<AssetHandle<Mesh>>, + materials: Vec<MaterialDescription>, +} + +impl SpecBuilder +{ + pub fn mesh(mut self, asset: AssetHandle<Mesh>) -> Self + { + self.mesh_asset = Some(asset); + + self + } + + pub fn materials( + mut self, + materials: impl IntoIterator<Item = MaterialDescription>, + ) -> Self + { + self.materials = materials.into_iter().collect(); + + self + } + + #[tracing::instrument(skip_all)] + pub fn build(self) -> Spec + { + Spec { + mesh_asset: self.mesh_asset, + materials: self.materials, + } + } +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct MaterialDescription +{ + pub asset: AssetHandle<Material>, +} + +impl MaterialDescription +{ + pub fn new(asset: AssetHandle<Material>) -> Self + { + Self { asset } + } +} + +pub enum MaterialSearchResult<'a> +{ + Found(&'a AssetHandle<Material>), + NotFound, + NoMaterials, +} diff --git a/engine/src/model/asset.rs b/engine/src/model/asset.rs new file mode 100644 index 0000000..52a6733 --- /dev/null +++ b/engine/src/model/asset.rs @@ -0,0 +1,120 @@ +use std::fs::read_to_string; +use std::path::{Path, PathBuf}; + +use ecs::util::Either; + +use crate::asset::{Assets, Label as AssetLabel, Submitter as AssetSubmitter}; +use crate::material::Material; +use crate::model::{MaterialDescription, Spec}; + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct Settings +{ + /// Whether all UVs should be flipped on the Y axis. This is necessary if the UVs + /// have their origin at the bottom left corner of the image + /// + /// The default is `true`. + pub y_flip_uvs: bool, +} + +impl Settings +{ + /// Whether all UVs should be flipped on the Y axis. This is necessary if the UVs have + /// their origin at the bottom left corner of the image + /// + /// The default is `true`. + pub fn y_flip_uvs(mut self, y_flip_uvs: bool) -> Self + { + self.y_flip_uvs = y_flip_uvs; + self + } +} + +impl Default for Settings +{ + fn default() -> Self + { + Self { y_flip_uvs: true } + } +} + +pub fn add_importers(assets: &mut Assets) +{ + assets.set_importer(["obj"], import_wavefront_obj_asset); +} + +fn import_wavefront_obj_asset( + asset_submitter: &mut AssetSubmitter<'_>, + path: &Path, + settings: Option<&Settings>, +) -> Result<(), Error> +{ + let settings = match settings { + Some(settings) => settings, + None => &Settings::default(), + }; + + let parent_path = path + .parent() + .ok_or_else(|| Error::InvalidPath(path.to_path_buf()))?; + + let obj = crate::file_format::wavefront::obj::parse( + &read_to_string(path) + .map_err(|err| Error::ReadFailed(err, path.to_path_buf()))?, + )?; + + let mesh = obj.to_mesh( + crate::file_format::wavefront::obj::ToMeshOptions::default() + .y_flip_uvs(settings.y_flip_uvs), + )?; + + let mesh_asset = asset_submitter.submit_store_named("mesh", mesh); + + if obj.mtl_libs.len() > 1 { + return Err(Error::MoreThanOneMaterialLibrary); + } + + asset_submitter.submit_store( + Spec::builder() + .mesh(mesh_asset) + .materials(if obj.mtl_libs.is_empty() { + Either::A([].into_iter()) + } else { + Either::B( + obj.unique_used_material_names + .iter() + .zip(std::iter::repeat(obj.mtl_libs.iter()).flatten()) + .map(|(material_name, mtl_lib)| { + MaterialDescription::new( + asset_submitter.submit_load_other::<Material>( + AssetLabel { + path: parent_path.join(mtl_lib).into(), + name: Some(material_name.as_ref().into()), + }, + ), + ) + }), + ) + }) + .build(), + ); + + Ok(()) +} + +#[derive(Debug, thiserror::Error)] +enum Error +{ + #[error("Invalid path '{}'", .0.display())] + InvalidPath(PathBuf), + + #[error("Failed to read file {}", .1.display())] + ReadFailed(#[source] std::io::Error, PathBuf), + + #[error("More than one material library is specified. This is not supported")] + MoreThanOneMaterialLibrary, + + #[error(transparent)] + Other(#[from] crate::file_format::wavefront::obj::Error), +} diff --git a/engine/src/opengl/buffer.rs b/engine/src/opengl/buffer.rs deleted file mode 100644 index 2be7f12..0000000 --- a/engine/src/opengl/buffer.rs +++ /dev/null @@ -1,92 +0,0 @@ -use std::marker::PhantomData; -use std::mem::size_of_val; - -#[derive(Debug)] -pub struct Buffer<Item> -{ - buf: gl::types::GLuint, - _pd: PhantomData<Item>, -} - -impl<Item> Buffer<Item> -{ - pub fn new() -> Self - { - let mut buffer = gl::types::GLuint::default(); - - unsafe { - gl::CreateBuffers(1, &mut buffer); - }; - - Self { buf: buffer, _pd: PhantomData } - } - - /// Stores items in the currently bound buffer. - pub fn store(&mut self, items: &[Item], usage: Usage) - { - unsafe { - #[allow(clippy::cast_possible_wrap)] - gl::NamedBufferData( - self.buf, - size_of_val(items) as gl::types::GLsizeiptr, - items.as_ptr().cast(), - usage.into_gl(), - ); - } - } - - pub fn object(&self) -> gl::types::GLuint - { - self.buf - } - - /// Does a weak clone of this buffer. The buffer itself is NOT copied in any way this - /// function only copies the internal buffer ID. - /// - /// # Safety - /// The returned `Buffer` must not be dropped if another `Buffer` referencing the - /// same buffer ID is used later or if a [`VertexArray`] is used later. - /// - /// [`VertexArray`]: crate::opengl::vertex_array::VertexArray - pub unsafe fn clone_weak(&self) -> Self - { - Self { buf: self.buf, _pd: PhantomData } - } -} - -impl<Item> Drop for Buffer<Item> -{ - fn drop(&mut self) - { - unsafe { - gl::DeleteBuffers(1, &self.buf); - } - } -} - -/// Buffer usage. -#[derive(Debug)] -#[allow(dead_code)] -pub enum Usage -{ - /// The buffer data is set only once and used by the GPU at most a few times. - Stream, - - /// The buffer data is set only once and used many times. - Static, - - /// The buffer data is changed a lot and used many times. - Dynamic, -} - -impl Usage -{ - fn into_gl(self) -> gl::types::GLenum - { - match self { - Self::Stream => gl::STREAM_DRAW, - Self::Static => gl::STATIC_DRAW, - Self::Dynamic => gl::DYNAMIC_DRAW, - } - } -} diff --git a/engine/src/opengl/debug.rs b/engine/src/opengl/debug.rs deleted file mode 100644 index 203590a..0000000 --- a/engine/src/opengl/debug.rs +++ /dev/null @@ -1,145 +0,0 @@ -use std::ffi::c_void; -use std::io::{stderr, Write}; -use std::panic::catch_unwind; -use std::ptr::null_mut; -use std::sync::Mutex; - -use crate::opengl::util::gl_enum; - -pub type MessageCallback = fn( - source: MessageSource, - ty: MessageType, - id: u32, - severity: MessageSeverity, - message: &str, -); - -pub fn enable_debug_output() -{ - unsafe { - gl::Enable(gl::DEBUG_OUTPUT); - gl::Enable(gl::DEBUG_OUTPUT_SYNCHRONOUS); - } -} - -pub fn set_debug_message_callback(cb: MessageCallback) -{ - *DEBUG_MESSAGE_CB.lock().unwrap() = Some(cb); - - unsafe { - gl::DebugMessageCallback(Some(debug_message_cb), null_mut()); - } -} - -pub fn set_debug_message_control( - source: Option<MessageSource>, - ty: Option<MessageType>, - severity: Option<MessageSeverity>, - ids: &[u32], - ids_action: MessageIdsAction, -) -{ - // Ids shouldn't realistically be large enough to cause a panic here - let ids_len: i32 = ids.len().try_into().unwrap(); - - unsafe { - gl::DebugMessageControl( - source.map_or(gl::DONT_CARE, |source| source as u32), - ty.map_or(gl::DONT_CARE, |ty| ty as u32), - severity.map_or(gl::DONT_CARE, |severity| severity as u32), - ids_len, - ids.as_ptr(), - ids_action as u8, - ); - } -} - -#[derive(Debug, Clone, Copy)] -#[allow(dead_code)] -pub enum MessageIdsAction -{ - Enable = 1, - Disable = 0, -} - -gl_enum! { -pub enum MessageSource -{ - Api = gl::DEBUG_SOURCE_API, - WindowSystem = gl::DEBUG_SOURCE_WINDOW_SYSTEM, - ShaderCompiler = gl::DEBUG_SOURCE_SHADER_COMPILER, - ThirdParty = gl::DEBUG_SOURCE_THIRD_PARTY, - Application = gl::DEBUG_SOURCE_APPLICATION, - Other = gl::DEBUG_SOURCE_OTHER, -} -} - -gl_enum! { -pub enum MessageType -{ - DeprecatedBehavior = gl::DEBUG_TYPE_DEPRECATED_BEHAVIOR, - Error = gl::DEBUG_TYPE_ERROR, - Marker = gl::DEBUG_TYPE_MARKER, - Other = gl::DEBUG_TYPE_OTHER, - Performance = gl::DEBUG_TYPE_PERFORMANCE, - PopGroup = gl::DEBUG_TYPE_POP_GROUP, - PushGroup = gl::DEBUG_TYPE_PUSH_GROUP, - Portability = gl::DEBUG_TYPE_PORTABILITY, - UndefinedBehavior = gl::DEBUG_TYPE_UNDEFINED_BEHAVIOR, -} -} - -gl_enum! { -pub enum MessageSeverity -{ - High = gl::DEBUG_SEVERITY_HIGH, - Medium = gl::DEBUG_SEVERITY_MEDIUM, - Low = gl::DEBUG_SEVERITY_LOW, - Notification = gl::DEBUG_SEVERITY_NOTIFICATION, -} -} - -static DEBUG_MESSAGE_CB: Mutex<Option<MessageCallback>> = Mutex::new(None); - -extern "system" fn debug_message_cb( - source: gl::types::GLenum, - ty: gl::types::GLenum, - id: gl::types::GLuint, - severity: gl::types::GLenum, - message_length: gl::types::GLsizei, - message: *const gl::types::GLchar, - _user_param: *mut c_void, -) -{ - // Unwinds are catched because unwinding from Rust code into foreign code is UB. - let res = catch_unwind(|| { - let cb_lock = DEBUG_MESSAGE_CB.lock().unwrap(); - - if let Some(cb) = *cb_lock { - let msg_source = MessageSource::from_gl(source).unwrap(); - let msg_type = MessageType::from_gl(ty).unwrap(); - let msg_severity = MessageSeverity::from_gl(severity).unwrap(); - - let msg_length = usize::try_from(message_length).unwrap(); - - // SAFETY: The received message should be a valid ASCII string - let message = unsafe { - std::str::from_utf8_unchecked(std::slice::from_raw_parts( - message.cast(), - msg_length, - )) - }; - - cb(msg_source, msg_type, id, msg_severity, message); - } - }); - - if res.is_err() { - // eprintln is not used since it can panic and unwinds are unwanted because - // unwinding from Rust code into foreign code is UB. - stderr() - .write_all(b"ERROR: Panic in debug message callback") - .ok(); - println!(); - } -} diff --git a/engine/src/opengl/mod.rs b/engine/src/opengl/mod.rs deleted file mode 100644 index 0b1bb8a..0000000 --- a/engine/src/opengl/mod.rs +++ /dev/null @@ -1,107 +0,0 @@ -use bitflags::bitflags; - -use crate::data_types::dimens::Dimens; -use crate::vector::Vec2; - -pub mod buffer; -pub mod shader; -pub mod texture; -pub mod vertex_array; - -mod util; - -#[cfg(feature = "debug")] -pub mod debug; - -pub fn set_viewport(position: Vec2<u32>, size: Dimens<u32>) -{ - unsafe { - #[allow(clippy::cast_possible_wrap)] - gl::Viewport( - position.x as i32, - position.y as i32, - size.width as i32, - size.height as i32, - ); - } -} - -pub fn clear_buffers(mask: BufferClearMask) -{ - unsafe { - gl::Clear(mask.bits()); - } -} - -pub fn set_polygon_mode(face: impl Into<PolygonModeFace>, mode: impl Into<PolygonMode>) -{ - unsafe { - gl::PolygonMode(face.into() as u32, mode.into() as u32); - } -} - -pub fn enable(capacity: Capability) -{ - unsafe { - gl::Enable(capacity as u32); - } -} - -bitflags! { - #[derive(Debug, Clone, Copy)] - pub struct BufferClearMask: u32 { - const COLOR = gl::COLOR_BUFFER_BIT; - const DEPTH = gl::DEPTH_BUFFER_BIT; - const STENCIL = gl::STENCIL_BUFFER_BIT; - } -} - -#[derive(Debug)] -#[repr(u32)] -pub enum Capability -{ - DepthTest = gl::DEPTH_TEST, - MultiSample = gl::MULTISAMPLE, -} - -#[derive(Debug)] -#[repr(u32)] -pub enum PolygonMode -{ - Point = gl::POINT, - Line = gl::LINE, - Fill = gl::FILL, -} - -impl From<crate::draw_flags::PolygonMode> for PolygonMode -{ - fn from(mode: crate::draw_flags::PolygonMode) -> Self - { - match mode { - crate::draw_flags::PolygonMode::Point => Self::Point, - crate::draw_flags::PolygonMode::Fill => Self::Fill, - crate::draw_flags::PolygonMode::Line => Self::Line, - } - } -} - -#[derive(Debug)] -#[repr(u32)] -pub enum PolygonModeFace -{ - Front = gl::FRONT, - Back = gl::BACK, - FrontAndBack = gl::FRONT_AND_BACK, -} - -impl From<crate::draw_flags::PolygonModeFace> for PolygonModeFace -{ - fn from(face: crate::draw_flags::PolygonModeFace) -> Self - { - match face { - crate::draw_flags::PolygonModeFace::Front => Self::Front, - crate::draw_flags::PolygonModeFace::Back => Self::Back, - crate::draw_flags::PolygonModeFace::FrontAndBack => Self::FrontAndBack, - } - } -} diff --git a/engine/src/opengl/shader.rs b/engine/src/opengl/shader.rs deleted file mode 100644 index 070897e..0000000 --- a/engine/src/opengl/shader.rs +++ /dev/null @@ -1,240 +0,0 @@ -use std::ffi::CStr; -use std::ptr::null_mut; - -use crate::matrix::Matrix; -use crate::shader::Kind; -use crate::vector::Vec3; - -#[derive(Debug)] -pub struct Shader -{ - shader: gl::types::GLuint, -} - -impl Shader -{ - pub fn new(kind: Kind) -> Self - { - let shader = unsafe { gl::CreateShader(kind.into_gl()) }; - - Self { shader } - } - - pub fn set_source(&self, source: &str) -> Result<(), Error> - { - if !source.is_ascii() { - return Err(Error::SourceNotAscii); - } - - unsafe { - #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] - gl::ShaderSource( - self.shader, - 1, - &source.as_ptr().cast(), - &(source.len() as gl::types::GLint), - ); - } - - Ok(()) - } - - pub fn compile(&self) -> Result<(), Error> - { - unsafe { - gl::CompileShader(self.shader); - } - - let mut compile_success = gl::types::GLint::default(); - - unsafe { - gl::GetShaderiv(self.shader, gl::COMPILE_STATUS, &mut compile_success); - } - - if compile_success == 0 { - let info_log = self.get_info_log(); - - return Err(Error::CompileFailed(info_log)); - } - - Ok(()) - } - - fn get_info_log(&self) -> String - { - let mut buf = vec![gl::types::GLchar::default(); 512]; - - unsafe { - #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] - gl::GetShaderInfoLog( - self.shader, - buf.len() as gl::types::GLsizei, - null_mut(), - buf.as_mut_ptr(), - ); - } - - let info_log = unsafe { CStr::from_ptr(buf.as_ptr()) }; - - unsafe { String::from_utf8_unchecked(info_log.to_bytes().to_vec()) } - } -} - -impl Drop for Shader -{ - fn drop(&mut self) - { - unsafe { - gl::DeleteShader(self.shader); - } - } -} - -impl Kind -{ - fn into_gl(self) -> gl::types::GLenum - { - match self { - Self::Vertex => gl::VERTEX_SHADER, - Self::Fragment => gl::FRAGMENT_SHADER, - } - } -} - -/// Shader program -#[derive(Debug, PartialEq, Eq, Hash)] -pub struct Program -{ - program: gl::types::GLuint, -} - -impl Program -{ - pub fn new() -> Self - { - let program = unsafe { gl::CreateProgram() }; - - Self { program } - } - - pub fn attach(&self, shader: &Shader) - { - unsafe { - gl::AttachShader(self.program, shader.shader); - } - } - - pub fn link(&self) -> Result<(), Error> - { - unsafe { - gl::LinkProgram(self.program); - } - - let mut link_success = gl::types::GLint::default(); - - unsafe { - gl::GetProgramiv(self.program, gl::LINK_STATUS, &mut link_success); - } - - if link_success == 0 { - let info_log = self.get_info_log(); - - return Err(Error::CompileFailed(info_log)); - } - - Ok(()) - } - - pub fn activate(&self) - { - unsafe { - gl::UseProgram(self.program); - } - } - - pub fn set_uniform_matrix_4fv(&mut self, name: &CStr, matrix: &Matrix<f32, 4, 4>) - { - let uniform_location = - unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) }; - - unsafe { - gl::ProgramUniformMatrix4fv( - self.program, - uniform_location, - 1, - gl::FALSE, - matrix.as_ptr(), - ); - } - } - - pub fn set_uniform_vec_3fv(&mut self, name: &CStr, vec: &Vec3<f32>) - { - let uniform_location = - unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) }; - - unsafe { - gl::ProgramUniform3fv(self.program, uniform_location, 1, vec.as_ptr()); - } - } - - pub fn set_uniform_1fv(&mut self, name: &CStr, num: f32) - { - let uniform_location = - unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) }; - - unsafe { - gl::ProgramUniform1fv(self.program, uniform_location, 1, &num); - } - } - - pub fn set_uniform_1i(&mut self, name: &CStr, num: i32) - { - let uniform_location = - unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) }; - - unsafe { - gl::ProgramUniform1i(self.program, uniform_location, num); - } - } - - fn get_info_log(&self) -> String - { - let mut buf = vec![gl::types::GLchar::default(); 512]; - - unsafe { - #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] - gl::GetProgramInfoLog( - self.program, - buf.len() as gl::types::GLsizei, - null_mut(), - buf.as_mut_ptr(), - ); - } - - let info_log = unsafe { CStr::from_ptr(buf.as_ptr()) }; - - unsafe { String::from_utf8_unchecked(info_log.to_bytes().to_vec()) } - } -} - -impl Drop for Program -{ - fn drop(&mut self) - { - unsafe { - gl::DeleteProgram(self.program); - } - } -} - -/// Shader error. -#[derive(Debug, thiserror::Error)] -pub enum Error -{ - #[error("All characters in source are not within the ASCII range")] - SourceNotAscii, - - #[error("Failed to compile: {0}")] - CompileFailed(String), -} diff --git a/engine/src/opengl/texture.rs b/engine/src/opengl/texture.rs deleted file mode 100644 index 074ade7..0000000 --- a/engine/src/opengl/texture.rs +++ /dev/null @@ -1,240 +0,0 @@ -use crate::data_types::dimens::Dimens; -use crate::texture::{Id, Properties}; - -#[derive(Debug)] -pub struct Texture -{ - texture: gl::types::GLuint, -} - -impl Texture -{ - pub fn new() -> Self - { - let mut texture = gl::types::GLuint::default(); - - unsafe { - gl::CreateTextures(gl::TEXTURE_2D, 1, &mut texture); - }; - - Self { texture } - } - - pub fn bind(&self) - { - unsafe { - gl::BindTexture(gl::TEXTURE_2D, self.texture); - } - } - - pub fn generate( - &mut self, - dimens: Dimens<u32>, - data: &[u8], - pixel_data_format: PixelDataFormat, - ) - { - self.alloc_image(pixel_data_format, dimens, data); - - unsafe { - gl::GenerateTextureMipmap(self.texture); - } - } - - pub fn apply_properties(&mut self, properties: &Properties) - { - self.set_wrap(properties.wrap); - self.set_magnifying_filter(properties.magnifying_filter); - self.set_minifying_filter(properties.minifying_filter); - } - - pub fn set_wrap(&mut self, wrapping: Wrapping) - { - let wrapping_gl = wrapping.to_gl(); - - #[allow(clippy::cast_possible_wrap)] - unsafe { - gl::TextureParameteri(self.texture, gl::TEXTURE_WRAP_S, wrapping_gl as i32); - gl::TextureParameteri(self.texture, gl::TEXTURE_WRAP_T, wrapping_gl as i32); - } - } - - pub fn set_magnifying_filter(&mut self, filtering: Filtering) - { - let filtering_gl = filtering.to_gl(); - - #[allow(clippy::cast_possible_wrap)] - unsafe { - gl::TextureParameteri( - self.texture, - gl::TEXTURE_MAG_FILTER, - filtering_gl as i32, - ); - } - } - - pub fn set_minifying_filter(&mut self, filtering: Filtering) - { - let filtering_gl = filtering.to_gl(); - - #[allow(clippy::cast_possible_wrap)] - unsafe { - gl::TextureParameteri( - self.texture, - gl::TEXTURE_MIN_FILTER, - filtering_gl as i32, - ); - } - } - - fn alloc_image( - &mut self, - pixel_data_format: PixelDataFormat, - dimens: Dimens<u32>, - data: &[u8], - ) - { - unsafe { - #[allow(clippy::cast_possible_wrap)] - gl::TextureStorage2D( - self.texture, - 1, - pixel_data_format.to_sized_internal_format(), - dimens.width as i32, - dimens.height as i32, - ); - - #[allow(clippy::cast_possible_wrap)] - gl::TextureSubImage2D( - self.texture, - 0, - 0, - 0, - dimens.width as i32, - dimens.height as i32, - pixel_data_format.to_format(), - gl::UNSIGNED_BYTE, - data.as_ptr().cast(), - ); - } - } -} - -impl Drop for Texture -{ - fn drop(&mut self) - { - unsafe { - gl::DeleteTextures(1, &self.texture); - } - } -} - -/// Texture wrapping. -#[derive(Debug, Clone, Copy)] -pub enum Wrapping -{ - Repeat, - MirroredRepeat, - ClampToEdge, - ClampToBorder, -} - -impl Wrapping -{ - fn to_gl(self) -> gl::types::GLenum - { - match self { - Self::Repeat => gl::REPEAT, - Self::MirroredRepeat => gl::MIRRORED_REPEAT, - Self::ClampToEdge => gl::CLAMP_TO_EDGE, - Self::ClampToBorder => gl::CLAMP_TO_BORDER, - } - } -} - -#[derive(Debug, Clone, Copy)] -pub enum Filtering -{ - Nearest, - Linear, -} - -impl Filtering -{ - fn to_gl(self) -> gl::types::GLenum - { - match self { - Self::Linear => gl::LINEAR, - Self::Nearest => gl::NEAREST, - } - } -} - -/// Texture pixel data format. -#[derive(Debug, Clone, Copy)] -pub enum PixelDataFormat -{ - Rgb8, - Rgba8, -} - -impl PixelDataFormat -{ - fn to_sized_internal_format(self) -> gl::types::GLenum - { - match self { - Self::Rgb8 => gl::RGB8, - Self::Rgba8 => gl::RGBA8, - } - } - - fn to_format(self) -> gl::types::GLenum - { - match self { - Self::Rgb8 => gl::RGB, - Self::Rgba8 => gl::RGBA, - } - } -} - -pub fn set_active_texture_unit(texture_unit: TextureUnit) -{ - unsafe { - gl::ActiveTexture(texture_unit.into_gl()); - } -} - -macro_rules! texture_unit_enum { - (cnt=$cnt: literal) => { - seq_macro::seq!(N in 0..$cnt { - #[derive(Debug, Clone, Copy)] - pub enum TextureUnit { - #( - No~N, - )* - } - - impl TextureUnit { - fn into_gl(self) -> gl::types::GLenum { - match self { - #( - Self::No~N => gl::TEXTURE~N, - )* - } - } - - pub fn from_texture_id(texture_id: Id) -> Option<Self> { - match texture_id.into_inner() { - #( - N => Some(Self::No~N), - )* - _ => None - } - } - } - }); - }; -} - -texture_unit_enum!(cnt = 31); diff --git a/engine/src/opengl/util.rs b/engine/src/opengl/util.rs deleted file mode 100644 index e60778f..0000000 --- a/engine/src/opengl/util.rs +++ /dev/null @@ -1,30 +0,0 @@ -// May only be used when certain crate features are enabled -#![allow(unused_macros, unused_imports)] - -macro_rules! gl_enum { - ( - $visibility: vis enum $name: ident - {$( - $variant: ident = gl::$gl_enum: ident, - )+} - ) => { - #[derive(Debug, Clone, Copy)] - #[repr(u32)] - $visibility enum $name - {$( - $variant = gl::$gl_enum, - )+} - - impl $name { - fn from_gl(num: gl::types::GLenum) -> Option<Self> - { - match num { - $(gl::$gl_enum => Some(Self::$variant),)+ - _ => None - } - } - } - }; -} - -pub(crate) use gl_enum; diff --git a/engine/src/opengl/vertex_array.rs b/engine/src/opengl/vertex_array.rs deleted file mode 100644 index da5d91e..0000000 --- a/engine/src/opengl/vertex_array.rs +++ /dev/null @@ -1,183 +0,0 @@ -use std::mem::size_of; - -use crate::opengl::buffer::Buffer; -use crate::vertex::Vertex; - -#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] -const VERTEX_STRIDE: i32 = size_of::<Vertex>() as i32; - -#[derive(Debug)] -pub struct VertexArray -{ - array: gl::types::GLuint, -} - -impl VertexArray -{ - pub fn new() -> Self - { - let mut array = 0; - - unsafe { - gl::CreateVertexArrays(1, &mut array); - } - - Self { array } - } - - /// Draws the currently bound vertex array. - pub fn draw_arrays(primitive_kind: PrimitiveKind, start_index: u32, cnt: u32) - { - unsafe { - #[allow(clippy::cast_possible_wrap)] - gl::DrawArrays( - primitive_kind.into_gl(), - start_index as gl::types::GLint, - cnt as gl::types::GLsizei, - ); - } - } - - /// Draws the currently bound vertex array. - pub fn draw_elements(primitive_kind: PrimitiveKind, offset: u32, cnt: u32) - { - unsafe { - #[allow(clippy::cast_possible_wrap)] - gl::DrawElements( - primitive_kind.into_gl(), - cnt as gl::types::GLsizei, - gl::UNSIGNED_INT, - (offset as gl::types::GLint) as *const _, - ); - } - } - - pub fn bind_element_buffer(&mut self, element_buffer: &Buffer<u32>) - { - unsafe { - gl::VertexArrayElementBuffer(self.array, element_buffer.object()); - } - } - - pub fn bind_vertex_buffer( - &mut self, - binding_index: u32, - vertex_buffer: &Buffer<Vertex>, - offset: isize, - ) - { - unsafe { - gl::VertexArrayVertexBuffer( - self.array, - binding_index, - vertex_buffer.object(), - offset, - VERTEX_STRIDE, - ); - } - } - - pub fn enable_attrib(&mut self, attrib_index: u32) - { - unsafe { - gl::EnableVertexArrayAttrib(self.array, attrib_index as gl::types::GLuint); - } - } - - pub fn set_attrib_format( - &mut self, - attrib_index: u32, - data_type: DataType, - normalized: bool, - offset: u32, - ) - { - unsafe { - #[allow(clippy::cast_possible_wrap)] - gl::VertexArrayAttribFormat( - self.array, - attrib_index, - data_type.size() as gl::types::GLint, - data_type as u32, - if normalized { gl::TRUE } else { gl::FALSE }, - offset, - ); - } - } - - /// Associate a vertex attribute and a vertex buffer binding. - pub fn set_attrib_vertex_buf_binding( - &mut self, - attrib_index: u32, - vertex_buf_binding_index: u32, - ) - { - unsafe { - gl::VertexArrayAttribBinding( - self.array, - attrib_index, - vertex_buf_binding_index, - ); - } - } - - pub fn bind(&self) - { - unsafe { gl::BindVertexArray(self.array) } - } - - /// Does a weak clone of this vertex array. The vertex array itself is NOT copied in - /// any way this function only copies the internal vertex array ID. - /// - /// # Safety - /// The returned `VertexArray` must not be dropped if another `VertexArray` - /// referencing the same vertex array ID is used later. - pub unsafe fn clone_unsafe(&self) -> Self - { - Self { array: self.array } - } -} - -impl Drop for VertexArray -{ - fn drop(&mut self) - { - unsafe { - gl::DeleteVertexArrays(1, &self.array); - } - } -} - -#[derive(Debug)] -pub enum PrimitiveKind -{ - Triangles, -} - -impl PrimitiveKind -{ - fn into_gl(self) -> gl::types::GLenum - { - match self { - Self::Triangles => gl::TRIANGLES, - } - } -} - -#[derive(Debug, Clone, Copy)] -#[repr(u32)] -pub enum DataType -{ - Float = gl::FLOAT, -} - -impl DataType -{ - pub fn size(self) -> u32 - { - #[allow(clippy::cast_possible_truncation)] - match self { - Self::Float => size_of::<gl::types::GLfloat>() as u32, - } - } -} diff --git a/engine/src/performance.rs b/engine/src/performance.rs deleted file mode 100644 index ffc5c27..0000000 --- a/engine/src/performance.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::time::Instant; - -use ecs::component::local::Local; -use ecs::system::{Into, System}; -use ecs::Component; - -use crate::event::PostPresent as PostPresentEvent; - -#[derive(Debug, Default)] -#[non_exhaustive] -pub struct Extension {} - -impl ecs::extension::Extension for Extension -{ - fn collect(self, mut collector: ecs::extension::Collector<'_>) - { - collector.add_system( - PostPresentEvent, - log_perf.into_system().initialize((State::default(),)), - ); - } -} - -#[cfg(feature = "debug")] -macro_rules! log_perf { - ($($tt: tt)*) => { - tracing::info!($($tt)*); - }; -} - -#[cfg(not(feature = "debug"))] -macro_rules! log_perf { - ($($tt: tt)*) => { - println!($($tt)*); - }; -} - -fn log_perf(mut state: Local<State>) -{ - let Some(last_time) = state.last_time else { - state.last_time = Some(Instant::now()); - return; - }; - - let time_now = Instant::now(); - - state.last_time = Some(time_now); - - log_perf!( - "Frame time: {}us", - time_now.duration_since(last_time).as_micros() - ); -} - -#[derive(Debug, Default, Component)] -struct State -{ - last_time: Option<Instant>, -} diff --git a/engine/src/projection.rs b/engine/src/projection.rs index aa84a9f..b3bfa00 100644 --- a/engine/src/projection.rs +++ b/engine/src/projection.rs @@ -1,57 +1,210 @@ -use crate::matrix::Matrix; +use crate::builder; +use crate::data_types::dimens::Dimens; +use crate::matrix::{CellPos as MatrixCellPos, Matrix}; +use crate::reflection::Reflection; +use crate::vector::Vec2; +use crate::windowing::dpi::PhysicalSize; -#[derive(Debug)] +#[derive(Debug, Clone, Reflection)] #[non_exhaustive] pub enum Projection { Perspective(Perspective), + Orthographic(Orthographic), +} + +impl Projection +{ + pub fn to_matrix_rh( + &self, + window_size: PhysicalSize<u32>, + clip_volume: ClipVolume, + ) -> Matrix<f32, 4, 4> + { + match self { + Projection::Perspective(perspective_proj) => perspective_proj.to_matrix_rh( + window_size.width as f32 / window_size.height as f32, + clip_volume, + ), + Projection::Orthographic(orthographic_proj) => { + orthographic_proj.to_matrix_rh(window_size, clip_volume) + } + } + } } /// Perspective projection parameters. -#[derive(Debug)] +#[derive(Debug, Clone, Reflection)] pub struct Perspective { - pub fov_radians: f32, + /// Field-of-view in degrees + pub fov_degs: f32, pub far: f32, pub near: f32, } +impl Perspective +{ + /// Creates a perspective projection matrix using right-handed coordinates. + #[inline] + pub fn to_matrix_rh(&self, aspect: f32, clip_volume: ClipVolume) + -> Matrix<f32, 4, 4> + { + let mut out = Matrix::new(); + + let fov_rads = self.fov_degs.to_radians(); + + match clip_volume { + ClipVolume::NegOneToOne => { + out[MatrixCellPos { row: 0, col: 0 }] = + (1.0 / (fov_rads / 2.0).tan()) / aspect; + out[MatrixCellPos { row: 1, col: 1 }] = 1.0 / (fov_rads / 2.0).tan(); + out[MatrixCellPos { row: 2, col: 2 }] = + (self.near + self.far) / (self.near - self.far); + out[MatrixCellPos { row: 2, col: 3 }] = + (2.0 * self.near * self.far) / (self.near - self.far); + out[MatrixCellPos { row: 3, col: 2 }] = -1.0; + } + } + + out + } +} + impl Default for Perspective { fn default() -> Self { Self { - fov_radians: 80.0f32.to_radians(), + fov_degs: 80.0, far: 100.0, near: 0.1, } } } -pub(crate) fn new_perspective_matrix( - perspective: &Perspective, - aspect: f32, -) -> Matrix<f32, 4, 4> +#[derive(Debug, Default, Clone, Reflection)] +pub enum OrthographicSize +{ + FixedSize(Dimens<f32>), + + #[default] + WindowSize, +} + +builder! { +#[builder(name = OrthographicBuilder, derives=(Debug, Clone))] +#[derive(Debug, Clone, Reflection)] +#[non_exhaustive] +pub struct Orthographic +{ + pub near: f32, + pub far: f32, + pub viewport_origin: Vec2<f32>, + pub size: OrthographicSize, +} +} + +impl Orthographic +{ + pub fn builder() -> OrthographicBuilder + { + OrthographicBuilder::default() + } + + /// Creates a orthographic projection matrix using right-handed coordinates. + pub fn to_matrix_rh( + &self, + window_size: PhysicalSize<u32>, + clip_volume: ClipVolume, + ) -> Matrix<f32, 4, 4> + { + let size = match self.size { + OrthographicSize::FixedSize(fixed_size) => fixed_size, + OrthographicSize::WindowSize => Dimens { + width: window_size.width as f32, + height: window_size.height as f32, + }, + }; + + let origin_x = size.width * self.viewport_origin.x; + let origin_y = size.height * self.viewport_origin.y; + + let left = -origin_x; + let right = size.width - origin_x; + + let bottom = -origin_y; + let top = size.height - origin_y; + + let near = self.near; + let far = self.far; + + orthographic_rh( + OrthographicParams { left, right, bottom, top, near, far }, + clip_volume, + ) + } +} + +impl Default for Orthographic { - let mut out = Matrix::new(); + fn default() -> Self + { + Self::builder().build() + } +} - out.set_cell(0, 0, (1.0 / (perspective.fov_radians / 2.0).tan()) / aspect); +impl Default for OrthographicBuilder +{ + fn default() -> Self + { + Self { + near: 0.0, + far: 1000.0, + viewport_origin: Vec2 { x: 0.5, y: 0.5 }, + size: OrthographicSize::WindowSize, + } + } +} - out.set_cell(1, 1, 1.0 / (perspective.fov_radians / 2.0).tan()); +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum ClipVolume +{ + /// -1 to +1. This is the OpenGL clip volume definition. + NegOneToOne, +} - out.set_cell( - 2, - 2, - (perspective.near + perspective.far) / (perspective.near - perspective.far), - ); +#[derive(Debug, Clone)] +pub struct OrthographicParams +{ + pub left: f32, + pub right: f32, + pub bottom: f32, + pub top: f32, + pub near: f32, + pub far: f32, +} - out.set_cell( - 2, - 3, - (2.0 * perspective.near * perspective.far) / (perspective.near - perspective.far), - ); +/// Creates a orthographic projection matrix using right-handed coordinates. +pub fn orthographic_rh( + OrthographicParams { left, right, bottom, top, near, far }: OrthographicParams, + clip_volume: ClipVolume, +) -> Matrix<f32, 4, 4> +{ + let mut result = Matrix::<f32, 4, 4>::new(); - out.set_cell(3, 2, -1.0); + match clip_volume { + ClipVolume::NegOneToOne => { + result[MatrixCellPos { row: 0, col: 0 }] = 2.0 / (right - left); + result[MatrixCellPos { row: 1, col: 1 }] = 2.0 / (top - bottom); + result[MatrixCellPos { row: 2, col: 2 }] = -2.0 / (far - near); + result[MatrixCellPos { row: 0, col: 3 }] = -(right + left) / (right - left); + result[MatrixCellPos { row: 1, col: 3 }] = -(top + bottom) / (top - bottom); + result[MatrixCellPos { row: 2, col: 3 }] = -(far + near) / (far - near); + result[MatrixCellPos { row: 3, col: 3 }] = 1.0; + } + } - out + result } diff --git a/engine/src/reflection.rs b/engine/src/reflection.rs new file mode 100644 index 0000000..fd7e379 --- /dev/null +++ b/engine/src/reflection.rs @@ -0,0 +1,2 @@ +pub use engine_macros::Reflection; +pub use engine_reflection::*; diff --git a/engine/src/renderer.rs b/engine/src/renderer.rs deleted file mode 100644 index 2544919..0000000 --- a/engine/src/renderer.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod opengl; diff --git a/engine/src/renderer/opengl.rs b/engine/src/renderer/opengl.rs deleted file mode 100644 index a353c6a..0000000 --- a/engine/src/renderer/opengl.rs +++ /dev/null @@ -1,719 +0,0 @@ -//! OpenGL renderer. - -use std::collections::HashMap; -use std::ffi::{c_void, CString}; -use std::ops::Deref; -use std::process::abort; - -use ecs::actions::Actions; -use ecs::component::local::Local; -use ecs::query::options::{Not, With}; -use ecs::sole::Single; -use ecs::system::{Into as _, System}; -use ecs::{Component, Query}; - -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::event::{Present as PresentEvent, Start as StartEvent}; -use crate::lighting::{DirectionalLight, GlobalLight, PointLight}; -use crate::material::{Flags as MaterialFlags, Material}; -use crate::matrix::Matrix; -use crate::mesh::Mesh; -use crate::opengl::buffer::{Buffer, Usage as BufferUsage}; -#[cfg(feature = "debug")] -use crate::opengl::debug::{MessageSeverity, MessageSource, MessageType}; -use crate::opengl::shader::{ - Error as GlShaderError, - Program as GlShaderProgram, - Shader as GlShader, -}; -use crate::opengl::texture::{ - set_active_texture_unit, - Texture as GlTexture, - TextureUnit, -}; -use crate::opengl::vertex_array::{ - DataType as VertexArrayDataType, - PrimitiveKind, - VertexArray, -}; -use crate::opengl::{clear_buffers, enable, BufferClearMask, Capability}; -use crate::projection::{new_perspective_matrix, Projection}; -use crate::shader::Program as ShaderProgram; -use crate::texture::{Id as TextureId, Texture}; -use crate::transform::{Position, Scale}; -use crate::util::NeverDrop; -use crate::vector::{Vec2, Vec3}; -use crate::vertex::{AttributeComponentType, Vertex}; -use crate::window::Window; - -type RenderableEntity = ( - Mesh, - ShaderProgram, - Material, - Option<MaterialFlags>, - Option<Position>, - Option<Scale>, - Option<DrawFlags>, - Option<GlObjects>, -); - -#[derive(Debug, Default)] -pub struct Extension {} - -impl ecs::extension::Extension for Extension -{ - fn collect(self, mut collector: ecs::extension::Collector<'_>) - { - collector.add_system(StartEvent, initialize); - - collector.add_system( - PresentEvent, - render - .into_system() - .initialize((GlobalGlObjects::default(),)), - ); - } -} - -fn initialize(window: Single<Window>) -{ - window - .make_context_current() - .expect("Failed to make window context current"); - - gl::load_with(|symbol| match window.get_proc_address(symbol) { - Ok(addr) => addr as *const c_void, - Err(err) => { - println!( - "FATAL ERROR: Failed to get adress of OpenGL function {symbol}: {err}", - ); - - abort(); - } - }); - - #[cfg(feature = "debug")] - initialize_debug(); - - let window_size = window.size().expect("Failed to get window size"); - - set_viewport(Vec2 { x: 0, y: 0 }, window_size); - - window.set_framebuffer_size_callback(|new_window_size| { - set_viewport(Vec2::ZERO, new_window_size); - }); - - enable(Capability::DepthTest); - enable(Capability::MultiSample); -} - -#[allow(clippy::too_many_arguments)] -fn render( - query: Query<RenderableEntity, Not<With<NoDraw>>>, - point_light_query: Query<(PointLight,)>, - directional_lights: Query<(DirectionalLight,)>, - camera_query: Query<(Camera, Position, ActiveCamera)>, - window: Single<Window>, - global_light: Single<GlobalLight>, - mut gl_objects: Local<GlobalGlObjects>, - mut actions: Actions, -) -{ - let Some((camera, camera_pos, _)) = camera_query.iter().next() else { - #[cfg(feature = "debug")] - tracing::warn!("No current camera. Nothing will be rendered"); - return; - }; - - let point_lights = point_light_query - .iter() - .map(|(point_light,)| point_light) - .collect::<Vec<_>>(); - - let directional_lights = directional_lights.iter().collect::<Vec<_>>(); - - let GlobalGlObjects { - shader_programs: gl_shader_programs, - textures: gl_textures, - } = &mut *gl_objects; - - clear_buffers(BufferClearMask::COLOR | BufferClearMask::DEPTH); - - for ( - entity_index, - ( - mesh, - shader_program, - material, - material_flags, - position, - scale, - draw_flags, - gl_objects, - ), - ) in query.iter().enumerate() - { - let material_flags = material_flags - .map(|material_flags| material_flags.clone()) - .unwrap_or_default(); - - let shader_program = gl_shader_programs - .entry(shader_program.u64_hash()) - .or_insert_with(|| create_gl_shader_program(&shader_program).unwrap()); - - let new_gl_objects; - - let gl_objects = if let Some(gl_objects) = gl_objects.as_deref() { - gl_objects - } else { - // TODO: Account for when meshes are changed - let gl_objects = GlObjects::new(&mesh); - - new_gl_objects = Some(gl_objects.clone()); - - actions.add_components( - query.get_entity_uid(entity_index).unwrap(), - (gl_objects,), - ); - - &*new_gl_objects.unwrap() - }; - - apply_transformation_matrices( - Transformation { - position: position.map(|pos| *pos).unwrap_or_default().position, - scale: scale.map(|scale| *scale).unwrap_or_default().scale, - }, - shader_program, - &camera, - &camera_pos, - window.size().expect("Failed to get window size"), - ); - - apply_light( - &material, - &material_flags, - &global_light, - shader_program, - point_lights.as_slice(), - directional_lights - .iter() - .map(|(dir_light,)| &**dir_light) - .collect::<Vec<_>>() - .as_slice(), - &camera_pos, - ); - - for texture in &material.textures { - let gl_texture = gl_textures - .entry(texture.id()) - .or_insert_with(|| create_gl_texture(texture)); - - let texture_unit = - TextureUnit::from_texture_id(texture.id()).unwrap_or_else(|| { - panic!("Texture id {} is a invalid texture unit", texture.id()); - }); - - set_active_texture_unit(texture_unit); - - gl_texture.bind(); - } - - shader_program.activate(); - - if let Some(draw_flags) = &draw_flags { - crate::opengl::set_polygon_mode( - draw_flags.polygon_mode_config.face, - draw_flags.polygon_mode_config.mode, - ); - } - - draw_mesh(gl_objects); - - if draw_flags.is_some() { - let default_polygon_mode_config = PolygonModeConfig::default(); - - crate::opengl::set_polygon_mode( - default_polygon_mode_config.face, - default_polygon_mode_config.mode, - ); - } - } -} - -#[derive(Debug, Default, Component)] -struct GlobalGlObjects -{ - shader_programs: HashMap<u64, GlShaderProgram>, - textures: HashMap<TextureId, GlTexture>, -} - -fn set_viewport(position: Vec2<u32>, size: Dimens<u32>) -{ - crate::opengl::set_viewport(position, size); -} - -#[cfg(feature = "debug")] -fn initialize_debug() -{ - use crate::opengl::debug::{ - enable_debug_output, - set_debug_message_callback, - set_debug_message_control, - MessageIdsAction, - }; - - enable_debug_output(); - - set_debug_message_callback(opengl_debug_message_cb); - - set_debug_message_control(None, None, None, &[], MessageIdsAction::Disable); -} - -fn draw_mesh(gl_objects: &GlObjects) -{ - gl_objects.vertex_arr.bind(); - - if gl_objects.index_buffer.is_some() { - VertexArray::draw_elements(PrimitiveKind::Triangles, 0, gl_objects.index_cnt); - } else { - VertexArray::draw_arrays(PrimitiveKind::Triangles, 0, 3); - } -} - -fn create_gl_texture(texture: &Texture) -> GlTexture -{ - let mut gl_texture = GlTexture::new(); - - gl_texture.generate( - *texture.dimensions(), - texture.image().as_bytes(), - texture.pixel_data_format(), - ); - - gl_texture.apply_properties(texture.properties()); - - gl_texture -} - -fn create_gl_shader_program( - shader_program: &ShaderProgram, -) -> Result<GlShaderProgram, GlShaderError> -{ - let gl_shaders = shader_program - .shaders() - .iter() - .map(|shader| { - let gl_shader = GlShader::new(shader.kind()); - - gl_shader.set_source(shader.source())?; - gl_shader.compile()?; - - Ok(gl_shader) - }) - .collect::<Result<Vec<_>, _>>()?; - - let gl_shader_program = GlShaderProgram::new(); - - for gl_shader in &gl_shaders { - gl_shader_program.attach(gl_shader); - } - - gl_shader_program.link()?; - - Ok(gl_shader_program) -} - -#[derive(Debug, Component)] -struct GlObjects -{ - /// Vertex and index buffer has to live as long as the vertex array - vertex_buffer: Buffer<Vertex>, - index_buffer: Option<Buffer<u32>>, - index_cnt: u32, - - vertex_arr: VertexArray, -} - -impl GlObjects -{ - #[cfg_attr(feature = "debug", tracing::instrument(skip_all))] - fn new(mesh: &Mesh) -> Self - { - #[cfg(feature = "debug")] - tracing::trace!( - "Creating vertex array, vertex buffer{}", - if mesh.indices().is_some() { - " and index buffer" - } else { - "" - } - ); - - let mut vertex_arr = VertexArray::new(); - let mut vertex_buffer = Buffer::new(); - - vertex_buffer.store(mesh.vertices(), BufferUsage::Static); - - vertex_arr.bind_vertex_buffer(0, &vertex_buffer, 0); - - let mut offset = 0u32; - - for attrib in Vertex::attrs() { - vertex_arr.enable_attrib(attrib.index); - - vertex_arr.set_attrib_format( - attrib.index, - match attrib.component_type { - AttributeComponentType::Float => VertexArrayDataType::Float, - }, - false, - offset, - ); - - vertex_arr.set_attrib_vertex_buf_binding(attrib.index, 0); - - offset += attrib.component_size * attrib.component_cnt as u32; - } - - if let Some(indices) = mesh.indices() { - let mut index_buffer = Buffer::new(); - - index_buffer.store(indices, BufferUsage::Static); - - vertex_arr.bind_element_buffer(&index_buffer); - - return Self { - vertex_buffer, - index_buffer: Some(index_buffer), - index_cnt: indices.len().try_into().unwrap(), - vertex_arr, - }; - } - - Self { - vertex_buffer, - index_buffer: None, - index_cnt: 0, - vertex_arr, - } - } - - pub fn clone(&self) -> NeverDrop<Self> - { - NeverDrop::new(Self { - // SAFETY: The vertex buffer will never become dropped (NeverDrop ensures it) - vertex_buffer: unsafe { self.vertex_buffer.clone_weak() }, - index_buffer: self - .index_buffer - .as_ref() - // SAFETY: The index buffer will never become dropped (NeverDrop ensures - // it) - .map(|index_buffer| unsafe { index_buffer.clone_weak() }), - index_cnt: self.index_cnt, - // SAFETY: The vertex array will never become dropped (NeverDrop ensures it) - vertex_arr: unsafe { self.vertex_arr.clone_unsafe() }, - }) - } -} - -fn apply_transformation_matrices( - transformation: Transformation, - gl_shader_program: &mut GlShaderProgram, - camera: &Camera, - camera_pos: &Position, - window_size: Dimens<u32>, -) -{ - gl_shader_program - .set_uniform_matrix_4fv(c"model", &create_transformation_matrix(transformation)); - - let view = create_view(camera, camera_pos); - - gl_shader_program.set_uniform_matrix_4fv(c"view", &view); - - #[allow(clippy::cast_precision_loss)] - let projection = match &camera.projection { - Projection::Perspective(perspective) => new_perspective_matrix( - perspective, - window_size.width as f32 / window_size.height as f32, - ), - }; - - gl_shader_program.set_uniform_matrix_4fv(c"projection", &projection); -} - -fn apply_light<PointLightHolder>( - material: &Material, - material_flags: &MaterialFlags, - global_light: &GlobalLight, - gl_shader_program: &mut GlShaderProgram, - point_lights: &[PointLightHolder], - directional_lights: &[&DirectionalLight], - camera_pos: &Position, -) where - PointLightHolder: Deref<Target = PointLight>, -{ - debug_assert!( - point_lights.len() < 64, - "Shader cannot handle more than 64 point lights" - ); - - debug_assert!( - directional_lights.len() < 64, - "Shader cannot handle more than 64 directional lights" - ); - - for (dir_light_index, dir_light) in directional_lights.iter().enumerate() { - gl_shader_program.set_uniform_vec_3fv( - &create_light_uniform_name( - "directional_lights", - dir_light_index, - "direction", - ), - &dir_light.direction, - ); - - set_light_phong_uniforms( - gl_shader_program, - "directional_lights", - dir_light_index, - *dir_light, - ); - } - - // There probably won't be more than 2147483648 directional lights - #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] - gl_shader_program - .set_uniform_1i(c"directional_light_cnt", directional_lights.len() as i32); - - for (point_light_index, point_light) in point_lights.iter().enumerate() { - gl_shader_program.set_uniform_vec_3fv( - &create_light_uniform_name("point_lights", point_light_index, "position"), - &point_light.position, - ); - - set_light_phong_uniforms( - gl_shader_program, - "point_lights", - point_light_index, - &**point_light, - ); - - set_light_attenuation_uniforms( - gl_shader_program, - "point_lights", - point_light_index, - point_light, - ); - } - - // There probably won't be more than 2147483648 point lights - #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] - gl_shader_program.set_uniform_1i(c"point_light_cnt", point_lights.len() as i32); - - gl_shader_program.set_uniform_vec_3fv( - c"material.ambient", - &if material_flags.use_ambient_color { - material.ambient.clone() - } else { - global_light.ambient.clone() - } - .into(), - ); - - gl_shader_program - .set_uniform_vec_3fv(c"material.diffuse", &material.diffuse.clone().into()); - - #[allow(clippy::cast_possible_wrap)] - gl_shader_program - .set_uniform_vec_3fv(c"material.specular", &material.specular.clone().into()); - - #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform_1i( - c"material.ambient_map", - material.ambient_map.into_inner() as i32, - ); - - #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform_1i( - c"material.diffuse_map", - material.diffuse_map.into_inner() as i32, - ); - - #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform_1i( - c"material.specular_map", - material.specular_map.into_inner() as i32, - ); - - gl_shader_program.set_uniform_1fv(c"material.shininess", material.shininess); - - gl_shader_program.set_uniform_vec_3fv(c"view_pos", &camera_pos.position); -} - -fn set_light_attenuation_uniforms( - gl_shader_program: &mut GlShaderProgram, - light_array: &str, - light_index: usize, - light: &PointLight, -) -{ - gl_shader_program.set_uniform_1fv( - &create_light_uniform_name( - light_array, - light_index, - "attenuation_props.constant", - ), - light.attenuation_params.constant, - ); - - gl_shader_program.set_uniform_1fv( - &create_light_uniform_name(light_array, light_index, "attenuation_props.linear"), - light.attenuation_params.linear, - ); - - gl_shader_program.set_uniform_1fv( - &create_light_uniform_name( - light_array, - light_index, - "attenuation_props.quadratic", - ), - light.attenuation_params.quadratic, - ); -} - -fn set_light_phong_uniforms( - gl_shader_program: &mut GlShaderProgram, - light_array: &str, - light_index: usize, - light: &impl Light, -) -{ - gl_shader_program.set_uniform_vec_3fv( - &create_light_uniform_name(light_array, light_index, "phong.diffuse"), - &light.diffuse().clone().into(), - ); - - gl_shader_program.set_uniform_vec_3fv( - &create_light_uniform_name(light_array, light_index, "phong.specular"), - &light.specular().clone().into(), - ); -} - -trait Light -{ - fn diffuse(&self) -> &Color<f32>; - fn specular(&self) -> &Color<f32>; -} - -impl Light for PointLight -{ - fn diffuse(&self) -> &Color<f32> - { - &self.diffuse - } - - fn specular(&self) -> &Color<f32> - { - &self.specular - } -} - -impl Light for DirectionalLight -{ - fn diffuse(&self) -> &Color<f32> - { - &self.diffuse - } - - fn specular(&self) -> &Color<f32> - { - &self.specular - } -} - -fn create_light_uniform_name( - light_array: &str, - light_index: usize, - light_field: &str, -) -> CString -{ - unsafe { - CString::from_vec_with_nul_unchecked( - format!("{light_array}[{light_index}].{light_field}\0").into(), - ) - } -} - -fn create_view(camera: &Camera, camera_pos: &Position) -> Matrix<f32, 4, 4> -{ - let mut view = Matrix::new(); - - view.look_at(&camera_pos.position, &camera.target, &camera.global_up); - - view -} - -#[cfg(feature = "debug")] -#[tracing::instrument(skip_all)] -fn opengl_debug_message_cb( - source: MessageSource, - ty: MessageType, - id: u32, - severity: MessageSeverity, - message: &str, -) -{ - use std::backtrace::{Backtrace, BacktraceStatus}; - - use tracing::{event, Level}; - - macro_rules! create_event { - ($level: expr) => { - event!($level, ?source, ?ty, id, ?severity, message); - }; - } - - if matches!(severity, MessageSeverity::Notification) { - return; - } - - match ty { - MessageType::Error => { - create_event!(Level::ERROR); - - let backtrace = Backtrace::capture(); - - if matches!(backtrace.status(), BacktraceStatus::Captured) { - event!(Level::TRACE, "{backtrace}"); - } - } - MessageType::Other => { - create_event!(Level::INFO); - } - _ => { - create_event!(Level::WARN); - } - }; -} - -#[derive(Debug)] -struct Transformation -{ - position: Vec3<f32>, - scale: Vec3<f32>, -} - -fn create_transformation_matrix(transformation: Transformation) -> Matrix<f32, 4, 4> -{ - let mut matrix = Matrix::new_identity(); - - matrix.translate(&transformation.position); - matrix.scale(&transformation.scale); - - matrix -} diff --git a/engine/src/rendering.rs b/engine/src/rendering.rs new file mode 100644 index 0000000..6840dbe --- /dev/null +++ b/engine/src/rendering.rs @@ -0,0 +1,715 @@ +use std::collections::VecDeque; +use std::sync::atomic::{AtomicU64, Ordering}; + +use bitflags::bitflags; +use engine_macros::Reflection; + +use crate::asset::Handle as AssetHandle; +use crate::builder; +use crate::data_types::dimens::Dimens; +use crate::draw_flags::PolygonModeConfig; +use crate::ecs::actions::Actions; +use crate::ecs::component::local::Local; +use crate::ecs::event::component::{EventMatchExt, Removed}; +use crate::ecs::pair::{ChildOf, Pair}; +use crate::ecs::phase::{Phase, POST_UPDATE as POST_UPDATE_PHASE}; +use crate::ecs::query::term::With; +use crate::ecs::sole::Single; +use crate::ecs::system::initializable::Initializable; +use crate::ecs::system::observer::Observe; +use crate::ecs::system::Into; +use crate::ecs::{declare_entity, pair, Component, Query, Sole}; +use crate::image::{ColorType as ImageColorType, Image}; +use crate::mesh::Mesh; +use crate::rendering::blending::Config as BlendingConfig; +use crate::rendering::object::{Id as ObjectId, Store as ObjectStore}; +use crate::rendering::shader::cursor::Binding as ShaderBinding; +use crate::rendering::shader::Program as ShaderProgram; +use crate::texture::{CubeMapFace, Properties as TextureProperties}; +use crate::vector::Vec2; +use crate::windowing::dpi::PhysicalSize; +use crate::windowing::window::Window; + +pub mod backend; +pub mod blending; +pub mod main_render_pass; +pub mod object; +pub mod shader; + +static NEXT_SURFACE_ID: AtomicU64 = AtomicU64::new(0); + +declare_entity! { +pub PRE_RENDER_PHASE: (Phase, pair!(ChildOf, { *POST_UPDATE_PHASE })); + +pub RENDER_PHASE: (Phase, pair!(ChildOf, { *PRE_RENDER_PHASE })); + +pub POST_RENDER_PHASE: (Phase, pair!(ChildOf, { *RENDER_PHASE })); +} + +builder! { +#[builder(name=ExtensionBuilder, derives=(Debug, Clone, Default))] +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct Extension { + pub graphics_props: GraphicsProperties, +} +} + +impl Extension +{ + pub fn builder() -> ExtensionBuilder + { + ExtensionBuilder::default() + } +} + +impl crate::ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: crate::ecs::extension::Collector<'_>) + { + collector.spawn_declared_entity(&PRE_RENDER_PHASE); + collector.spawn_declared_entity(&RENDER_PHASE); + collector.spawn_declared_entity(&POST_RENDER_PHASE); + + let _ = collector.add_sole(RenderPasses::default()); + let _ = collector.add_sole(CommandQueue::default()); + let _ = collector.add_sole(ObjectStore::default()); + + let _ = collector.add_sole(self.graphics_props); + + collector.add_system( + *PRE_RENDER_PHASE, + main_render_pass::add_main_render_pass + .into_system() + .initialize(Default::default()), + ); + + collector.add_system( + *RENDER_PHASE, + enqueue_commands_from_render_passes + .into_system() + .initialize((ActiveDrawProperties::default(),)), + ); + + collector.add_observer(handle_window_removed); + + crate::rendering::shader::prepare(&mut collector); + + crate::rendering::backend::get_default().collect(collector); + } +} + +impl Default for Extension +{ + fn default() -> Self + { + Self::builder().build() + } +} + +/// Marker component for windows that should be renderer to. +#[derive(Debug, Default, Clone, Copy, Component, Reflection)] +pub struct TargetWindow; + +builder! { +#[builder(name=GraphicsPropertiesBuilder, derives=(Debug, Clone))] +#[derive(Debug, Clone, Sole)] +#[non_exhaustive] +pub struct GraphicsProperties +{ + /// Number of samples for multisampling. `None` means no multisampling. + #[builder(skip_generate_fn)] + pub multisampling_sample_cnt: Option<u8>, + + /// Whether graphics API debugging is enabled. + pub debug: bool, + + /// Whether depth testing is enabled + pub depth_test: bool, +} +} + +impl GraphicsProperties +{ + pub fn builder() -> GraphicsPropertiesBuilder + { + GraphicsPropertiesBuilder::default() + } +} + +impl Default for GraphicsProperties +{ + fn default() -> Self + { + Self::builder().build() + } +} + +impl GraphicsPropertiesBuilder +{ + pub fn multisampling_sample_cnt(mut self, multisampling_sample_cnt: u8) -> Self + { + self.multisampling_sample_cnt = Some(multisampling_sample_cnt); + self + } + + pub fn no_multisampling(mut self) -> Self + { + self.multisampling_sample_cnt = None; + self + } +} + +impl Default for GraphicsPropertiesBuilder +{ + fn default() -> Self + { + Self { + multisampling_sample_cnt: Some(4), + debug: false, + depth_test: true, + } + } +} + +#[derive(Debug, Component)] +pub struct Surface +{ + pub id: SurfaceId, + pub size: PhysicalSize<u32>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SurfaceId +{ + inner: u64, +} + +impl SurfaceId +{ + pub fn new_unique() -> Self + { + Self { + inner: NEXT_SURFACE_ID.fetch_add(1, Ordering::Relaxed), + } + } +} + +#[derive(Debug, Default, Sole)] +pub struct RenderPasses +{ + pub passes: VecDeque<RenderPass>, +} + +#[derive(Debug)] +pub struct RenderPass +{ + pub commands: Vec<Command>, + pub draw_properties: DrawProperties, +} + +#[derive(Debug, Reflection)] +#[non_exhaustive] +pub enum Command +{ + RemoveSurface(SurfaceId), + MakeCurrent(SurfaceId), + SetSurfaceSize(SurfaceId, PhysicalSize<u32>), + ClearBuffers(BufferClearMask), + SwapBuffers(SurfaceId), + CreateShaderProgram(ObjectId, ShaderProgram), + ActivateShader(ObjectId), + SetShaderBinding(ObjectId, ShaderBinding), + CreateTexture + { + obj_id: ObjectId, + pixel_data_format: TexturePixelDataFormat, + creation: TextureCreation, + properties: TextureProperties, + }, + UpdateTexture + { + obj_id: ObjectId, + pixels: Box<[u8]>, + pixel_data_format: TexturePixelDataFormat, + update: TextureUpdate, + }, + RemoveTexture(ObjectId), + CreateFramebuffer(ObjectId), + RemoveFramebuffer(ObjectId), + UpdateFramebufferProperties + { + object_id: ObjectId, + properties: FramebufferProperties, + update_flags: FramebufferPropertiesUpdateFlags, + }, + CreateMesh + { + obj_id: ObjectId, + mesh: AssetOrValue<Mesh>, + usage: MeshUsage, + }, + UpdateMesh + { + obj_id: ObjectId, + mesh: Mesh, + usage: MeshUsage, + }, + RemoveMesh(ObjectId), + DrawMesh(ObjectId, DrawMeshOptions), + UpdateDrawProperties(DrawProperties, DrawPropertiesUpdateFlags), +} + +#[derive(Debug)] +pub enum AssetOrValue<T: 'static> +{ + Asset(AssetHandle<T>), + Value(T), +} + +builder! { + #[builder(name = DrawMeshOptionsBuilder, derives = (Debug, Default, Clone))] + #[derive(Debug, Default, Clone)] + #[non_exhaustive] + pub struct DrawMeshOptions { + pub element_offset: u32, + pub vertex_offset: u32, + + #[builder(skip_generate_fn)] + pub element_cnt: Option<u32>, + } +} + +impl DrawMeshOptions +{ + pub fn builder() -> DrawMeshOptionsBuilder + { + DrawMeshOptionsBuilder::default() + } +} + +impl DrawMeshOptionsBuilder +{ + pub fn element_cnt(mut self, element_cnt: u32) -> Self + { + self.element_cnt = Some(element_cnt); + self + } +} + +bitflags! { + #[derive(Debug, Clone, Copy)] + pub struct BufferClearMask: u8 { + const COLOR = 1; + const DEPTH = 2; + const STENCIL = 3; + } +} + +#[derive(Debug, Clone, Copy)] +pub enum MeshUsage +{ + /// The mesh data is set only once and used by the GPU at most a few times. + Stream, + + /// The mesh data is set only once and used many times. + Static, + + /// The mesh data is changed a lot and used many times. + Dynamic, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ScissorBox +{ + /// Size of the scissor box in window coordinates. When `None`, the dimensions of the + /// window is used + pub size: Option<Dimens<u16>>, + + /// Position (in window coordinates) of the lower left corner of the scissor box. + pub lower_left_corner_pos: Vec2<u16>, +} + +impl Default for ScissorBox +{ + fn default() -> Self + { + Self { + size: None, + lower_left_corner_pos: Vec2 { x: 0, y: 0 }, + } + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum DepthFunction +{ + /// Never passes. + Never, + + /// Passes if the incoming depth value is less than the stored depth value. + #[default] + Less, + + /// Passes if the incoming depth value is equal to the stored depth value. + Equal, + + /// Passes if the incoming depth value is less than or equal to the stored depth + /// value. + LessOrEqual, + + /// Passes if the incoming depth value is greater than the stored depth value. + Greater, + + /// Passes if the incoming depth value is not equal to the stored depth value. + NotEqual, + + /// Passes if the incoming depth value is greater than or equal to the stored depth + /// value. + GreaterOrEqual, + + /// Always passes. + Always, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct DrawProperties +{ + pub polygon_mode_config: PolygonModeConfig, + pub blending_enabled: bool, + pub blending_config: BlendingConfig, + pub depth_test_enabled: bool, + pub depth_function: DepthFunction, + pub scissor_test_enabled: bool, + pub scissor_box: ScissorBox, + pub face_culling_enabled: bool, + pub face_culling_mode: FaceCullingMode, + + /// `None` for default framebuffer + pub draw_framebuffer: Option<ObjectId>, + + /// `None` for default framebuffer + pub read_framebuffer: Option<ObjectId>, +} + +impl Default for DrawProperties +{ + fn default() -> Self + { + Self { + polygon_mode_config: PolygonModeConfig::default(), + blending_enabled: false, + blending_config: BlendingConfig::default(), + depth_test_enabled: true, + depth_function: DepthFunction::default(), + scissor_test_enabled: false, + scissor_box: ScissorBox::default(), + face_culling_enabled: false, + face_culling_mode: FaceCullingMode::default(), + draw_framebuffer: None, + read_framebuffer: None, + } + } +} + +bitflags! { + #[derive(Debug, Clone, Copy)] + pub struct DrawPropertiesUpdateFlags: u16 + { + const POLYGON_MODE_CONFIG = 1 << 0; + const BLENDING_CONFIG = 1 << 1; + const BLENDING_ENABLED = 1 << 2; + const DEPTH_TEST_ENABLED = 1 << 3; + const DEPTH_FUNCTION = 1 << 4; + const SCISSOR_TEST_ENABLED = 1 << 5; + const SCISSOR_BOX = 1 << 6; + const FACE_CULLING_ENABLED = 1 << 7; + const FACE_CULLING_MODE = 1 << 8; + const DRAW_FRAMEBUFFER = 1 << 9; + const READ_FRAMEBUFFER = 1 << 10; + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum FaceCullingMode +{ + Front, + + #[default] + Back, + + FrontAndBack, +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum TextureCreation +{ + Texture2D + { + size: Dimens<u32>, + image: Option<Box<[u8]>>, + }, + CubeMap + { + size: Dimens<u32>, + images: Option<[(CubeMapFace, Box<[u8]>); 6]>, + }, +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum TextureUpdate +{ + Texture2D + { + size: Dimens<u32>, + + /// Specifies texel index offsets in the x and y directions within the texture + /// pixels. + offset: Vec2<u32>, + }, + CubeMap + { + size: Dimens<u32>, + + /// Specifies texel index offsets in the x and y directions within the texture + /// pixels. + offset: Vec2<u32>, + + face: CubeMapFace, + }, +} + +/// Rendering command FIFO queue. +#[derive(Debug, Sole)] +pub struct CommandQueue +{ + queue: VecDeque<Command>, +} + +impl CommandQueue +{ + pub fn push(&mut self, command: Command) + { + self.queue.push_back(command); + } + + pub fn drain(&mut self) -> impl Iterator<Item = Command> + use<'_> + { + self.queue.drain(..) + } +} + +impl Default for CommandQueue +{ + fn default() -> Self + { + CommandQueue { queue: VecDeque::with_capacity(100) } + } +} + +#[tracing::instrument(skip_all)] +fn enqueue_commands_from_render_passes( + window_surface_query: Query<(&mut Surface, &Window, With<TargetWindow>)>, + mut command_queue: Single<CommandQueue>, + mut render_passes: Single<RenderPasses>, + mut active_draw_props: Local<ActiveDrawProperties>, +) +{ + let Ok(command_queue) = command_queue.get_mut() else { + unreachable!(); + }; + + let Ok(render_passes) = render_passes.get_mut() else { + unreachable!(); + }; + + let mut last_render_pass_draw_props = active_draw_props.draw_properties.clone(); + + for (mut window_surface, window) in &window_surface_query { + if window_surface.size != window.inner_size { + command_queue.push(Command::SetSurfaceSize( + window_surface.id, + window.inner_size, + )); + + window_surface.size = window.inner_size.clone(); + } + } + + for render_pass in render_passes.passes.drain(..) { + if render_pass.draw_properties != last_render_pass_draw_props { + command_queue.push(Command::UpdateDrawProperties( + render_pass.draw_properties.clone(), + DrawPropertiesUpdateFlags::all(), + )); + + last_render_pass_draw_props = render_pass.draw_properties; + } + + let last_updated_draw_props = render_pass + .commands + .iter() + .filter_map(|command| match command { + Command::UpdateDrawProperties(draw_props, _) => Some(draw_props.clone()), + _ => None, + }) + .last(); + + command_queue.queue.extend(render_pass.commands); + + if let Some(last_updated_draw_props) = last_updated_draw_props { + last_render_pass_draw_props = last_updated_draw_props; + } + } + + active_draw_props.draw_properties = last_render_pass_draw_props; + + for (window_surface, _) in &window_surface_query { + command_queue.push(Command::SwapBuffers(window_surface.id)); + } +} + +#[tracing::instrument(skip_all)] +fn handle_window_removed( + observe: Observe<Pair<Removed, Window>>, + mut command_queue: Single<CommandQueue>, + mut actions: Actions, +) +{ + let Ok(command_queue) = command_queue.get_mut() else { + unreachable!(); + }; + + for evt_match in &observe { + let window_ent_id = evt_match.entity_id(); + + let window_ent = evt_match.get_entity(); + + if window_ent.get::<TargetWindow>().is_none() { + continue; + } + + tracing::debug!( + entity_id = %window_ent_id, + title = %evt_match.get_ent_target_comp().title, + "Handling removal of window" + ); + + let Some(window_surface) = window_ent.get::<Surface>() else { + continue; + }; + + actions.remove_comps::<(Surface,)>(window_ent_id); + + command_queue + .queue + .push_front(Command::RemoveSurface(window_surface.id)); + } +} + +#[derive(Debug, Default, Clone, Component)] +struct ActiveDrawProperties +{ + pub draw_properties: DrawProperties, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] +pub enum RgbTextureDataType +{ + UnsignedByte, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] +pub enum RgbaTextureDataType +{ + UnsignedByte, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] +pub enum SrgbTextureDataType +{ + UnsignedByte, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] +pub enum SrgbaTextureDataType +{ + UnsignedByte, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] +pub enum DepthTextureDataType +{ + Float32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] +pub enum TexturePixelDataFormat +{ + Rgb(RgbTextureDataType), + Rgba(RgbaTextureDataType), + Srgb(SrgbTextureDataType), + Srgba(SrgbaTextureDataType), + Depth(DepthTextureDataType), +} + +impl TexturePixelDataFormat +{ + pub fn for_image(image: &Image) -> Option<Self> + { + let is_srgb = image.color_space_is_srgb(); + + match image.color_type() { + ImageColorType::Rgb8 if is_srgb => Some(TexturePixelDataFormat::Srgb( + SrgbTextureDataType::UnsignedByte, + )), + ImageColorType::Rgb8 => Some(TexturePixelDataFormat::Rgb( + RgbTextureDataType::UnsignedByte, + )), + ImageColorType::Rgba8 if is_srgb => Some(TexturePixelDataFormat::Srgba( + SrgbaTextureDataType::UnsignedByte, + )), + ImageColorType::Rgba8 => Some(TexturePixelDataFormat::Rgba( + RgbaTextureDataType::UnsignedByte, + )), + _ => None, + } + } +} + +/// Index of a color texture attached to a framebuffer. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct FramebufferColorTextureIndex(pub u8); + +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct FramebufferProperties +{ + pub depth_texture: Option<FramebufferTexture>, + pub color_draw_buffer: Option<FramebufferColorTextureIndex>, +} + +bitflags! { + #[derive(Debug, Default, Clone)] + pub struct FramebufferPropertiesUpdateFlags: u8 + { + const DEPTH_TEXTURE = 1 << 0; + const COLOR_DRAW_BUFFER = 1 << 1; + } +} + +#[derive(Debug, Clone)] +pub struct FramebufferTexture +{ + pub object_id: ObjectId, + pub mipmap_level: u16, +} diff --git a/engine/src/rendering/backend.rs b/engine/src/rendering/backend.rs new file mode 100644 index 0000000..d69b338 --- /dev/null +++ b/engine/src/rendering/backend.rs @@ -0,0 +1,9 @@ +use ecs::extension::Extension; + +pub mod opengl; + +/// Returns the default rendering backend. +pub fn get_default() -> impl Extension +{ + self::opengl::Extension::default() +} diff --git a/engine/src/rendering/backend/opengl.rs b/engine/src/rendering/backend/opengl.rs new file mode 100644 index 0000000..5b009ca --- /dev/null +++ b/engine/src/rendering/backend/opengl.rs @@ -0,0 +1,2060 @@ +//! OpenGL rendering backend. + +use std::borrow::Cow; +use std::collections::HashMap; +use std::convert::Infallible; +use std::hint::cold_path; +use std::num::NonZero; + +use ecs::query::term::With; +use glutin::config::Config as GlutinConfig; +use glutin::display::GetGlDisplay; +use glutin::error::Error as GlutinError; +use glutin::prelude::{GlDisplay, PossiblyCurrentGlContext}; +use glutin::surface::{ + GlSurface as _, + Surface as GlutinSurface, + WindowSurface as GlutinWindowSurface, +}; +use intmap::IntMap; +use opengl_bindings::blending::{ + configure as gl_blending_configure, + Configuration as GlBlendingConfig, + Equation as GlBlendingEquation, + Factor as GlBlendingFactor, +}; +use opengl_bindings::buffer::{ + BindingTarget as GlBufferBindingTarget, + Buffer as GlBuffer, +}; +use opengl_bindings::debug::{ + set_debug_message_callback, + set_debug_message_control, + MessageIdsAction, + MessageSeverity, + MessageSource, + MessageType, + SetDebugMessageControlError as GlSetDebugMessageControlError, +}; +use opengl_bindings::framebuffer::{ + bind as gl_bind_framebuffer, + Attachment as GlFramebufferAttachment, + ColorAttachment as GlFramebufferColorAttachment, + Framebuffer as GlFramebuffer, + Target as GlFramebufferTarget, +}; +use opengl_bindings::misc::{ + clear_buffers, + define_scissor_box as gl_define_scissor_box, + enable, + get_viewport as gl_get_viewport, + set_depth_function as gl_set_depth_function, + set_enabled, + set_face_culling_mode as gl_set_face_culling_mode, + set_viewport as gl_set_viewport, + BufferClearMask as GlBufferClearMask, + Capability, + DepthFunction as GlDepthFunction, + FaceCullingMode as GlFaceCullingMode, + PolygonMode as GlPolygonMode, + PolygonModeFace as GlPolygonModeFace, +}; +use opengl_bindings::shader::{ + Error as GlShaderError, + Kind as ShaderKind, + Program as GlShaderProgram, + Shader as GlShader, +}; +use opengl_bindings::texture::{ + CubeMapFace as GlCubeMapTextureFace, + Error as GlTextureError, + Filtering as GlTextureFiltering, + PixelDataFormat as GlTexturePixelDataFormat, + Texture as GlTexture, + Wrapping as GlTextureWrapping, +}; +use opengl_bindings::vertex_array::{ + DrawError as GlDrawError, + PrimitiveKind, + VertexArray, +}; +use opengl_bindings::{ + MakeContextCurrentError as GlMakeContextCurrentError, + MaybeCurrentContextWithFns, +}; +use raw_window_handle::WindowHandle; +use safer_ffi::layout::ReprC; +use zerocopy::IntoBytes; + +use crate::asset::Assets; +use crate::data_types::color::Color; +use crate::data_types::dimens::Dimens; +use crate::draw_flags::{PolygonMode, PolygonModeFace}; +use crate::ecs::actions::Actions; +use crate::ecs::query::term::Without; +use crate::ecs::sole::Single; +use crate::ecs::{Component, Query, Sole}; +use crate::reflection::EnumReflectionExt; +use crate::rendering::backend::opengl::glutin_compat::{ + DisplayBuilder, + Error as GlutinCompatError, +}; +use crate::rendering::backend::opengl::graphics_mesh::GraphicsMesh; +use crate::rendering::blending::{ + Equation as BlendingEquation, + Factor as BlendingFactor, +}; +use crate::rendering::object::{ + Id as ObjectId, + Kind as ObjectKind, + Object, + RawValue as ObjectRawValue, + Store as ObjectStore, +}; +use crate::rendering::shader::cursor::{ + Binding as ShaderBinding, + BindingLocation as ShaderBindingLocation, + BindingValue as ShaderBindingValue, +}; +use crate::rendering::shader::{ + Error as ShaderError, + Program as ShaderProgram, + ProgramMetadata as ShaderProgramMetadata, + Stage as ShaderStage, +}; +use crate::rendering::{ + AssetOrValue, + BufferClearMask, + Command, + CommandQueue, + DepthFunction, + DrawMeshOptions, + DrawProperties, + DrawPropertiesUpdateFlags, + FaceCullingMode, + FramebufferProperties, + FramebufferPropertiesUpdateFlags, + GraphicsProperties, + Surface, + SurfaceId, + TargetWindow, + TextureCreation, + TexturePixelDataFormat, + TextureUpdate, + POST_RENDER_PHASE, + RENDER_PHASE, +}; +use crate::texture::{ + Border as TextureBorder, + CubeMapFace as CubeMapTextureFace, + Filtering as TextureFiltering, + Properties as TextureProperties, + Wrapping as TextureWrapping, +}; +use crate::util::OptionExt; +use crate::vector::{Vec2, Vec3}; +use crate::windowing::dpi::PhysicalSize; +use crate::windowing::window::{ + Closed as WindowClosed, + CreationAttributes as WindowCreationAttributes, + CreationReady, + Window, +}; +use crate::windowing::Context as WindowingContext; + +mod glutin_compat; +mod graphics_mesh; + +#[derive(Debug, Component)] +struct WindowGlConfig +{ + gl_config: GlutinConfig, +} + +#[derive(Sole, Default)] +struct GraphicsContext +{ + gl_context: Option<MaybeCurrentContextWithFns>, + surfaces: HashMap<SurfaceId, GraphicsContextSurface>, + backend_resources: BackendResourceStore, +} + +#[derive(Debug, Default)] +struct BackendResourceStore +{ + inner: IntMap<BackendResourceId, BackendResource>, + next_id: BackendResourceId, +} + +impl BackendResourceStore +{ + fn try_create_resource<Err>( + &mut self, + object_store: &mut ObjectStore, + object_id: ObjectId, + resource_fn: impl FnOnce() -> Result<BackendResource, Err>, + ) -> Result<(), Err> + { + if object_store.contains_non_pending_with_id(&object_id) { + cold_path(); + tracing::error!( + ?object_id, + "Object store already contains a object with this ID" + ); + return Ok(()); + } + + let resource = resource_fn()?; + + let res_id = self.next_id; + + self.next_id.0 += 1; + + let object_kind = resource.object_kind(); + + self.inner.insert(res_id, resource); + + object_store.insert(object_id, Object::from_raw(res_id.0, object_kind)); + + Ok(()) + } + + fn get_resource( + &self, + object_store: &ObjectStore, + object_id: ObjectId, + object_kind: ObjectKind, + ) -> Option<&BackendResource> + { + let Some(object) = object_store.get_obj(&object_id) else { + cold_path(); + tracing::error!( + ?object_id, + %object_kind, + "Object does not exist in the rendering object store" + ); + return None; + }; + + if object.kind() != object_kind { + cold_path(); + tracing::error!( + ?object_id, + expected_object_kind = %object_kind, + found_object_kind = %object.kind(), + "Unexpected object kind of object in object store" + ); + return None; + } + + let resource_id = BackendResourceId(object.as_raw()); + + let Some(resource) = self.inner.get(resource_id) else { + cold_path(); + tracing::error!( + ?object_id, + %object_kind, + ?resource_id, + "Backend resource does not exist" + ); + return None; + }; + + debug_assert_eq!(resource.object_kind(), object_kind); + + Some(resource) + } + + fn get_resource_mut( + &mut self, + object_store: &ObjectStore, + object_id: ObjectId, + object_kind: ObjectKind, + ) -> Option<&mut BackendResource> + { + let Some(object) = object_store.get_obj(&object_id) else { + cold_path(); + tracing::error!( + ?object_id, + %object_kind, + "Object does not exist in the rendering object store" + ); + return None; + }; + + if object.kind() != object_kind { + cold_path(); + tracing::error!( + ?object_id, + expected_object_kind = %object_kind, + found_object_kind = %object.kind(), + "Unexpected object kind of object in object store" + ); + return None; + } + + let resource_id = BackendResourceId(object.as_raw()); + + let Some(resource) = self.inner.get_mut(resource_id) else { + cold_path(); + tracing::error!( + ?object_id, + %object_kind, + ?resource_id, + "Backend resource does not exist" + ); + return None; + }; + + debug_assert_eq!(resource.object_kind(), object_kind); + + Some(resource) + } + + fn remove_resource( + &mut self, + object_store: &mut ObjectStore, + object_id: ObjectId, + object_kind: ObjectKind, + curr_gl_ctx: &MaybeCurrentContextWithFns, + ) + { + if let Some(object) = object_store.get_obj(&object_id) { + if object.kind() != object_kind { + cold_path(); + tracing::error!( + ?object_id, + expected_object_kind = %object_kind, + found_object_kind = %object.kind(), + "Unexpected object kind" + ); + return; + } + } + + let Some(object) = object_store.remove(&object_id).flatten() else { + tracing::error!( + ?object_id, + %object_kind, + "Object does not exist in the rendering object store" + ); + return; + }; + + let resource_id = BackendResourceId(object.as_raw()); + + let Some(resource) = self.inner.remove(resource_id) else { + cold_path(); + tracing::error!( + ?object_id, + %object_kind, + ?resource_id, + "Backend resource does not exist" + ); + return; + }; + + resource.destroy(curr_gl_ctx); + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct BackendResourceId(ObjectRawValue); + +impl intmap::IntKey for BackendResourceId +{ + type Int = ObjectRawValue; + + const PRIME: Self::Int = <ObjectRawValue as intmap::IntKey>::PRIME; + + fn into_int(self) -> Self::Int + { + self.0.into_int() + } +} + +#[derive(Debug)] +struct GraphicsContextSurface +{ + window_surface: GlutinSurface<GlutinWindowSurface>, + size: PhysicalSize<u32>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct ShaderBindingIndex(u32); + +impl intmap::IntKey for ShaderBindingIndex +{ + type Int = u32; + + const PRIME: Self::Int = <u32 as intmap::IntKey>::PRIME; + + fn into_int(self) -> Self::Int + { + self.0 + } +} + +#[derive(Debug)] +enum BackendShaderBinding +{ + Uniform(GlBuffer<u8>), + Texture(GlTexture), +} + +impl BackendShaderBinding +{ + fn bind( + &self, + gl_context: &MaybeCurrentContextWithFns, + binding_index: ShaderBindingIndex, + ) + { + match self { + BackendShaderBinding::Uniform(uniform_buf) => { + uniform_buf.bind_to_indexed_target( + gl_context, + GlBufferBindingTarget::UniformBuffer, + binding_index.0, + ); + } + BackendShaderBinding::Texture(texture) => { + texture.bind_to_texture_unit(gl_context, binding_index.0) + } + } + } +} + +#[derive(Debug)] +enum BackendResource +{ + Mesh + { + mesh: GraphicsMesh, + vertex_attrs_updated_for_shader: Option<ObjectId>, + }, + Shader + { + program: GlShaderProgram, + program_metadata: ShaderProgramMetadata, + bindings: IntMap<ShaderBindingIndex, BackendShaderBinding>, + }, + Texture + { + texture: GlTexture, + pixel_data_format: TexturePixelDataFormat, + kind: TextureKind, + }, + Framebuffer + { + framebuffer: GlFramebuffer + }, +} + +impl BackendResource +{ + fn object_kind(&self) -> ObjectKind + { + match self { + Self::Mesh { .. } => ObjectKind::Mesh, + Self::Shader { .. } => ObjectKind::ShaderProgram, + Self::Texture { .. } => ObjectKind::Texture, + Self::Framebuffer { .. } => ObjectKind::Framebuffer, + } + } + + fn destroy(&self, curr_gl_ctx: &MaybeCurrentContextWithFns) + { + match self { + Self::Mesh { mesh, .. } => { + mesh.destroy(curr_gl_ctx); + } + Self::Shader { program, .. } => { + program.clone().delete(curr_gl_ctx); + } + Self::Texture { texture, .. } => { + texture.clone().delete(curr_gl_ctx); + } + Self::Framebuffer { framebuffer } => { + framebuffer.clone().delete(curr_gl_ctx); + } + } + } +} + +#[derive(Debug)] +enum TextureKind +{ + Texture2D, + CubeMap, +} + +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct Extension {} + +impl crate::ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: crate::ecs::extension::Collector<'_>) + { + collector.add_system(*RENDER_PHASE, handle_commands); + + collector.add_system(*POST_RENDER_PHASE, prepare_windows); + collector.add_system(*POST_RENDER_PHASE, init_window_graphics); + + let _ = collector.add_sole(GraphicsContext::default()); + } +} + +fn prepare_windows( + window_query: Query<( + Option<&Window>, + &mut WindowCreationAttributes, + With<TargetWindow>, + Without<CreationReady>, + Without<WindowGlConfig>, + Without<WindowClosed>, + )>, + windowing_context: Single<WindowingContext>, + graphics_props: Single<GraphicsProperties>, + mut actions: Actions, +) -> Result<(), crate::Error> +{ + let windowing_context = windowing_context.get()?; + let graphics_props = graphics_props.get()?; + + let Some(display_handle) = windowing_context.display_handle() else { + return Ok(()); + }; + + for (window_ent_id, (window, mut window_creation_attrs)) in + window_query.iter_with_euids() + { + tracing::debug!("Preparing window entity {window_ent_id} for use in rendering"); + + let mut glutin_config_template_builder = + glutin::config::ConfigTemplateBuilder::new(); + + if let Some(multisampling_sample_cnt) = graphics_props.multisampling_sample_cnt { + glutin_config_template_builder = glutin_config_template_builder + .with_multisampling(multisampling_sample_cnt); + } + + let window_handle = match window + .as_ref() + .map(|window| unsafe { + windowing_context.get_window_as_handle(&window.wid()) + }) + .flatten() + .transpose() + { + Ok(window_handle) => window_handle, + Err(err) => { + tracing::error!("Failed to get window handle: {err}"); + continue; + } + }; + + let (new_window_creation_attrs, gl_config) = match DisplayBuilder::new() + .with_window_attributes(window_creation_attrs.clone()) + .build( + window_handle, + &display_handle, + glutin_config_template_builder, + |mut cfgs| cfgs.next(), + ) { + Ok((new_window_creation_attrs, gl_config)) => { + (new_window_creation_attrs, gl_config) + } + Err(GlutinCompatError::WindowRequired) => { + actions.add_components(window_ent_id, (CreationReady,)); + continue; + } + Err(err) => { + tracing::error!("Failed to create platform graphics display: {err}"); + continue; + } + }; + + *window_creation_attrs = new_window_creation_attrs; + + actions.add_components(window_ent_id, (WindowGlConfig { gl_config },)); + + if window.is_none() { + actions.add_components(window_ent_id, (CreationReady,)); + } + } + + Ok(()) +} + +#[tracing::instrument(skip_all)] +fn init_window_graphics( + window_query: Query<( + &Window, + &WindowGlConfig, + With<TargetWindow>, + Without<Surface>, + )>, + windowing_context: Single<WindowingContext>, + graphics_props: Single<GraphicsProperties>, + mut graphics_ctx: Single<GraphicsContext>, + mut actions: Actions, +) -> Result<(), crate::Error> +{ + let Ok(graphics_ctx) = graphics_ctx.get_mut() else { + unreachable!(); + }; + + let windowing_context = windowing_context.get()?; + let graphics_props = graphics_props.get()?; + + for (window_ent_id, (window, window_gl_config)) in window_query.iter_with_euids() { + tracing::info!( + window_entity_id=%window_ent_id, + window_title=&*window.title, + "Initializing graphics for window" + ); + + let display = window_gl_config.gl_config.display(); + + let window_handle = + match unsafe { windowing_context.get_window_as_handle(&window.wid()) } + .transpose() + { + Ok(Some(window_handle)) => window_handle, + Ok(None) => { + tracing::error!( + wid = ?window.wid(), + entity_id = %window_ent_id, + "Windowing context does not contain window" + ); + continue; + } + Err(err) => { + tracing::error!("Failed to get window handle: {err}"); + continue; + } + }; + + let Ok(window_inner_size) = + PhysicalSize::<NonZero<u32>>::try_convert_from(window.inner_size.clone()) + else { + tracing::error!( + "Cannot create a surface for a window with a width/height of 0", + ); + continue; + }; + + let window_surface = match unsafe { + display.create_window_surface( + &window_gl_config.gl_config, + &glutin::surface::SurfaceAttributesBuilder::< + glutin::surface::WindowSurface, + >::new() + .build( + window_handle.as_raw(), + window_inner_size.width, + window_inner_size.height, + ), + ) + } { + Ok(window_surface) => window_surface, + Err(err) => { + tracing::error!("Failed to create window surface: {err}"); + continue; + } + }; + + let gl_context = match graphics_ctx.gl_context.get_or_try_insert_with_fn(|| { + create_gl_context( + &window_gl_config.gl_config, + &graphics_props, + window_handle, + &window_surface, + ) + }) { + Ok(gl_context) => gl_context, + Err(err) => { + tracing::error!("Failed to create GL context: {err}"); + continue; + } + }; + + if let Err(err) = gl_context.make_current(&window_surface) { + tracing::error!("Failed to make GL context current: {err}"); + continue; + }; + + if let Err(err) = gl_set_viewport( + &gl_context, + &Vec2 { x: 0, y: 0 }.into(), + &opengl_bindings::data_types::Dimens { + width: window.inner_size.width, + height: window.inner_size.height, + }, + ) { + tracing::error!("Failed to set viewport: {err}"); + } + + set_enabled( + &gl_context, + Capability::DepthTest, + graphics_props.depth_test, + ); + + set_enabled( + &gl_context, + Capability::MultiSample, + graphics_props.multisampling_sample_cnt.is_some(), + ); + + if graphics_props.debug { + enable(&gl_context, Capability::DebugOutput); + enable(&gl_context, Capability::DebugOutputSynchronous); + + set_debug_message_callback(&gl_context, opengl_debug_message_cb); + + match set_debug_message_control( + &gl_context, + None, + None, + None, + &[], + MessageIdsAction::Disable, + ) { + Ok(()) => {} + Err(GlSetDebugMessageControlError::TooManyIds { + id_cnt: _, + max_id_cnt: _, + }) => { + unreachable!() // No ids are given + } + } + } + + let surface_id = SurfaceId::new_unique(); + + actions.add_components( + window_ent_id, + (Surface { + id: surface_id, + size: window.inner_size.clone(), + },), + ); + + graphics_ctx.surfaces.insert( + surface_id, + GraphicsContextSurface { + window_surface, + size: window.inner_size.clone(), + }, + ); + } + + Ok(()) +} + +#[tracing::instrument(skip_all)] +fn handle_commands( + mut graphics_ctx: Single<GraphicsContext>, + mut object_store: Single<ObjectStore>, + mut command_queue: Single<CommandQueue>, + assets: Single<Assets>, +) -> Result<(), crate::Error> +{ + let Ok(graphics_ctx) = graphics_ctx.get_mut() else { + unreachable!(); + }; + + let object_store = object_store.get_mut()?; + let command_queue = command_queue.get_mut()?; + let assets = assets.get()?; + + let GraphicsContext { + ref gl_context, + ref mut surfaces, + ref mut backend_resources, + } = *graphics_ctx; + + let Some(gl_context) = gl_context else { + return Ok(()); + }; + + let mut activated_gl_shader_program: Option<( + ObjectId, + GlShaderProgram, + ShaderProgramMetadata, + )> = None; + + for command in command_queue.drain() { + let tracing_span = tracing::info_span!( + "handle_cmd", + command = %command.get_variant_reflection().name, + ); + let _tracing_span_enter = tracing_span.enter(); + + match command { + Command::RemoveSurface(surface_id) => { + let Some(surface) = surfaces.remove(&surface_id) else { + tracing::error!(surface_id=?surface_id, "Surface does not exist"); + continue; + }; + + if surface.window_surface.is_current(gl_context.context()) { + if let Err(err) = gl_context.context().make_not_current_in_place() { + tracing::error!("Failed to make GL context not current: {err}"); + } + + if let Err(err) = gl_context.make_current_surfaceless() { + tracing::error!("Failed to make GL context current: {err}"); + } + } + + drop(surface); + } + Command::MakeCurrent(surface_id) => { + let Some(surface) = surfaces.get(&surface_id) else { + tracing::error!(surface_id=?surface_id, "Surface does not exist"); + continue; + }; + + if let Err(err) = gl_context.make_current(&surface.window_surface) { + tracing::error!("Failed to make graphics context current: {err}"); + continue; + } + + if let Err(err) = gl_set_viewport( + gl_context, + &Vec2 { x: 0, y: 0 }.into(), + &opengl_bindings::data_types::Dimens { + width: surface.size.width, + height: surface.size.height, + }, + ) { + tracing::error!("Failed to set viewport: {err}"); + } + } + Command::SetSurfaceSize(surface_id, new_surface_size) => { + let Some(surface) = surfaces.get_mut(&surface_id) else { + tracing::error!(surface_id=?surface_id, "Surface does not exist"); + continue; + }; + + surface.size = new_surface_size; + + if !surface.window_surface.is_current(gl_context.context()) { + continue; + } + + if let Err(err) = gl_set_viewport( + gl_context, + &Vec2 { x: 0, y: 0 }.into(), + &opengl_bindings::data_types::Dimens { + width: surface.size.width, + height: surface.size.height, + }, + ) { + tracing::error!("Failed to set viewport: {err}"); + } + } + Command::ClearBuffers(buffer_clear_mask) => { + let mut clear_mask = GlBufferClearMask::empty(); + + clear_mask.set( + GlBufferClearMask::COLOR, + buffer_clear_mask.contains(BufferClearMask::COLOR), + ); + + clear_mask.set( + GlBufferClearMask::DEPTH, + buffer_clear_mask.contains(BufferClearMask::DEPTH), + ); + + clear_mask.set( + GlBufferClearMask::STENCIL, + buffer_clear_mask.contains(BufferClearMask::STENCIL), + ); + + clear_buffers(gl_context, clear_mask); + } + Command::SwapBuffers(surface_id) => { + let Some(surface) = surfaces.get(&surface_id) else { + tracing::error!(surface_id=?surface_id, "Surface does not exist"); + continue; + }; + + if let Err(err) = + surface.window_surface.swap_buffers(gl_context.context()) + { + tracing::error!("Failed to swap buffers: {err}"); + } + } + Command::CreateShaderProgram(object_id, shader_program) => { + if let Err(err) = backend_resources + .try_create_resource::<CreateShaderError>( + object_store, + object_id, + || { + Ok(BackendResource::Shader { + program: create_shader_program( + gl_context, + &shader_program, + )?, + program_metadata: shader_program.metadata().clone(), + bindings: IntMap::with_capacity(4), + }) + }, + ) + { + tracing::error!("Failed to create shader program: {err}"); + } + } + Command::ActivateShader(shader_object_id) => { + let Some(resource) = backend_resources.get_resource( + object_store, + shader_object_id, + ObjectKind::ShaderProgram, + ) else { + continue; + }; + + let BackendResource::Shader { program, program_metadata, bindings } = + resource + else { + unreachable!(); + }; + + program.activate(gl_context); + + for (binding_index, binding) in bindings.iter() { + binding.bind(gl_context, binding_index); + } + + activated_gl_shader_program = + Some((shader_object_id, program.clone(), program_metadata.clone())); + } + Command::SetShaderBinding( + shader_object_id, + ShaderBinding { + location: binding_location, + value: binding_value, + }, + ) => { + let binding = set_shader_binding( + backend_resources, + object_store, + gl_context, + shader_object_id.clone(), + &binding_location, + binding_value, + ); + + if let Some(binding) = binding { + if Some(shader_object_id) + == activated_gl_shader_program + .as_ref() + .map(|(obj_id, ..)| *obj_id) + { + binding.bind( + gl_context, + ShaderBindingIndex(binding_location.binding_index), + ); + } + } + } + Command::CreateTexture { + obj_id, + pixel_data_format, + creation, + properties, + } => { + let kind = match &creation { + TextureCreation::Texture2D { .. } => TextureKind::Texture2D, + TextureCreation::CubeMap { .. } => TextureKind::CubeMap, + }; + + if let Err(err) = backend_resources + .try_create_resource::<opengl_bindings::texture::Error>( + object_store, + obj_id, + || { + let gl_texture = create_gl_texture( + gl_context, + creation, + pixel_data_format, + &properties, + )?; + + Ok(BackendResource::Texture { + texture: gl_texture, + pixel_data_format, + kind, + }) + }, + ) + { + tracing::error!("Failed to create texture: {err}"); + } + } + Command::UpdateTexture { + obj_id, + pixels, + pixel_data_format, + update, + } => { + let Some(texture_resource) = backend_resources.get_resource( + object_store, + obj_id, + ObjectKind::Texture, + ) else { + continue; + }; + + let BackendResource::Texture { + texture: gl_texture, + pixel_data_format: tex_pixel_data_format, + kind: tex_metadata, + } = texture_resource + else { + unreachable!(); + }; + + if !matches!( + (&update, tex_metadata), + (TextureUpdate::Texture2D { .. }, TextureKind::Texture2D) + | (TextureUpdate::CubeMap { .. }, TextureKind::CubeMap) + ) { + tracing::error!( + texture_object_id = ?obj_id, + "Texture is of incorrect kind" + ); + continue; + } + + if &pixel_data_format != tex_pixel_data_format { + cold_path(); + tracing::error!( + texture_object_id = ?obj_id, + expected_pixel_data_format = ?*tex_pixel_data_format, + found_pixel_data_format = ?pixel_data_format, + "Texture has incorrect pixel data format" + ); + continue; + } + + if let Err(err) = update_texture_object( + gl_context, + gl_texture, + pixels, + pixel_data_format, + update, + ) { + tracing::error!("Failed to update texture object: {err}"); + } + } + Command::RemoveTexture(object_id) => { + backend_resources.remove_resource( + object_store, + object_id, + ObjectKind::Texture, + gl_context, + ); + } + Command::CreateFramebuffer(object_id) => { + backend_resources.try_create_resource::<Infallible>( + object_store, + object_id, + || { + Ok(BackendResource::Framebuffer { + framebuffer: GlFramebuffer::new(gl_context), + }) + }, + ); + } + Command::RemoveFramebuffer(object_id) => { + backend_resources.remove_resource( + object_store, + object_id, + ObjectKind::Framebuffer, + gl_context, + ); + } + Command::UpdateFramebufferProperties { + object_id, + properties, + update_flags, + } => { + update_framebuffer_properties( + backend_resources, + object_store, + gl_context, + object_id, + properties, + update_flags, + ); + } + Command::CreateMesh { obj_id, mesh, usage: mesh_usage } => { + let mesh = match &mesh { + AssetOrValue::Asset(mesh_asset) => { + let Some(mesh) = assets.get(&mesh_asset) else { + tracing::error!( + asset_id=?mesh_asset.id(), + "Mesh asset does not exist" + ); + continue; + }; + + mesh + } + AssetOrValue::Value(mesh) => mesh, + }; + + if let Err(err) = backend_resources + .try_create_resource::<graphics_mesh::Error>( + object_store, + obj_id, + || { + Ok(BackendResource::Mesh { + mesh: GraphicsMesh::new(gl_context, &mesh, mesh_usage)?, + vertex_attrs_updated_for_shader: None, + }) + }, + ) + { + tracing::error!("Failed to create mesh: {err}"); + } + } + Command::UpdateMesh { obj_id, mesh, usage: mesh_usage } => { + let Some(resource) = backend_resources.get_resource_mut( + object_store, + obj_id, + ObjectKind::Mesh, + ) else { + continue; + }; + + let BackendResource::Mesh { mesh: graphics_mesh, .. } = resource else { + unreachable!(); + }; + + if let Err(err) = graphics_mesh.update(gl_context, &mesh, mesh_usage) { + tracing::error!("Failed to update mesh: {err}"); + } + } + Command::RemoveMesh(object_id) => { + backend_resources.remove_resource( + object_store, + object_id, + ObjectKind::Mesh, + gl_context, + ); + } + Command::DrawMesh(object_id, draw_mesh_opts) => { + let Some(resource) = backend_resources.get_resource_mut( + object_store, + object_id, + ObjectKind::Mesh, + ) else { + continue; + }; + + let BackendResource::Mesh { + mesh: graphics_mesh, + vertex_attrs_updated_for_shader, + } = resource + else { + unreachable!(); + }; + + let Some((shader_program_obj_id, _, shader_program_metadata)) = + &activated_gl_shader_program + else { + tracing::error!("No shader program is activated"); + continue; + }; + + if vertex_attrs_updated_for_shader.is_none_or( + |vertex_attrs_updated_for_shader| { + vertex_attrs_updated_for_shader != *shader_program_obj_id + }, + ) { + let Some(shader_program_vertex_desc) = + &shader_program_metadata.vertex_desc + else { + tracing::error!( + "Activated shader program does not have a vertex description" + ); + continue; + }; + + if let Err(err) = graphics_mesh.update_vertex_attrs_for_shader( + gl_context, + shader_program_vertex_desc, + ) { + tracing::error!("Cannot draw mesh: {err}"); + continue; + } + + *vertex_attrs_updated_for_shader = Some(*shader_program_obj_id); + } + + if let Err(err) = draw_mesh(gl_context, graphics_mesh, &draw_mesh_opts) { + tracing::error!("Failed to draw mesh: {err}"); + }; + } + Command::UpdateDrawProperties(properties, update_flags) => { + update_draw_properties( + backend_resources, + object_store, + gl_context, + properties, + update_flags, + ); + } + } + } + + Ok(()) +} + +fn create_gl_context( + gl_config: &GlutinConfig, + graphics_props: &GraphicsProperties, + window_handle: WindowHandle<'_>, + surface: &GlutinSurface<GlutinWindowSurface>, +) -> Result<MaybeCurrentContextWithFns, CreateGlContextError> +{ + let display = gl_config.display(); + + let glutin_context = unsafe { + display.create_context( + gl_config, + &glutin::context::ContextAttributesBuilder::new() + .with_debug(graphics_props.debug) + .build(Some(window_handle.as_raw())), + ) + } + .map_err(CreateGlContextError::CreateGlutinContext)?; + + MaybeCurrentContextWithFns::new(glutin_context, &surface) + .map_err(CreateGlContextError::MakeContextCurrent) +} + +#[derive(Debug, thiserror::Error)] +enum CreateGlContextError +{ + #[error("Glutin context creation failed")] + CreateGlutinContext(#[source] GlutinError), + + #[error("Making GL context current failed")] + MakeContextCurrent(#[source] GlMakeContextCurrentError), +} + +#[tracing::instrument(skip_all)] +fn update_texture_object( + curr_gl_ctx: &MaybeCurrentContextWithFns, + gl_texture: &GlTexture, + pixels: Box<[u8]>, + pixel_data_format: TexturePixelDataFormat, + update: TextureUpdate, +) -> Result<(), GlTextureError> +{ + match update { + TextureUpdate::Texture2D { size, offset } => { + gl_texture.store_image_2d( + curr_gl_ctx, + 0, + offset.into(), + size.into(), + tex_pixel_data_format_into_gl(pixel_data_format), + &pixels, + )?; + } + TextureUpdate::CubeMap { size, offset, face } => { + gl_texture.store_image_3d( + curr_gl_ctx, + 0, + Vec3 { + x: offset.x, + y: offset.y, + z: cube_map_texture_face_to_gl(face) as u32, + } + .into(), + [size.width, size.height, 1], + tex_pixel_data_format_into_gl(pixel_data_format), + &pixels, + )?; + } + } + + gl_texture.generate_mipmap(curr_gl_ctx); + + Ok(()) +} + +#[tracing::instrument(skip_all)] +fn update_draw_properties( + backend_resources: &BackendResourceStore, + object_store: &ObjectStore, + gl_context: &MaybeCurrentContextWithFns, + properties: DrawProperties, + update_flags: DrawPropertiesUpdateFlags, +) +{ + if update_flags.contains(DrawPropertiesUpdateFlags::POLYGON_MODE_CONFIG) { + opengl_bindings::misc::set_polygon_mode( + gl_context, + polygon_mode_face_to_gl(properties.polygon_mode_config.face), + polygon_mode_to_gl(properties.polygon_mode_config.mode), + ); + } + + if update_flags.contains(DrawPropertiesUpdateFlags::BLENDING_ENABLED) { + set_enabled(gl_context, Capability::Blend, properties.blending_enabled); + } + + if update_flags.contains(DrawPropertiesUpdateFlags::BLENDING_CONFIG) { + gl_blending_configure( + gl_context, + GlBlendingConfig::default() + .with_source_factor(blending_factor_to_gl( + properties.blending_config.source_factor, + )) + .with_destination_factor(blending_factor_to_gl( + properties.blending_config.destination_factor, + )) + .with_equation(blending_equation_to_gl( + properties.blending_config.equation, + )), + ); + } + + if update_flags.contains(DrawPropertiesUpdateFlags::DEPTH_TEST_ENABLED) { + set_enabled( + gl_context, + Capability::DepthTest, + properties.depth_test_enabled, + ); + } + + if update_flags.contains(DrawPropertiesUpdateFlags::DEPTH_FUNCTION) { + gl_set_depth_function( + gl_context, + match properties.depth_function { + DepthFunction::Never => GlDepthFunction::Never, + DepthFunction::Less => GlDepthFunction::Less, + DepthFunction::Equal => GlDepthFunction::Equal, + DepthFunction::LessOrEqual => GlDepthFunction::LessOrEqual, + DepthFunction::Greater => GlDepthFunction::Greater, + DepthFunction::NotEqual => GlDepthFunction::NotEqual, + DepthFunction::GreaterOrEqual => GlDepthFunction::GreaterOrEqual, + DepthFunction::Always => GlDepthFunction::Always, + }, + ); + } + + if update_flags.contains(DrawPropertiesUpdateFlags::SCISSOR_TEST_ENABLED) { + set_enabled( + gl_context, + Capability::ScissorTest, + properties.scissor_test_enabled, + ); + } + + if update_flags.contains(DrawPropertiesUpdateFlags::SCISSOR_BOX) { + gl_define_scissor_box( + gl_context, + properties.scissor_box.lower_left_corner_pos.into(), + properties + .scissor_box + .size + .unwrap_or_else(|| { + let (_, viewport_size) = gl_get_viewport(gl_context); + + Dimens::<u16> { + width: viewport_size + .width + .try_into() + .expect("Viewport width too large"), + height: viewport_size + .height + .try_into() + .expect("Viewport height too large"), + } + }) + .into(), + ); + } + + if update_flags.contains(DrawPropertiesUpdateFlags::FACE_CULLING_ENABLED) { + set_enabled( + gl_context, + Capability::CullFace, + properties.face_culling_enabled, + ); + } + + if update_flags.contains(DrawPropertiesUpdateFlags::FACE_CULLING_MODE) { + gl_set_face_culling_mode( + gl_context, + match properties.face_culling_mode { + FaceCullingMode::Front => GlFaceCullingMode::Front, + FaceCullingMode::Back => GlFaceCullingMode::Back, + FaceCullingMode::FrontAndBack => GlFaceCullingMode::FrontAndBack, + }, + ); + } + + 'if_draw_fb: { + if update_flags.contains(DrawPropertiesUpdateFlags::DRAW_FRAMEBUFFER) { + let Some(framebuffer_obj_id) = properties.draw_framebuffer else { + gl_bind_framebuffer(gl_context, GlFramebufferTarget::Draw, None); + break 'if_draw_fb; + }; + + let Some(framebuffer_resource) = backend_resources.get_resource( + object_store, + framebuffer_obj_id, + ObjectKind::Framebuffer, + ) else { + break 'if_draw_fb; + }; + + let BackendResource::Framebuffer { framebuffer } = framebuffer_resource + else { + unreachable!(); + }; + + gl_bind_framebuffer(gl_context, GlFramebufferTarget::Draw, Some(framebuffer)); + } + } + + 'if_read_fb: { + if update_flags.contains(DrawPropertiesUpdateFlags::READ_FRAMEBUFFER) { + let Some(framebuffer_obj_id) = properties.read_framebuffer else { + gl_bind_framebuffer(gl_context, GlFramebufferTarget::Read, None); + break 'if_read_fb; + }; + + let Some(framebuffer_resource) = backend_resources.get_resource( + object_store, + framebuffer_obj_id, + ObjectKind::Framebuffer, + ) else { + break 'if_read_fb; + }; + + let BackendResource::Framebuffer { framebuffer } = framebuffer_resource + else { + unreachable!(); + }; + + gl_bind_framebuffer(gl_context, GlFramebufferTarget::Read, Some(framebuffer)); + } + } +} + +#[tracing::instrument(skip_all)] +fn update_framebuffer_properties( + backend_resources: &BackendResourceStore, + object_store: &ObjectStore, + current_context: &MaybeCurrentContextWithFns, + framebuffer_object_id: ObjectId, + properties: FramebufferProperties, + update_flags: FramebufferPropertiesUpdateFlags, +) +{ + let Some(resource) = backend_resources.get_resource( + object_store, + framebuffer_object_id, + ObjectKind::Framebuffer, + ) else { + return; + }; + + let BackendResource::Framebuffer { framebuffer } = resource else { + unreachable!(); + }; + + 'if_dt: { + if update_flags.contains(FramebufferPropertiesUpdateFlags::DEPTH_TEXTURE) { + let Some(framebuffer_depth_texture) = &properties.depth_texture else { + framebuffer + .detach_texture(current_context, GlFramebufferAttachment::Depth); + + break 'if_dt; + }; + + let Some(depth_texture_resource) = backend_resources.get_resource( + object_store, + framebuffer_depth_texture.object_id, + ObjectKind::Texture, + ) else { + break 'if_dt; + }; + + let BackendResource::Texture { texture: depth_texture, .. } = + depth_texture_resource + else { + unreachable!(); + }; + + framebuffer.attach_texture( + current_context, + GlFramebufferAttachment::Depth, + depth_texture.clone(), + framebuffer_depth_texture.mipmap_level, + ); + } + } + + if update_flags.contains(FramebufferPropertiesUpdateFlags::COLOR_DRAW_BUFFER) { + framebuffer.set_draw_buffer( + current_context, + properties.color_draw_buffer.map(|color_texture_index| { + GlFramebufferColorAttachment(color_texture_index.0) + }), + ); + } +} + +fn set_shader_binding<'backend_resources>( + backend_resources: &'backend_resources mut BackendResourceStore, + object_store: &ObjectStore, + gl_context: &MaybeCurrentContextWithFns, + shader_object_id: ObjectId, + binding_location: &ShaderBindingLocation, + binding_value: ShaderBindingValue, +) -> Option<&'backend_resources BackendShaderBinding> +{ + if let ShaderBindingValue::Texture(texture_object_id, _) = &binding_value { + let gl_texture = match backend_resources.get_resource( + object_store, + *texture_object_id, + ObjectKind::Texture, + ) { + Some(BackendResource::Texture { texture, .. }) => texture.clone(), + Some(_) => unreachable!(), + None => return None, + }; + + let shader_bindings = match backend_resources.get_resource_mut( + object_store, + shader_object_id, + ObjectKind::ShaderProgram, + ) { + Some(BackendResource::Shader { bindings, .. }) => bindings, + Some(_) => unreachable!(), + None => return None, + }; + + if let Some(prev_binding) = + shader_bindings.remove(ShaderBindingIndex(binding_location.binding_index)) + { + // TODO: Textures should probably also be handled somehow here + if let BackendShaderBinding::Uniform(prev_binding_uniform_buf) = prev_binding + { + prev_binding_uniform_buf.delete(gl_context); + } + } + + return Some( + shader_bindings + .entry(ShaderBindingIndex(binding_location.binding_index)) + .set_or_insert_with(|| BackendShaderBinding::Texture(gl_texture.clone())), + ); + + // if Some(shader_object_id) + // == activated_gl_shader_program + // .as_ref() + // .map(|(obj_id, ..)| *obj_id) + // { + // BackendShaderBinding::Texture(gl_texture.clone()).bind( + // gl_context, + // ShaderBindingIndex(binding_location.binding_index), + // ); + // } + } + + let shader_bindings = match backend_resources.get_resource_mut( + object_store, + shader_object_id, + ObjectKind::ShaderProgram, + ) { + Some(BackendResource::Shader { bindings, .. }) => bindings, + Some(_) => unreachable!(), + None => return None, + }; + + let binding = + match shader_bindings.get(ShaderBindingIndex(binding_location.binding_index)) { + Some(binding @ BackendShaderBinding::Uniform(_)) => binding, + Some(_) | None => { + let uniform_buf = GlBuffer::<u8>::new(gl_context); + + uniform_buf + .init( + gl_context, + binding_location.binding_size, + opengl_bindings::buffer::Usage::Dynamic, + ) + .unwrap(); + + shader_bindings + .entry(ShaderBindingIndex(binding_location.binding_index)) + .set_or_insert_with(|| BackendShaderBinding::Uniform(uniform_buf)) + } + }; + + let BackendShaderBinding::Uniform(binding_uniform_buf) = binding else { + unreachable!(); + }; + + let fvec3_value; + let mut color_value = [0.0; 4]; + + binding_uniform_buf + .store_at_byte_offset( + gl_context, + binding_location.byte_offset, + match binding_value { + ShaderBindingValue::Uint(ref value) => value.as_bytes(), + ShaderBindingValue::Int(ref value) => value.as_bytes(), + ShaderBindingValue::Float(ref value) => value.as_bytes(), + ShaderBindingValue::FVec3(value) => { + fvec3_value = [value.x, value.y, value.z]; + fvec3_value.as_bytes() + } + ShaderBindingValue::Color(value) => match value { + Color::Rgb(value) => { + color_value[..3].copy_from_slice(&[value.r, value.g, value.b]); + + color_value[..3].as_bytes() + } + Color::Rgba(value) => { + color_value[..4] + .copy_from_slice(&[value.r, value.g, value.b, value.a]); + + color_value[..4].as_bytes() + } + }, + ShaderBindingValue::FMat4x4(ref value) => value.items().as_bytes(), + ShaderBindingValue::Texture(_, _) => unreachable!(), + }, + ) + .unwrap(); + + Some(binding) + + // if Some(shader_object_id) + // == activated_gl_shader_program + // .as_ref() + // .map(|(obj_id, ..)| *obj_id) + // { + // BackendShaderBinding::Uniform(binding_uniform_buf.clone()).bind( + // gl_context, + // ShaderBindingIndex(binding_location.binding_index), + // ); + // } +} + +fn draw_mesh( + current_context: &MaybeCurrentContextWithFns, + graphics_mesh: &GraphicsMesh, + opts: &DrawMeshOptions, +) -> Result<(), GlDrawError> +{ + graphics_mesh.vertex_arr.bind(current_context); + + if graphics_mesh.index_buffer.is_some() { + VertexArray::draw_elements( + current_context, + opengl_bindings::vertex_array::DrawElementsOptions { + primitive_kind: PrimitiveKind::Triangles, + element_offset: opts.element_offset, + element_cnt: opts.element_cnt.unwrap_or(graphics_mesh.element_cnt), + vertex_offset: opts.vertex_offset, + }, + )?; + } else { + VertexArray::draw_arrays( + current_context, + PrimitiveKind::Triangles, + opts.vertex_offset, + opts.element_cnt.unwrap_or(graphics_mesh.element_cnt), + )?; + } + + Ok(()) +} + +fn tex_pixel_data_format_into_gl( + tex_pixel_data_format: TexturePixelDataFormat, +) -> GlTexturePixelDataFormat +{ + match tex_pixel_data_format { + TexturePixelDataFormat::Rgb(data_type) => { + GlTexturePixelDataFormat::Rgb(match data_type { + crate::rendering::RgbTextureDataType::UnsignedByte => { + opengl_bindings::texture::RgbDataType::UnsignedByte + } + }) + } + TexturePixelDataFormat::Srgb(data_type) => { + GlTexturePixelDataFormat::Srgb(match data_type { + crate::rendering::SrgbTextureDataType::UnsignedByte => { + opengl_bindings::texture::SrgbDataType::UnsignedByte + } + }) + } + TexturePixelDataFormat::Rgba(data_type) => { + GlTexturePixelDataFormat::Rgba(match data_type { + crate::rendering::RgbaTextureDataType::UnsignedByte => { + opengl_bindings::texture::RgbaDataType::UnsignedByte + } + }) + } + TexturePixelDataFormat::Srgba(data_type) => { + GlTexturePixelDataFormat::Srgba(match data_type { + crate::rendering::SrgbaTextureDataType::UnsignedByte => { + opengl_bindings::texture::SrgbaDataType::UnsignedByte + } + }) + } + TexturePixelDataFormat::Depth(data_type) => { + GlTexturePixelDataFormat::DepthComponent(match data_type { + crate::rendering::DepthTextureDataType::Float32 => { + opengl_bindings::texture::DepthComponentDataType::Float32 + } + }) + } + } +} + +fn create_gl_texture( + curr_gl_context: &MaybeCurrentContextWithFns, + texture_creation: TextureCreation, + texture_pixel_data_format: TexturePixelDataFormat, + texture_properties: &TextureProperties, +) -> Result<GlTexture, GlTextureError> +{ + let gl_texture = match texture_creation { + TextureCreation::Texture2D { size, image } => { + GlTexture::builder().size(size.into()).create_2d( + curr_gl_context, + image.as_deref(), + tex_pixel_data_format_into_gl(texture_pixel_data_format), + ) + } + TextureCreation::CubeMap { size, images } => { + GlTexture::builder().size(size.into()).create_cube_map( + curr_gl_context, + images.as_ref().map(|images| { + images.each_ref().map(|(face, image)| { + (cube_map_texture_face_to_gl(*face), &**image) + }) + }), + tex_pixel_data_format_into_gl(texture_pixel_data_format), + ) + } + }?; + + gl_texture.set_wrap( + curr_gl_context, + texture_wrapping_to_gl(texture_properties.wrap), + ); + + gl_texture.set_magnifying_filter( + curr_gl_context, + texture_filtering_to_gl(texture_properties.magnifying_filter), + ); + + gl_texture.set_minifying_filter( + curr_gl_context, + texture_filtering_to_gl(texture_properties.minifying_filter), + ); + + if let Some(border) = texture_properties.border.as_ref() { + gl_texture.set_float_border_values( + curr_gl_context, + match border { + TextureBorder::RgbaF32(color) => [color.r, color.g, color.b, color.a], + TextureBorder::Depth(depth) => [*depth, 0.0, 0.0, 0.0], + }, + ); + } + + Ok(gl_texture) +} + +fn cube_map_texture_face_to_gl(face: CubeMapTextureFace) -> GlCubeMapTextureFace +{ + match face { + CubeMapTextureFace::PositiveX => GlCubeMapTextureFace::PositiveX, + CubeMapTextureFace::NegativeX => GlCubeMapTextureFace::NegativeX, + CubeMapTextureFace::PositiveY => GlCubeMapTextureFace::PositiveY, + CubeMapTextureFace::NegativeY => GlCubeMapTextureFace::NegativeY, + CubeMapTextureFace::PositiveZ => GlCubeMapTextureFace::PositiveZ, + CubeMapTextureFace::NegativeZ => GlCubeMapTextureFace::NegativeZ, + } +} + +fn create_shader_program( + current_context: &MaybeCurrentContextWithFns, + shader_program: &ShaderProgram, +) -> Result<GlShaderProgram, CreateShaderError> +{ + let shader_program_reflection = shader_program.reflection(0).expect("Not possible"); + + let (vs_entry_point_index, vs_entry_point_reflection) = shader_program_reflection + .entry_points() + .enumerate() + .find(|(_, entry_point)| entry_point.stage() == ShaderStage::Vertex) + .ok_or_else(|| { + CreateShaderError::NoShaderStageEntrypointFound(ShaderStage::Vertex) + })?; + + let vertex_shader_entry_point_code = shader_program + .get_entry_point_code(vs_entry_point_index.try_into().expect( + "Vertex shader entry point index does not fit in 32-bit unsigned int", + )) + .map_err(|err| CreateShaderError::GetShaderEntryPointCodeFailed { + err, + stage: ShaderStage::Vertex, + entrypoint: vs_entry_point_reflection + .name() + .map(|name| name.to_string().into()) + .unwrap_or("(none)".into()), + })?; + + let (fs_entry_point_index, fs_entry_point_reflection) = shader_program_reflection + .entry_points() + .enumerate() + .find(|(_, entry_point)| entry_point.stage() == ShaderStage::Fragment) + .ok_or_else(|| { + CreateShaderError::NoShaderStageEntrypointFound(ShaderStage::Fragment) + })?; + + let fragment_shader_entry_point_code = shader_program + .get_entry_point_code(fs_entry_point_index.try_into().expect( + "Fragment shader entry point index does not fit in 32-bit unsigned int", + )) + .map_err(|err| CreateShaderError::GetShaderEntryPointCodeFailed { + err, + stage: ShaderStage::Fragment, + entrypoint: fs_entry_point_reflection + .name() + .map(|name| name.to_string().into()) + .unwrap_or("(none)".into()), + })?; + + let vertex_shader = GlShader::new(current_context, ShaderKind::Vertex); + + vertex_shader.set_source( + current_context, + &vertex_shader_entry_point_code.as_str().unwrap(), + )?; + + vertex_shader.compile(current_context)?; + + let fragment_shader = GlShader::new(current_context, ShaderKind::Fragment); + + fragment_shader.set_source( + current_context, + &fragment_shader_entry_point_code.as_str().unwrap(), + )?; + + fragment_shader.compile(current_context)?; + + let gl_shader_program = GlShaderProgram::new(current_context); + + gl_shader_program.attach(current_context, &vertex_shader); + gl_shader_program.attach(current_context, &fragment_shader); + + gl_shader_program.link(current_context)?; + + Ok(gl_shader_program) +} + +#[derive(Debug, thiserror::Error)] +enum CreateShaderError +{ + #[error( + "Failed to get code of shader program entry point {entrypoint} of stage {stage:?}" + )] + GetShaderEntryPointCodeFailed + { + #[source] + err: ShaderError, + stage: ShaderStage, + entrypoint: Cow<'static, str>, + }, + + #[error("No entrypoint was found for shader stage {0:?}")] + NoShaderStageEntrypointFound(ShaderStage), + + #[error(transparent)] + ShaderError(#[from] GlShaderError), +} + +// This function not having tracing instrumentation is intentional. It would not provide +// any value +fn opengl_debug_message_cb( + source: MessageSource, + ty: MessageType, + id: u32, + severity: MessageSeverity, + message: &str, +) +{ + macro_rules! emit_log_with_message { + ($level: expr) => { + tracing::event!($level, ?source, ?ty, id, ?severity, "{message}"); + }; + } + + if matches!(severity, MessageSeverity::Notification) { + return; + } + + match ty { + MessageType::Error => { + emit_log_with_message!(tracing::Level::ERROR); + + let backtrace = std::backtrace::Backtrace::capture(); + + if backtrace.status() == std::backtrace::BacktraceStatus::Captured { + tracing::error!("{backtrace}"); + } + } + MessageType::Other => { + emit_log_with_message!(tracing::Level::INFO); + } + _ => { + emit_log_with_message!(tracing::Level::WARN); + } + }; +} + +#[inline] +fn texture_wrapping_to_gl(texture_wrapping: TextureWrapping) -> GlTextureWrapping +{ + match texture_wrapping { + TextureWrapping::Repeat => GlTextureWrapping::Repeat, + TextureWrapping::MirroredRepeat => GlTextureWrapping::MirroredRepeat, + TextureWrapping::ClampToEdge => GlTextureWrapping::ClampToEdge, + TextureWrapping::ClampToBorder => GlTextureWrapping::ClampToBorder, + } +} + +#[inline] +fn texture_filtering_to_gl(texture_filtering: TextureFiltering) -> GlTextureFiltering +{ + match texture_filtering { + TextureFiltering::Linear => GlTextureFiltering::Linear, + TextureFiltering::Nearest => GlTextureFiltering::Nearest, + } +} + +impl<Value: ReprC + Copy> From<Vec2<Value>> for opengl_bindings::data_types::Vec2<Value> +{ + fn from(vec2: Vec2<Value>) -> Self + { + Self { x: vec2.x, y: vec2.y } + } +} + +impl<Value: ReprC + IntoBytes + Copy> From<Vec3<Value>> + for opengl_bindings::data_types::Vec3<Value> +{ + fn from(vec3: Vec3<Value>) -> Self + { + Self { x: vec3.x, y: vec3.y, z: vec3.z } + } +} + +impl<Value: Copy> From<Dimens<Value>> for opengl_bindings::data_types::Dimens<Value> +{ + fn from(dimens: Dimens<Value>) -> Self + { + Self { + width: dimens.width, + height: dimens.height, + } + } +} + +fn polygon_mode_to_gl(mode: PolygonMode) -> GlPolygonMode +{ + match mode { + PolygonMode::Point => GlPolygonMode::Point, + PolygonMode::Fill => GlPolygonMode::Fill, + PolygonMode::Line => GlPolygonMode::Line, + } +} + +fn polygon_mode_face_to_gl(face: PolygonModeFace) -> GlPolygonModeFace +{ + match face { + PolygonModeFace::Front => GlPolygonModeFace::Front, + PolygonModeFace::Back => GlPolygonModeFace::Back, + PolygonModeFace::FrontAndBack => GlPolygonModeFace::FrontAndBack, + } +} + +fn blending_factor_to_gl(blending_factor: BlendingFactor) -> GlBlendingFactor +{ + match blending_factor { + BlendingFactor::Zero => GlBlendingFactor::Zero, + BlendingFactor::One => GlBlendingFactor::One, + BlendingFactor::SrcColor => GlBlendingFactor::SrcColor, + BlendingFactor::OneMinusSrcColor => GlBlendingFactor::OneMinusSrcColor, + BlendingFactor::DstColor => GlBlendingFactor::DstColor, + BlendingFactor::OneMinusDstColor => GlBlendingFactor::OneMinusDstColor, + BlendingFactor::SrcAlpha => GlBlendingFactor::SrcAlpha, + BlendingFactor::OneMinusSrcAlpha => GlBlendingFactor::OneMinusSrcAlpha, + BlendingFactor::DstAlpha => GlBlendingFactor::DstAlpha, + BlendingFactor::OneMinusDstAlpha => GlBlendingFactor::OneMinusDstAlpha, + BlendingFactor::ConstantColor => GlBlendingFactor::ConstantColor, + BlendingFactor::OneMinusConstantColor => GlBlendingFactor::OneMinusConstantColor, + BlendingFactor::ConstantAlpha => GlBlendingFactor::ConstantAlpha, + BlendingFactor::OneMinusConstantAlpha => GlBlendingFactor::OneMinusConstantAlpha, + } +} + +fn blending_equation_to_gl(blending_equation: BlendingEquation) -> GlBlendingEquation +{ + match blending_equation { + BlendingEquation::Add => GlBlendingEquation::Add, + BlendingEquation::Subtract => GlBlendingEquation::Subtract, + BlendingEquation::ReverseSubtract => GlBlendingEquation::ReverseSubtract, + BlendingEquation::Min => GlBlendingEquation::Min, + BlendingEquation::Max => GlBlendingEquation::Max, + } +} + +trait IntMapEntryExt<'map, Value> +{ + fn set_or_insert_with(self, func: impl FnOnce() -> Value) -> &'map mut Value; +} + +impl<'map, Key, Value> IntMapEntryExt<'map, Value> for intmap::Entry<'map, Key, Value> +where + Key: intmap::IntKey, + Value: 'map, +{ + fn set_or_insert_with(self, func: impl FnOnce() -> Value) -> &'map mut Value + { + match self { + Self::Occupied(occupied) => { + let value = occupied.into_mut(); + + *value = func(); + + value + } + Self::Vacant(vacant) => vacant.insert(func()), + } + } +} diff --git a/engine/src/rendering/backend/opengl/glutin_compat.rs b/engine/src/rendering/backend/opengl/glutin_compat.rs new file mode 100644 index 0000000..27f82ad --- /dev/null +++ b/engine/src/rendering/backend/opengl/glutin_compat.rs @@ -0,0 +1,264 @@ +// Original file: +// https://github.com/rust-windowing/glutin/blob/ +// 0433af9018febe0696c485ed9d66c40dad41f2d4/glutin-winit/src/lib.rs +// +// Copyright © 2022 Kirill Chibisov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the “Software”), to deal +// in the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +//! This library provides helpers for cross-platform [`glutin`] bootstrapping +//! with [`winit`]. + +#![deny(rust_2018_idioms)] +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(clippy::all)] +#![deny(missing_debug_implementations)] +#![deny(missing_docs)] +#![cfg_attr(clippy, deny(warnings))] + +use glutin::config::{Config, ConfigTemplateBuilder}; +use glutin::display::{Display, DisplayApiPreference}; +use glutin::error::Error as GlutinError; +use glutin::prelude::*; +use raw_window_handle::{DisplayHandle, RawWindowHandle, WindowHandle}; + +use crate::windowing::window::CreationAttributes as WindowCreationAttributes; + +#[cfg(all(not(windows), not(target_os = "macos"), not(target_os = "linux")))] +compile_error!("Unsupported platform"); + +#[cfg(target_family = "wasm")] +compile_error!("Wasm targets are not supported"); + +/// The helper to perform [`Display`] creation and OpenGL platform +/// bootstrapping with the help of [`winit`] with little to no platform specific +/// code. +/// +/// This is only required for the initial setup. If you want to create +/// additional windows just use the [`finalize_window`] function and the +/// configuration you've used either for the original window or picked with the +/// existing [`Display`]. +/// +/// [`winit`]: winit +/// [`Display`]: glutin::display::Display +#[derive(Default, Debug, Clone)] +pub struct DisplayBuilder +{ + preference: ApiPreference, + window_attributes: WindowCreationAttributes, +} + +impl DisplayBuilder +{ + /// Create new display builder. + pub fn new() -> Self + { + Default::default() + } + + /// The preference in picking the configuration. + #[allow(dead_code)] + pub fn with_preference(mut self, preference: ApiPreference) -> Self + { + self.preference = preference; + self + } + + /// The window attributes to use when building a window. + /// + /// By default no window is created. + pub fn with_window_attributes( + mut self, + window_creation_attrs: WindowCreationAttributes, + ) -> Self + { + self.window_attributes = window_creation_attrs; + self + } + + /// Initialize the OpenGL platform and create a compatible window to use + /// with it when the [`WindowAttributes`] was passed with + /// [`Self::with_window_attributes()`]. It's optional, since on some + /// platforms like `Android` it is not available early on, so you want to + /// find configuration and later use it with the [`finalize_window`]. + /// But if you don't care about such platform you can always pass + /// [`WindowAttributes`]. + /// + /// # Api-specific + /// + /// **WGL:** - [`WindowAttributes`] **must** be passed in + /// [`Self::with_window_attributes()`] if modern OpenGL(ES) is desired, + /// otherwise only builtin functions like `glClear` will be available. + pub fn build<ConfigPickerFn>( + self, + window_handle: Option<WindowHandle<'_>>, + display_handle: &DisplayHandle<'_>, + template_builder: ConfigTemplateBuilder, + config_picker_fn: ConfigPickerFn, + ) -> Result<(WindowCreationAttributes, Config), Error> + where + ConfigPickerFn: FnOnce(Box<dyn Iterator<Item = Config> + '_>) -> Option<Config>, + { + // XXX with WGL backend window should be created first. + let raw_window_handle = if cfg!(windows) { + let Some(window_handle) = window_handle else { + return Err(Error::WindowRequired); + }; + + Some(window_handle.as_raw()) + } else { + None + }; + + let gl_display = + create_display(display_handle, self.preference, raw_window_handle) + .map_err(Error::CreateDisplayFailed)?; + + // XXX the native window must be passed to config picker when WGL is used + // otherwise very limited OpenGL features will be supported. + let template_builder = if let Some(raw_window_handle) = + raw_window_handle.filter(|_| cfg!(windows)) + { + template_builder.compatible_with_native_window(raw_window_handle) + } else { + template_builder + }; + + let template = template_builder.build(); + + // SAFETY: The RawWindowHandle passed on the config template + // (when cfg(windows)) will always point to a valid object since it is + // derived from the window_handle argument which when Some is a WindowHandle and + // WindowHandles always point to a valid object + let gl_configs = unsafe { gl_display.find_configs(template) } + .map_err(Error::FindConfigsFailed)?; + + let picked_gl_config = + config_picker_fn(gl_configs).ok_or(Error::NoConfigPicked)?; + + let window_attrs = cfg_select! { + windows => { self.window_attributes } + _ => { + finalize_window_creation_attrs(self.window_attributes, &picked_gl_config) + } + }; + + Ok((window_attrs, picked_gl_config)) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error("Failed to create display")] + CreateDisplayFailed(#[source] GlutinError), + + #[error("Failed to find configs")] + FindConfigsFailed(#[source] GlutinError), + + #[error("No config was picked by config picker function")] + NoConfigPicked, + + #[error("Window required for building display on current platform")] + WindowRequired, +} + +fn create_display( + display_handle: &DisplayHandle<'_>, + _api_preference: ApiPreference, + _raw_window_handle: Option<RawWindowHandle>, +) -> Result<Display, GlutinError> +{ + let preference = cfg_select! { + windows => { + match _api_preference { + ApiPreference::PreferEgl => { + DisplayApiPreference::EglThenWgl(_raw_window_handle) + } + ApiPreference::FallbackEgl => { + DisplayApiPreference::WglThenEgl(_raw_window_handle) + } + } + } + target_os = "linux" => { + match _api_preference { + ApiPreference::PreferEgl => DisplayApiPreference::EglThenGlx(Box::new( + crate::windowing::window::platform::x11::register_xlib_error_hook, + )), + ApiPreference::FallbackEgl => DisplayApiPreference::GlxThenEgl(Box::new( + crate::windowing::window::platform::x11::register_xlib_error_hook, + )), + } + } + target_os = "macos" => { DisplayApiPreference::Cgl } + }; + + let handle = display_handle.as_raw(); + + unsafe { Ok(Display::new(handle, preference)?) } +} + +/// Finalize [`Window`] creation by applying the options from the [`Config`], be +/// aware that it could remove incompatible options from the window builder like +/// `transparency`, when the provided config doesn't support it. +/// +/// [`Window`]: winit::window::Window +/// [`Config`]: glutin::config::Config +#[cfg(not(windows))] +fn finalize_window_creation_attrs( + mut attributes: WindowCreationAttributes, + gl_config: &Config, +) -> WindowCreationAttributes +{ + // Disable transparency if the end config doesn't support it. + if gl_config.supports_transparency() == Some(false) { + attributes = attributes.with_transparent(false); + } + + #[cfg(target_os = "linux")] + if let Some(x11_visual) = glutin::platform::x11::X11GlConfigExt::x11_visual(gl_config) + { + return attributes.with_x_visual_id(Some( + x11_visual.visual_id() as crate::windowing::window::XVisualID + )); + } + + attributes +} + +/// Simplified version of the [`DisplayApiPreference`] which is used to simplify +/// cross platform window creation. +/// +/// To learn about platform differences the [`DisplayApiPreference`] variants. +/// +/// [`DisplayApiPreference`]: glutin::display::DisplayApiPreference +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ApiPreference +{ + /// Prefer `EGL` over system provider like `GLX` and `WGL`. + PreferEgl, + + /// Fallback to `EGL` when failed to create the system profile. + /// + /// This behavior is used by default. However consider using + /// [`Self::PreferEgl`] if you don't care about missing EGL features. + #[default] + FallbackEgl, +} diff --git a/engine/src/rendering/backend/opengl/graphics_mesh.rs b/engine/src/rendering/backend/opengl/graphics_mesh.rs new file mode 100644 index 0000000..a3f4197 --- /dev/null +++ b/engine/src/rendering/backend/opengl/graphics_mesh.rs @@ -0,0 +1,259 @@ +use std::hint::cold_path; + +use opengl_bindings::buffer::{Buffer as GlBuffer, Usage as GlBufferUsage}; +use opengl_bindings::vertex_array::{ + AttributeFormat as GlVertexArrayAttributeFormat, + BindVertexBufferError as GlVertexArrayBindVertexBufferError, + DataType as GlVertexArrayDataType, + VertexArray as GlVertexArray, + VertexBufferSpec as GlVertexArrayVertexBufferSpec, +}; +use opengl_bindings::MaybeCurrentContextWithFns as GlCurrentContextWithFns; + +use crate::mesh::vertex_buffer::VertexAttrProperties as MeshVertexAttrProperties; +use crate::mesh::{Mesh, VertexAttrType}; +use crate::rendering::shader::{ + VertexDescription as ShaderVertexDescription, + VertexInputSemName, +}; +use crate::rendering::MeshUsage; + +const VERTEX_BUF_BINDING_INDEX: u32 = 0; + +#[derive(Debug)] +pub struct GraphicsMesh +{ + /// Vertex and index buffer has to live as long as the vertex array + vertex_buffer: GlBuffer<u8>, + vertex_attr_props: Vec<MeshVertexAttrProperties>, + last_max_vertex_attr_index: u32, + pub index_buffer: Option<GlBuffer<u32>>, + pub element_cnt: u32, + pub vertex_arr: GlVertexArray, +} + +impl GraphicsMesh +{ + #[tracing::instrument(skip_all)] + pub fn new( + current_context: &GlCurrentContextWithFns, + mesh: &Mesh, + mesh_usage: MeshUsage, + ) -> Result<Self, Error> + { + let buffer_usage = mesh_usage_to_gl_buffer_usage(mesh_usage); + + let vertex_arr = GlVertexArray::new(current_context); + let vertex_buffer = GlBuffer::new(current_context); + + vertex_buffer + .store(current_context, mesh.vertex_buf().as_bytes(), buffer_usage) + .map_err(Error::StoreVerticesFailed)?; + + if let Err(err) = vertex_arr.bind_vertex_buffer( + current_context, + VERTEX_BUF_BINDING_INDEX, + &vertex_buffer, + GlVertexArrayVertexBufferSpec { + offset: 0, + vertex_size: mesh.vertex_buf().vertex_size(), + }, + ) { + match err { + GlVertexArrayBindVertexBufferError::OffsetValueTooLarge { + value: _, + max_value: _, + } => unreachable!(), + GlVertexArrayBindVertexBufferError::VertexSizeValueTooLarge { + value, + max_value, + } => { + panic!( + "Size of vertex ({}) is too large. Must be less than {max_value}", + value + ); + } + } + } + + if let Some(indices) = mesh.indices() { + let index_buffer = GlBuffer::new(current_context); + + index_buffer + .store(current_context, indices, buffer_usage) + .map_err(Error::StoreIndicesFailed)?; + + vertex_arr.bind_element_buffer(current_context, &index_buffer); + + return Ok(Self { + vertex_buffer: vertex_buffer, + vertex_attr_props: mesh.vertex_buf().vertex_attr_props().to_vec(), + last_max_vertex_attr_index: 0, + index_buffer: Some(index_buffer), + element_cnt: indices + .len() + .try_into() + .expect("Mesh index count does not fit into a 32-bit unsigned int"), + vertex_arr, + }); + } + + Ok(Self { + vertex_buffer: vertex_buffer, + vertex_attr_props: mesh.vertex_buf().vertex_attr_props().to_vec(), + last_max_vertex_attr_index: 0, + index_buffer: None, + element_cnt: mesh + .vertex_buf() + .len() + .try_into() + .expect("Mesh vertex count does not fit into a 32-bit unsigned int"), + vertex_arr, + }) + } + + pub fn update( + &mut self, + current_context: &GlCurrentContextWithFns, + mesh: &Mesh, + mesh_usage: MeshUsage, + ) -> Result<(), Error> + { + let buffer_usage = mesh_usage_to_gl_buffer_usage(mesh_usage); + + self.vertex_buffer + .store(current_context, mesh.vertex_buf().as_bytes(), buffer_usage) + .map_err(Error::StoreVerticesFailed)?; + + if let Some(indices) = mesh.indices() { + let index_buffer = self + .index_buffer + .get_or_insert_with(|| GlBuffer::new(current_context)); + + index_buffer + .store(current_context, indices, buffer_usage) + .map_err(Error::StoreIndicesFailed)?; + + self.vertex_arr + .bind_element_buffer(current_context, &index_buffer); + + self.element_cnt = indices + .len() + .try_into() + .expect("Mesh index count does not fit into a 32-bit unsigned int"); + + return Ok(()); + } + + self.element_cnt = mesh + .vertex_buf() + .len() + .try_into() + .expect("Mesh vertex count does not fit into a 32-bit unsigned int"); + + Ok(()) + } + + pub fn update_vertex_attrs_for_shader( + &mut self, + curr_gl_ctx: &GlCurrentContextWithFns, + shader_vertex_desc: &ShaderVertexDescription, + ) -> Result<(), VertexAttrsUpdatingError> + { + for index in 0..self.last_max_vertex_attr_index { + self.vertex_arr.disable_attrib(curr_gl_ctx, index); + } + + let mut max_vertex_attr_index = 0u32; + + for vertex_input_desc in &*shader_vertex_desc.inputs { + let Some(vertex_attr_props) = + self.vertex_attr_props.iter().find(|vertex_attr_props| { + vertex_input_desc + .semantic_name + .matches_vertex_label(&vertex_attr_props.label) + }) + else { + cold_path(); + return Err(VertexAttrsUpdatingError::MissingVertexAttr( + vertex_input_desc.semantic_name.clone(), + )); + }; + + let attrib_index: u32 = vertex_input_desc.index.try_into().unwrap(); + + self.vertex_arr.enable_attrib(curr_gl_ctx, attrib_index); + + self.vertex_arr.set_attrib_format( + curr_gl_ctx, + attrib_index, + match &vertex_attr_props.ty { + VertexAttrType::Float32 => GlVertexArrayAttributeFormat { + data_type: GlVertexArrayDataType::Float, + count: 1, + normalized: false, + offset: vertex_attr_props.byte_offset.try_into().unwrap(), + }, + VertexAttrType::Float32Array { length } => { + GlVertexArrayAttributeFormat { + data_type: GlVertexArrayDataType::Float, + count: (*length).try_into().unwrap(), + normalized: false, + offset: vertex_attr_props.byte_offset.try_into().unwrap(), + } + } + }, + ); + + self.vertex_arr.set_attrib_vertex_buf_binding( + curr_gl_ctx, + attrib_index, + VERTEX_BUF_BINDING_INDEX, + ); + + max_vertex_attr_index = max_vertex_attr_index.max(attrib_index); + } + + self.last_max_vertex_attr_index = max_vertex_attr_index; + + Ok(()) + } + + pub fn destroy(&self, curr_gl_ctx: &GlCurrentContextWithFns) + { + self.vertex_arr.delete(curr_gl_ctx); + self.vertex_buffer.delete(curr_gl_ctx); + + if let Some(index_buffer) = &self.index_buffer { + index_buffer.delete(curr_gl_ctx); + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error("Failed to store vertices in vertex buffer")] + StoreVerticesFailed(#[source] opengl_bindings::buffer::Error), + + #[error("Failed to store indices in index buffer")] + StoreIndicesFailed(#[source] opengl_bindings::buffer::Error), +} + +#[derive(Debug, thiserror::Error)] +pub enum VertexAttrsUpdatingError +{ + #[error( + "Mesh is missing equivalent vertex attribute for shader's vertex input: {0}" + )] + MissingVertexAttr(VertexInputSemName), +} + +fn mesh_usage_to_gl_buffer_usage(mesh_usage: MeshUsage) -> GlBufferUsage +{ + match mesh_usage { + MeshUsage::Stream => GlBufferUsage::Stream, + MeshUsage::Static => GlBufferUsage::Static, + MeshUsage::Dynamic => GlBufferUsage::Dynamic, + } +} diff --git a/engine/src/rendering/blending.rs b/engine/src/rendering/blending.rs new file mode 100644 index 0000000..9ae2f82 --- /dev/null +++ b/engine/src/rendering/blending.rs @@ -0,0 +1,89 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Config +{ + pub source_factor: Factor, + pub destination_factor: Factor, + pub equation: Equation, +} + +impl Default for Config +{ + fn default() -> Self + { + Self { + source_factor: Factor::One, + destination_factor: Factor::Zero, + equation: Equation::default(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum Factor +{ + /// Factor will be the RGBA color `(0,0,0,0)` + Zero, + + /// Factor will be the RGBA color `(1,1,1,1)` + One, + + /// Factor will be the source color + SrcColor, + + /// Factor will be the RGBA color `(1,1,1,1) - source color` + OneMinusSrcColor, + + /// Factor will be the destination color + DstColor, + + /// Factor will be the RGBA color `(1,1,1,1) - destination color` + OneMinusDstColor, + + /// Factor will be the alpha component of the source color. + SrcAlpha, + + /// Factor will be the RGBA color `(1,1,1,1) - source color alpha` + OneMinusSrcAlpha, + + /// Factor will be the alpha component of the destination color. + DstAlpha, + + /// Factor will be the RGBA color `(1,1,1,1) - destination color alpha` + OneMinusDstAlpha, + + /// Factor will be the constant color + ConstantColor, + + /// Factor will be the RGBA color `(1,1,1,1) - constant color` + OneMinusConstantColor, + + /// Factor will be the alpha component of the constant color. + ConstantAlpha, + + /// Factor will be the RGBA color `(1,1,1,1) - constant color alpha` + OneMinusConstantAlpha, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum Equation +{ + /// The destination color and source color is added to each other in the blend + /// function + #[default] + Add, + + /// The destination color is subtracted from the source color in the blend function + Subtract, + + /// The source color is subtracted from the destination color in the blend function + ReverseSubtract, + + /// The blend function will take the component-wise minimum of the destination color + /// and the source color + Min, + + /// The blend function will take the component-wise maximum of the destination color + /// and the source color + Max, +} diff --git a/engine/src/rendering/main_render_pass.rs b/engine/src/rendering/main_render_pass.rs new file mode 100644 index 0000000..c762231 --- /dev/null +++ b/engine/src/rendering/main_render_pass.rs @@ -0,0 +1,1006 @@ +use std::path::Path; +use std::sync::LazyLock; + +use ecs::actions::Actions; +use ecs::component::local::Local; +use ecs::error::Context as _; +use ecs::pair::ChildOf; +use ecs::query::term::{Traverse, TraverseUp}; +use ecs::uid::Uid; +use ecs::Component; + +use crate::asset::{Assets, Label as AssetLabel}; +use crate::camera::{Active as ActiveCamera, Camera}; +use crate::color::Rgba; +use crate::data_types::dimens::{Dimens, Dimens3}; +use crate::draw_flags::{DrawFlags, NoDraw, PolygonModeConfig}; +use crate::ecs::query::term::{With, Without}; +use crate::ecs::sole::Single; +use crate::ecs::Query; +use crate::error; +use crate::image::Image; +use crate::lighting::{ + DirectionalLight, + Environmental as EnvironmentalLighting, + PointLight, +}; +use crate::material::{Flags as MaterialFlags, Material}; +use crate::mesh::Mesh; +use crate::model::{MaterialSearchResult, Model}; +use crate::projection::{ClipVolume as ProjectionClipVolume, Perspective, Projection}; +use crate::rendering::object::{Id as ObjectId, Store as ObjectStore}; +use crate::rendering::shader::cursor::{ + BindingTextureKind as ShaderBindingTextureKind, + BindingValue as ShaderBindingValue, + Cursor as ShaderCursor, +}; +use crate::rendering::shader::{ + Context as ShaderContext, + EntrypointFlags as ShaderEntrypointFlags, + ModuleSource as ShaderModuleSource, + Program as ShaderProgram, +}; +use crate::rendering::{ + AssetOrValue, + BufferClearMask, + Command, + DepthFunction, + DrawMeshOptions, + DrawProperties, + DrawPropertiesUpdateFlags, + MeshUsage, + RenderPass, + RenderPasses, + RgbaTextureDataType, + Surface, + TargetWindow, + TextureCreation, + TexturePixelDataFormat, +}; +use crate::scene::{Active as ActiveScene, Scene}; +use crate::sky_box::SkyBox; +use crate::texture::{ + Filtering as TextureFiltering, + Properties as TextureProperties, + Texture, + Wrapping as TextureWrapping, +}; +use crate::transform::Transform; +use crate::vector::Vec3; +use crate::windowing::window::Window; + +type RenderableEntity<'a> = ( + &'a Model, + Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>, + Option<&'a MaterialFlags>, + Option<&'a Transform>, + Option<&'a DrawFlags>, + Without<NoDraw>, +); + +pub static SKY_BOX_SHADER_ASSET_LABEL: LazyLock<AssetLabel> = + LazyLock::new(|| AssetLabel { + path: Path::new("").into(), + name: Some("sky_box_shader".into()), + }); + +pub static MAIN_3D_SHADER_ASSET_LABEL: LazyLock<AssetLabel> = + LazyLock::new(|| AssetLabel { + path: Path::new("").into(), + name: Some("main_3d_shader".into()), + }); + +struct SkyBoxIds +{ + texture_object: ObjectId, + mesh_object: ObjectId, +} + +#[derive(Debug, Default, Component)] +pub struct MiscObjectIds +{ + white_1x1_tex_obj_id: Option<ObjectId>, +} + +#[tracing::instrument(skip_all)] +pub fn add_main_render_pass( + renderable_query: Query<RenderableEntity<'_>>, + camera_query: Query<( + &Camera, + &Transform, + &ActiveCamera, + Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>, + )>, + window_query: Query<(&Window, &Surface, With<TargetWindow>)>, + scene_query: Query<( + Option<&EnvironmentalLighting>, + Option<&SkyBox>, + Option<&SkyBoxState>, + With<Scene>, + With<ActiveScene>, + )>, + point_light_query: Query<( + &PointLight, + &Transform, + Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>, + )>, + directional_light_query: Query<( + &DirectionalLight, + Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>, + )>, + shader_context: Single<ShaderContext>, + mut assets: Single<Assets>, + mut render_passes: Single<RenderPasses>, + mut object_store: Single<ObjectStore>, + mut actions: Actions, + mut misc_object_ids: Local<MiscObjectIds>, +) -> Result<(), crate::Error> +{ + let assets = assets.get_mut()?; + let shader_context = shader_context.get()?; + let render_passes = render_passes.get_mut()?; + let object_store = object_store.get_mut()?; + + let Some((scene_ent_id, (scene_env_lighting, scene_skybox, scene_skybox_state))) = + scene_query.iter_with_euids().next() + else { + return Ok(()); + }; + + let Some((camera, camera_transform, _)) = camera_query.iter().next() else { + tracing::trace!("No active camera"); + return Ok(()); + }; + + let render_pass = render_passes.passes.push_front_mut(RenderPass { + commands: Vec::with_capacity(30), + draw_properties: DrawProperties::default(), + }); + + for (model, ..) in &renderable_query { + add_renderable_creation_commands(assets, object_store, render_pass, &model); + } + + let shaders = + match get_or_create_shaders(assets, shader_context, object_store, render_pass) { + Ok(ShadersCreationStatus::Ready(ready_shaders)) => ready_shaders, + Ok(ShadersCreationStatus::ProcessingRequired) => return Ok(()), + Err(err) => { + tracing::error!("Failed to create shaders: {err}"); + return Ok(()); + } + }; + + let white_1x1_tex_obj_id = + *misc_object_ids.white_1x1_tex_obj_id.get_or_insert_with(|| { + create_white_1x1_texture_object(object_store, render_pass) + }); + + let main_3d_shader_cursor = ShaderCursor::new( + shaders + .main_3d_shader_program + .reflection(0) + .context("Unable to get reflection for main 3D shader")? + .global_params_var_layout() + .ok_or_else(|| { + crate::Error::message( + "Unable to get reflection for main 3D shader's global parameters", + ) + })?, + ); + + add_set_3d_shader_point_light_bindings( + render_pass, + &point_light_query, + shaders.main_3d_shader_obj_id, + &main_3d_shader_cursor, + )?; + + add_set_3d_shader_dir_lights_bindings( + render_pass, + &directional_light_query, + shaders.main_3d_shader_obj_id, + &main_3d_shader_cursor, + )?; + + for (window, window_surface) in &window_query { + render_pass + .commands + .push(Command::MakeCurrent(window_surface.id)); + + add_set_3d_shader_camera_bindings( + render_pass, + (&camera, &camera_transform), + &window, + shaders.main_3d_shader_obj_id, + &main_3d_shader_cursor, + )?; + + let mut buf_clear_mask = BufferClearMask::DEPTH; + + buf_clear_mask.set(BufferClearMask::COLOR, scene_skybox.is_none()); + + render_pass + .commands + .push(Command::ClearBuffers(buf_clear_mask)); + + for (model, material_flags, transform, draw_flags) in &renderable_query { + let Some(model_spec) = assets.get(&model.spec_asset) else { + continue; + }; + + let Some(mesh_asset) = &model_spec.mesh_asset else { + continue; + }; + + if assets.get(mesh_asset).is_none() { + continue; + } + + let model_material = match model_spec.find_first_material(&assets) { + MaterialSearchResult::Found(model_material_asset) + if let Some(model_material) = assets.get(&model_material_asset) => + { + model_material + } + MaterialSearchResult::Found(_) | MaterialSearchResult::NotFound => { + continue; + } + MaterialSearchResult::NoMaterials => &Material::builder().build(), + }; + + if model_material + .textures() + .any(|texture_asset| !assets.is_loaded_and_has_type(&texture_asset)) + { + continue; + } + + add_set_3d_shader_renderable_bindings( + render_pass, + ( + model_material, + material_flags.as_deref(), + transform.as_deref(), + ), + scene_env_lighting.as_deref(), + shaders.main_3d_shader_obj_id, + &main_3d_shader_cursor, + white_1x1_tex_obj_id, + )?; + + render_pass + .commands + .push(Command::ActivateShader(shaders.main_3d_shader_obj_id)); + + if let Some(draw_flags) = draw_flags.as_deref().and_then(|draw_flags| { + if draw_flags.polygon_mode_config != PolygonModeConfig::default() { + Some(draw_flags) + } else { + None + } + }) { + render_pass.commands.push(Command::UpdateDrawProperties( + DrawProperties { + polygon_mode_config: draw_flags.polygon_mode_config.clone(), + ..Default::default() + }, + DrawPropertiesUpdateFlags::POLYGON_MODE_CONFIG, + )); + } + + render_pass.commands.push(Command::DrawMesh( + ObjectId::Asset(mesh_asset.id()), + DrawMeshOptions::default(), + )); + + if draw_flags.as_deref().is_some_and(|draw_flags| { + draw_flags.polygon_mode_config != PolygonModeConfig::default() + }) { + render_pass.commands.push(Command::UpdateDrawProperties( + DrawProperties { + polygon_mode_config: PolygonModeConfig::default(), + ..Default::default() + }, + DrawPropertiesUpdateFlags::POLYGON_MODE_CONFIG, + )); + } + } + + if let Some(scene_skybox) = &scene_skybox { + let Some(sky_box_ids) = load_sky_box( + scene_ent_id, + &scene_skybox, + scene_skybox_state.as_deref(), + assets, + object_store, + render_pass, + &mut actions, + )? + else { + continue; + }; + + add_sky_box_commands( + render_pass, + &sky_box_ids, + &camera, + &camera_transform, + &window, + shaders.sky_box_shader_obj_id, + shaders.sky_box_shader_program, + )?; + } + } + + Ok(()) +} + +fn create_white_1x1_texture_object( + object_store: &mut ObjectStore, + render_pass: &mut RenderPass, +) -> ObjectId +{ + let white_1x1_tex_obj_id = ObjectId::new_sequential(); + + object_store.insert_pending(white_1x1_tex_obj_id); + + render_pass.commands.push(Command::CreateTexture { + obj_id: white_1x1_tex_obj_id, + pixel_data_format: TexturePixelDataFormat::Rgba( + RgbaTextureDataType::UnsignedByte, + ), + creation: TextureCreation::Texture2D { + size: Dimens { width: 1, height: 1 }, + image: Some( + Image::from_color(Rgba::<u8>::white(), Dimens { width: 1, height: 1 }) + .into_bytes() + .into_boxed_slice(), + ), + }, + properties: TextureProperties::default(), + }); + + white_1x1_tex_obj_id +} + +#[derive(Debug)] +enum ShadersCreationStatus<'shader_ctx> +{ + ProcessingRequired, + Ready(ReadyShaders<'shader_ctx>), +} + +#[derive(Debug)] +struct ReadyShaders<'shader_ctx> +{ + main_3d_shader_obj_id: ObjectId, + main_3d_shader_program: &'shader_ctx ShaderProgram, + sky_box_shader_obj_id: ObjectId, + sky_box_shader_program: &'shader_ctx ShaderProgram, +} + +fn get_or_create_shaders<'shader_ctx>( + assets: &mut Assets, + shader_context: &'shader_ctx ShaderContext, + object_store: &mut ObjectStore, + render_pass: &mut RenderPass, +) -> Result<ShadersCreationStatus<'shader_ctx>, crate::Error> +{ + let main_3d_shader_asset = assets + .get_handle_to_loaded::<ShaderModuleSource>(MAIN_3D_SHADER_ASSET_LABEL.clone()) + .or_else(|| { + assets.store_with_label( + MAIN_3D_SHADER_ASSET_LABEL.clone(), + ShaderModuleSource { + name: "main_3d_shader.slang".into(), + file_path: Path::new("@engine/main_3d_shader").into(), + source: include_str!("../../res/main_3d_shader.slang").into(), + link_entrypoints: ShaderEntrypointFlags::VERTEX + | ShaderEntrypointFlags::FRAGMENT, + }, + ); + + None + }); + + let sky_box_shader_asset = assets + .get_handle_to_loaded::<ShaderModuleSource>(SKY_BOX_SHADER_ASSET_LABEL.clone()) + .or_else(|| { + assets.store_with_label( + SKY_BOX_SHADER_ASSET_LABEL.clone(), + ShaderModuleSource { + name: "sky_box_shader.slang".into(), + file_path: Path::new("@engine/sky_box_shader").into(), + source: include_str!("../../res/sky_box_shader.slang").into(), + link_entrypoints: ShaderEntrypointFlags::VERTEX + | ShaderEntrypointFlags::FRAGMENT, + }, + ); + None + }); + + let (Some(main_3d_shader_asset), Some(sky_box_shader_asset)) = + (main_3d_shader_asset, sky_box_shader_asset) + else { + return Ok(ShadersCreationStatus::ProcessingRequired); + }; + + let main_3d_shader_program = shader_context.get_program(&main_3d_shader_asset.id()); + + let main_3d_shader_program = main_3d_shader_program.ok_or_else(|| { + error!("Shader context doesn't have a program for main 3D shader") + })?; + + let main_3d_shader_obj_id = ObjectId::Asset(main_3d_shader_asset.id()); + + if !object_store.contains_maybe_pending_with_id(&main_3d_shader_obj_id) { + object_store.insert_pending(main_3d_shader_obj_id); + + render_pass.commands.push(Command::CreateShaderProgram( + main_3d_shader_obj_id, + main_3d_shader_program.clone(), + )); + } + + let sky_box_shader_program = shader_context.get_program(&sky_box_shader_asset.id()); + + let sky_box_shader_program = sky_box_shader_program.ok_or_else(|| { + error!("Shader context doesn't have a program for sky box shader") + })?; + + let sky_box_shader_obj_id = ObjectId::Asset(sky_box_shader_asset.id()); + + if !object_store.contains_maybe_pending_with_id(&sky_box_shader_obj_id) { + object_store.insert_pending(sky_box_shader_obj_id); + + render_pass.commands.push(Command::CreateShaderProgram( + sky_box_shader_obj_id, + sky_box_shader_program.clone(), + )); + } + + Ok(ShadersCreationStatus::Ready(ReadyShaders { + main_3d_shader_obj_id, + main_3d_shader_program, + sky_box_shader_obj_id, + sky_box_shader_program, + })) +} + +fn add_renderable_creation_commands( + assets: &Assets, + object_store: &mut ObjectStore, + render_pass: &mut RenderPass, + model: &Model, +) +{ + let Some(model_spec) = assets.get(&model.spec_asset) else { + return; + }; + + let Some(mesh_asset) = &model_spec.mesh_asset else { + return; + }; + + if assets.get(mesh_asset).is_none() { + return; + } + + debug_assert!(model_spec.materials.len() <= 1); + + let model_material = match model_spec.find_first_material(&assets) { + MaterialSearchResult::Found(model_material_asset) => { + let Some(model_material) = assets.get(&model_material_asset) else { + return; + }; + + model_material + } + MaterialSearchResult::NotFound => { + return; + } + MaterialSearchResult::NoMaterials => &Material::builder().build(), + }; + + for texture_asset in model_material.textures() { + let Some(texture) = assets.get(texture_asset) else { + return; + }; + + let Texture::Texture2D(texture) = texture else { + tracing::error!( + texture_asset_id = ?texture_asset.id(), + texture_asset_label = ?assets.get_label(texture_asset), + "Material texture map is not 2D" + ); + return; + }; + + let texture_object_id = ObjectId::Asset(texture_asset.id()); + + if object_store.contains_maybe_pending_with_id(&texture_object_id) { + return; + } + + let Some(tex_pixel_data_format) = + TexturePixelDataFormat::for_image(&texture.image) + else { + tracing::error!("No texture pixel data format is available for image"); + return; + }; + + object_store.insert_pending(texture_object_id); + + render_pass.commands.push(Command::CreateTexture { + obj_id: texture_object_id, + pixel_data_format: tex_pixel_data_format, + creation: TextureCreation::Texture2D { + size: texture.image.dimensions(), + image: Some(texture.image.as_bytes().to_vec().into_boxed_slice()), + }, + properties: texture.properties.clone(), + }); + } + + if !object_store.contains_maybe_pending_with_id(&ObjectId::Asset(mesh_asset.id())) { + object_store.insert_pending(ObjectId::Asset(mesh_asset.id())); + + render_pass.commands.push(Command::CreateMesh { + obj_id: ObjectId::Asset(mesh_asset.id()), + mesh: AssetOrValue::Asset(mesh_asset.clone()), + usage: MeshUsage::Static, + }); + } +} + +fn add_set_3d_shader_point_light_bindings( + render_pass: &mut RenderPass, + point_light_query: &Query<( + &PointLight, + &Transform, + Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>, + )>, + main_3d_shader_obj_id: ObjectId, + main_3d_shader_cursor: &ShaderCursor<'_>, +) -> Result<(), crate::Error> +{ + let lighting_shader_cursor = + main_3d_shader_cursor.field("Uniforms").field("lighting"); + + render_pass + .commands + .reserve(1 + point_light_query.iter().count() * 6); + + render_pass.commands.push(Command::SetShaderBinding( + main_3d_shader_obj_id, + lighting_shader_cursor.field("point_light_cnt").binding( + u32::try_from(point_light_query.iter().count()) + .expect("Point light count does not fit in 32-bit unsigned integer") + .into(), + )?, + )); + + for (point_light_index, (point_light, point_light_transform)) in + point_light_query.iter().enumerate() + { + let point_light_shader_cursor = lighting_shader_cursor + .field("point_lights") + .element(point_light_index); + + let phong_shader_cursor = point_light_shader_cursor.field("phong"); + + let attenuation_props_shader_cursor = + point_light_shader_cursor.field("attenuation_props"); + + render_pass.commands.extend( + [ + phong_shader_cursor + .field("diffuse") + .binding(point_light.diffuse.to_rgb_lossy().into())?, + phong_shader_cursor + .field("specular") + .binding(point_light.specular.to_rgb_lossy().into())?, + point_light_shader_cursor.field("position").binding( + (point_light_transform.position + point_light.local_position).into(), + )?, + attenuation_props_shader_cursor + .field("constant") + .binding(point_light.attenuation_params.constant.into())?, + attenuation_props_shader_cursor + .field("linear") + .binding(point_light.attenuation_params.linear.into())?, + attenuation_props_shader_cursor + .field("quadratic") + .binding(point_light.attenuation_params.quadratic.into())?, + ] + .map(|binding| Command::SetShaderBinding(main_3d_shader_obj_id, binding)), + ); + } + + Ok(()) +} + +fn add_set_3d_shader_dir_lights_bindings( + render_pass: &mut RenderPass, + directional_light_query: &Query<( + &DirectionalLight, + Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>, + )>, + main_3d_shader_obj_id: ObjectId, + main_3d_shader_cursor: &ShaderCursor<'_>, +) -> Result<(), crate::Error> +{ + let lighting_shader_cursor = + main_3d_shader_cursor.field("Uniforms").field("lighting"); + + render_pass + .commands + .reserve(1 + directional_light_query.iter().count() * 3); + + render_pass.commands.push(Command::SetShaderBinding( + main_3d_shader_obj_id, + lighting_shader_cursor + .field("directional_light_cnt") + .binding( + u32::try_from(directional_light_query.iter().count()) + .expect( + "Directional light count does not fit in 32-bit unsigned integer", + ) + .into(), + )?, + )); + + for (directional_light_index, (directional_light,)) in + directional_light_query.iter().enumerate() + { + let directional_light_shader_cursor = lighting_shader_cursor + .field("directional_lights") + .element(directional_light_index); + + let phong_shader_cursor = directional_light_shader_cursor.field("phong"); + + render_pass.commands.extend( + [ + phong_shader_cursor + .field("diffuse") + .binding(directional_light.diffuse.to_rgb_lossy().into())?, + phong_shader_cursor + .field("specular") + .binding(directional_light.specular.to_rgb_lossy().into())?, + directional_light_shader_cursor + .field("direction") + .binding(directional_light.direction.into())?, + ] + .map(|binding| Command::SetShaderBinding(main_3d_shader_obj_id, binding)), + ); + } + + Ok(()) +} + +fn add_set_3d_shader_camera_bindings( + render_pass: &mut RenderPass, + (camera, camera_transform): (&Camera, &Transform), + window: &Window, + main_3d_shader_obj_id: ObjectId, + main_3d_shader_cursor: &ShaderCursor<'_>, +) -> Result<(), crate::Error> +{ + let model_3d_shader_cursor = + main_3d_shader_cursor.field("Uniforms").field("model_3d"); + + let lighting_shader_cursor = + main_3d_shader_cursor.field("Uniforms").field("lighting"); + + render_pass.commands.extend( + [ + model_3d_shader_cursor + .field("view") + .binding(camera.to_view_matrix(camera_transform.position).into())?, + model_3d_shader_cursor.field("projection").binding( + camera + .projection + .to_matrix_rh(window.inner_size, ProjectionClipVolume::NegOneToOne) + .into(), + )?, + lighting_shader_cursor + .field("view_pos") + .binding(camera_transform.position.into())?, + ] + .into_iter() + .map(|binding| Command::SetShaderBinding(main_3d_shader_obj_id, binding)), + ); + + Ok(()) +} + +fn add_set_3d_shader_renderable_bindings( + render_pass: &mut RenderPass, + renderable: (&Material, Option<&MaterialFlags>, Option<&Transform>), + scene_env_lighting: Option<&EnvironmentalLighting>, + main_3d_shader_obj_id: ObjectId, + main_3d_shader_cursor: &ShaderCursor<'_>, + white_1x1_tex_obj_id: ObjectId, +) -> Result<(), crate::Error> +{ + let (material, material_flags, transform) = renderable; + + let transform = match transform.as_deref() { + Some(transform) => transform, + None => &Transform::default(), + }; + + let model_matrix = transform.to_matrix(); + let inverted_model_matrix = model_matrix.inverse(); + + let material_flags = material_flags + .as_deref() + .unwrap_or(&const { MaterialFlags::builder().build() }); + + let env_lighting = match &scene_env_lighting { + Some(env_lighting) => &env_lighting, + None => &EnvironmentalLighting::default(), + }; + + let model_3d_shader_cursor = + main_3d_shader_cursor.field("Uniforms").field("model_3d"); + + let material_shader_cursor = main_3d_shader_cursor + .field("Uniforms") + .field("lighting") + .field("material"); + + let diffuse_map_obj_id = material + .diffuse_map + .as_ref() + .map(|diffuse_map| ObjectId::Asset(diffuse_map.id())) + .unwrap_or(white_1x1_tex_obj_id); + + render_pass.commands.extend( + [ + model_3d_shader_cursor + .field("model") + .binding(model_matrix.into())?, + model_3d_shader_cursor + .field("model_inverted") + .binding(inverted_model_matrix.into())?, + material_shader_cursor.field("ambient").binding( + material_flags + .use_ambient_color + .then_some(&material.ambient) + .unwrap_or(&env_lighting.ambient_color) + .to_rgb_lossy() + .into(), + )?, + material_shader_cursor + .field("diffuse") + .binding(material.diffuse.to_rgb_lossy().into())?, + material_shader_cursor + .field("specular") + .binding(material.specular.to_rgb_lossy().into())?, + main_3d_shader_cursor.field("ambient_map").binding( + ShaderBindingValue::Texture( + material + .ambient_map + .as_ref() + .map(|ambient_map| ObjectId::Asset(ambient_map.id())) + .unwrap_or(diffuse_map_obj_id), + ShaderBindingTextureKind::Texture2D, + ), + )?, + main_3d_shader_cursor.field("diffuse_map").binding( + ShaderBindingValue::Texture( + diffuse_map_obj_id, + ShaderBindingTextureKind::Texture2D, + ), + )?, + main_3d_shader_cursor.field("specular_map").binding( + ShaderBindingValue::Texture( + material + .specular_map + .as_ref() + .map(|specular_map| ObjectId::Asset(specular_map.id())) + .unwrap_or(white_1x1_tex_obj_id), + ShaderBindingTextureKind::Texture2D, + ), + )?, + material_shader_cursor + .field("shininess") + .binding(material.shininess.into())?, + ] + .into_iter() + .map(|binding| Command::SetShaderBinding(main_3d_shader_obj_id, binding)), + ); + + Ok(()) +} + +fn add_sky_box_commands( + render_pass: &mut RenderPass, + sky_box_ids: &SkyBoxIds, + camera: &Camera, + camera_transform: &Transform, + window: &Window, + sky_box_shader_obj_id: ObjectId, + sky_box_shader_program: &ShaderProgram, +) -> Result<(), crate::Error> +{ + render_pass + .commands + .push(Command::ActivateShader(sky_box_shader_obj_id)); + + let sky_box_shader_cursor = ShaderCursor::new( + sky_box_shader_program + .reflection(0) + .unwrap() + .global_params_var_layout() + .unwrap(), + ); + + let mut view = camera.to_view_matrix(camera_transform.position); + + view.translate(&Vec3 { x: 0.0, y: 0.0, z: 0.0 }); + + let sky_box_shader_bindings = [ + sky_box_shader_cursor + .field("Uniforms") + .field("projection") + .binding( + (match camera.projection { + Projection::Perspective(_) => camera.projection.to_matrix_rh( + window.inner_size, + ProjectionClipVolume::NegOneToOne, + ), + Projection::Orthographic(_) => { + // A orthographic projection cannot be used for a sky box + Perspective::default().to_matrix_rh( + window.inner_size.width as f32 + / window.inner_size.height as f32, + ProjectionClipVolume::NegOneToOne, + ) + } + }) + .into(), + )?, + sky_box_shader_cursor + .field("Uniforms") + .field("view") + .binding(view.into())?, + sky_box_shader_cursor.field("cube_texture").binding( + ShaderBindingValue::Texture( + sky_box_ids.texture_object, + ShaderBindingTextureKind::Cube, + ), + )?, + ]; + + for binding in sky_box_shader_bindings { + render_pass + .commands + .push(Command::SetShaderBinding(sky_box_shader_obj_id, binding)); + } + + render_pass.commands.push(Command::UpdateDrawProperties( + DrawProperties { + depth_function: DepthFunction::LessOrEqual, + ..Default::default() + }, + DrawPropertiesUpdateFlags::DEPTH_FUNCTION, + )); + + render_pass.commands.push(Command::DrawMesh( + sky_box_ids.mesh_object, + DrawMeshOptions::default(), + )); + + Ok(()) +} + +fn load_sky_box( + scene_ent_id: Uid, + sky_box: &SkyBox, + sky_box_state: Option<&SkyBoxState>, + assets: &mut Assets, + object_store: &mut ObjectStore, + render_pass: &mut RenderPass, + actions: &mut Actions, +) -> Result<Option<SkyBoxIds>, crate::Error> +{ + let mesh_object_id = match &sky_box_state { + Some(sky_box_state) => sky_box_state.mesh_object_id, + None => { + let sky_box_mesh = + Mesh::cube(Dimens3 { width: 1.0, height: 1.0, depth: 1.0 }); + + let mesh_object_id = ObjectId::new_sequential(); + + object_store.insert_pending(mesh_object_id); + + render_pass.commands.push(Command::CreateMesh { + obj_id: mesh_object_id, + mesh: AssetOrValue::Value(sky_box_mesh), + usage: MeshUsage::Static, + }); + + mesh_object_id + } + }; + + let texture_object_id = match &sky_box { + SkyBox::AssetPerCubeMapFace(cube_map_part_assets) => { + if let Some(sky_box_state) = sky_box_state { + sky_box_state.texture_object_id + } else { + let Some(part_images) = cube_map_part_assets + .iter() + .map_while(|(face, part_texture_asset)| { + let part_texture = assets.get(part_texture_asset)?; + + match part_texture { + Texture::Texture2D(part_texture) => { + Some((*face, &part_texture.image)) + } + Texture::CubeMap(_) => { + tracing::warn!( + "Cube map texture cannot be used as cube map part" + ); + + None + } + } + }) + .collect::<Vec<_>>() + .as_array::<6>() + .cloned() + else { + return Ok(None); + }; + + let texture_object_id = ObjectId::new_sequential(); + + object_store.insert_pending(texture_object_id); + + render_pass.commands.push(Command::CreateTexture { + obj_id: texture_object_id, + pixel_data_format: TexturePixelDataFormat::for_image( + part_images[0].1, + ) + .expect("No texture pixel data format is available for image"), + creation: TextureCreation::CubeMap { + size: part_images[0].1.dimensions(), + images: Some(part_images.map(|(face, image)| { + (face, image.as_bytes().to_vec().into_boxed_slice()) + })), + }, + properties: TextureProperties::builder() + .minifying_filter(TextureFiltering::Linear) + .magnifying_filter(TextureFiltering::Linear) + .wrap(TextureWrapping::ClampToEdge) + .build(), + }); + + texture_object_id + } + } + }; + + if sky_box_state.is_none() { + actions.add_components( + scene_ent_id, + (SkyBoxState { texture_object_id, mesh_object_id },), + ); + } + + Ok(Some(SkyBoxIds { + texture_object: texture_object_id, + mesh_object: mesh_object_id, + })) +} + +#[derive(Debug, Component)] +pub struct SkyBoxState +{ + texture_object_id: ObjectId, + mesh_object_id: ObjectId, +} diff --git a/engine/src/rendering/object.rs b/engine/src/rendering/object.rs new file mode 100644 index 0000000..61e7ed5 --- /dev/null +++ b/engine/src/rendering/object.rs @@ -0,0 +1,137 @@ +use std::collections::HashMap; +use std::fmt::Display; +use std::sync::atomic::{AtomicU64, Ordering}; + +use crate::asset::Id as AssetId; +use crate::ecs::Sole; + +pub type RawValue = u32; + +/// Rendering object ID. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Id +{ + Asset(AssetId), + Sequential(SequentialId), +} + +impl Id +{ + pub fn new_sequential() -> Self + { + static NEXT_SEQUENTIAL_ID: AtomicU64 = AtomicU64::new(0); + + Self::Sequential(SequentialId( + NEXT_SEQUENTIAL_ID.fetch_add(1, Ordering::Relaxed), + )) + } + + pub fn into_asset_id(self) -> Option<AssetId> + { + match self { + Self::Asset(asset_id) => Some(asset_id), + Self::Sequential(_) => None, + } + } +} + +impl From<AssetId> for Id +{ + fn from(asset_id: AssetId) -> Self + { + Self::Asset(asset_id) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SequentialId(u64); + +/// Rendering object store. +#[derive(Debug, Default, Sole)] +pub struct Store +{ + objects: HashMap<Id, Option<Object>>, +} + +impl Store +{ + pub fn get_obj(&self, id: &Id) -> Option<&Object> + { + self.objects.get(id).and_then(|obj| obj.as_ref()) + } + + pub fn contains_maybe_pending_with_id(&self, id: &Id) -> bool + { + self.objects.contains_key(id) + } + + pub fn contains_non_pending_with_id(&self, id: &Id) -> bool + { + self.objects.get(id).and_then(|obj| obj.as_ref()).is_some() + } + + pub fn insert(&mut self, id: Id, object: Object) + { + self.objects.insert(id, Some(object)); + } + + pub fn insert_pending(&mut self, id: Id) + { + self.objects.insert(id, None); + } + + pub fn remove(&mut self, id: &Id) -> Option<Option<Object>> + { + self.objects.remove(id) + } +} + +/// Rendering object. +#[derive(Debug, Clone)] +pub struct Object +{ + raw: RawValue, + kind: Kind, +} + +impl Object +{ + pub fn from_raw(raw: RawValue, kind: Kind) -> Self + { + Self { raw, kind } + } + + pub fn as_raw(&self) -> RawValue + { + self.raw + } + + pub fn kind(&self) -> Kind + { + self.kind + } +} + +/// Rendering object kind. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum Kind +{ + Texture, + ShaderProgram, + Mesh, + Framebuffer, +} + +impl Display for Kind +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + formatter.write_str(match self { + Self::Texture => "texture", + Self::ShaderProgram => "shader program", + Self::Mesh => "mesh", + Self::Framebuffer => "framebuffer", + }) + } +} diff --git a/engine/src/rendering/shader.rs b/engine/src/rendering/shader.rs new file mode 100644 index 0000000..3f07381 --- /dev/null +++ b/engine/src/rendering/shader.rs @@ -0,0 +1,1404 @@ +use std::any::type_name; +use std::borrow::Cow; +use std::collections::{HashMap, HashSet}; +use std::fmt::{Debug, Display, Write}; +use std::path::Path; +use std::str::Utf8Error; +use std::sync::Arc; + +use bitflags::{bitflags, bitflags_match}; +use ecs::phase::INIT as INIT_PHASE; +use shader_slang::{ + Blob as SlangBlob, + ComponentType as SlangComponentType, + DebugInfoLevel as SlangDebugInfoLevel, + EntryPoint as SlangEntryPoint, + GlobalSession as SlangGlobalSession, + Module as SlangModule, + ParameterCategory as SlangParameterCategory, + ScalarType as SlangScalarType, + Session as SlangSession, + TypeKind as SlangTypeKind, +}; + +use crate::asset::{ + Assets, + Event as AssetEvent, + Handle as AssetHandle, + Id as AssetId, + Submitter as AssetSubmitter, + HANDLE_ASSETS_PHASE, +}; +use crate::builder; +use crate::ecs::pair::ChildOf; +use crate::ecs::phase::Phase; +use crate::ecs::sole::Single; +use crate::ecs::{declare_entity, pair, Sole}; +use crate::mesh::vertex_buffer::VertexLabel; + +pub mod cursor; + +pub const STD_VERT_IN_SEM_NAME_POSITION: &str = "STD_POSITION"; +pub const STD_VERT_IN_SEM_NAME_NORMAL: &str = "STD_NORMAL"; +pub const STD_VERT_IN_SEM_NAME_UV_FROM_TOP_LEFT: &str = "STD_UV_FROM_TOP_LEFT"; +pub const STD_VERT_IN_SEM_NAME_COLOR: &str = "STD_COLOR"; + +/// Shader module. +#[derive(Debug)] +pub struct ModuleSource +{ + pub name: Cow<'static, str>, + pub file_path: Cow<'static, Path>, + pub source: Cow<'static, str>, + pub link_entrypoints: EntrypointFlags, +} + +bitflags! { + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] + pub struct EntrypointFlags: usize + { + const FRAGMENT = 1 << 0; + const VERTEX = 1 << 1; + } +} + +#[derive(Clone)] +pub struct Module +{ + inner: SlangModule, +} + +impl Module +{ + pub fn entry_points(&self) -> impl ExactSizeIterator<Item = EntryPoint> + use<'_> + { + self.inner + .entry_points() + .map(|entry_point| EntryPoint { inner: entry_point }) + } + + pub fn get_entry_point(&self, entry_point: &str) -> Option<EntryPoint> + { + let entry_point = self.inner.find_entry_point_by_name(entry_point)?; + + Some(EntryPoint { inner: entry_point }) + } + + pub fn file_path(&self) -> &str + { + self.inner.file_path() + } +} + +pub struct EntryPoint +{ + inner: SlangEntryPoint, +} + +impl EntryPoint +{ + pub fn function(&self) -> FunctionReflection<'_> + { + FunctionReflection { + inner: self.inner.function_reflection(), + } + } +} + +pub struct FunctionReflection<'a> +{ + inner: &'a shader_slang::reflection::Function, +} + +impl<'a> FunctionReflection<'a> +{ + pub fn name(&self) -> Option<&str> + { + self.inner.name() + } +} + +pub struct EntryPointReflection<'a> +{ + inner: &'a shader_slang::reflection::EntryPoint, +} + +impl<'a> EntryPointReflection<'a> +{ + pub fn name(&self) -> Option<&str> + { + self.inner.name() + } + + pub fn name_override(&self) -> Option<&str> + { + self.inner.name_override() + } + + pub fn stage(&self) -> Stage + { + Stage::from_slang_stage(self.inner.stage()) + } + + pub fn parameters(&self) -> impl ExactSizeIterator<Item = VariableLayout<'a>> + { + self.inner + .parameters() + .map(|param| VariableLayout { inner: param }) + } + + pub fn var_layout(&self) -> Option<VariableLayout<'a>> + { + Some(VariableLayout { inner: self.inner.var_layout()? }) + } +} + +#[derive(Clone)] +pub struct Program +{ + inner: SlangComponentType, + metadata: ProgramMetadata, +} + +impl Program +{ + pub fn into_linked(self) -> Result<Program, Error> + { + let linked_program = self.inner.link()?; + + Ok(Program { + inner: linked_program, + metadata: self.metadata, + }) + } + + pub fn metadata(&self) -> &ProgramMetadata + { + &self.metadata + } + + pub fn get_entry_point_code(&self, entry_point_index: u32) -> Result<Blob, Error> + { + let blob = self.inner.entry_point_code(entry_point_index.into(), 0)?; + + Ok(Blob { inner: blob }) + } + + pub fn reflection(&self, target: u32) -> Result<ProgramReflection<'_>, Error> + { + let reflection = self.inner.layout(target as i64)?; + + Ok(ProgramReflection { inner: reflection }) + } +} + +impl Debug for Program +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + formatter + .debug_struct(type_name::<Self>()) + .finish_non_exhaustive() + } +} + +pub struct ProgramReflection<'a> +{ + inner: &'a shader_slang::reflection::Shader, +} + +impl<'a> ProgramReflection<'a> +{ + pub fn get_entry_point_by_index(&self, index: u32) + -> Option<EntryPointReflection<'a>> + { + Some(EntryPointReflection { + inner: self.inner.entry_point_by_index(index)?, + }) + } + + pub fn get_entry_point_by_name(&self, name: &str) + -> Option<EntryPointReflection<'a>> + { + Some(EntryPointReflection { + inner: self.inner.find_entry_point_by_name(name)?, + }) + } + + pub fn entry_points( + &self, + ) -> impl ExactSizeIterator<Item = EntryPointReflection<'a>> + use<'a> + { + self.inner + .entry_points() + .map(|entry_point| EntryPointReflection { inner: entry_point }) + } + + pub fn global_params_type_layout(&self) -> Option<TypeLayout<'a>> + { + Some(TypeLayout { + inner: self.inner.global_params_type_layout()?, + }) + } + + pub fn global_params_var_layout(&self) -> Option<VariableLayout<'a>> + { + Some(VariableLayout { + inner: self.inner.global_params_var_layout()?, + }) + } + + pub fn get_type(&self, name: &str) -> Option<TypeReflection<'a>> + { + Some(TypeReflection { + inner: self.inner.find_type_by_name(name)?, + }) + } + + pub fn get_type_layout(&self, ty: &TypeReflection<'a>) -> Option<TypeLayout<'a>> + { + Some(TypeLayout { + inner: self + .inner + .type_layout(&ty.inner, shader_slang::LayoutRules::Default)?, + }) + } +} + +#[derive(Clone, Copy)] +pub struct VariableLayout<'a> +{ + inner: &'a shader_slang::reflection::VariableLayout, +} + +impl<'a> VariableLayout<'a> +{ + pub fn name(&self) -> Option<&'a str> + { + self.inner.name() + } + + pub fn semantic_name(&self) -> Option<&str> + { + self.inner.semantic_name() + } + + pub fn binding_index(&self) -> u32 + { + self.inner + .offset(shader_slang::ParameterCategory::DescriptorTableSlot) as u32 + + // self.inner.binding_index() + } + + pub fn varying_input_offset(&self) -> Option<usize> + { + if !self + .inner + .categories() + .any(|category| category == SlangParameterCategory::VaryingInput) + { + return None; + } + + Some(self.inner.offset(SlangParameterCategory::VaryingInput)) + } + + pub fn binding_space(&self) -> u32 + { + self.inner.binding_space() + } + + pub fn semantic_index(&self) -> usize + { + self.inner.semantic_index() + } + + pub fn offset(&self) -> usize + { + self.inner.offset(shader_slang::ParameterCategory::Uniform) + } + + pub fn ty(&self) -> Option<TypeReflection<'a>> + { + self.inner.ty().map(|ty| TypeReflection { inner: ty }) + } + + pub fn type_layout(&self) -> Option<TypeLayout<'a>> + { + Some(TypeLayout { inner: self.inner.type_layout()? }) + } +} + +#[derive(Clone, Copy)] +pub struct TypeLayout<'a> +{ + inner: &'a shader_slang::reflection::TypeLayout, +} + +impl<'a> TypeLayout<'a> +{ + pub fn kind(&self) -> TypeKind + { + TypeKind::from_slang_type_kind(self.inner.kind()) + } + + pub fn scalar_type(&self) -> Option<ScalarType> + { + Some(ScalarType::from_slang_scalar_type( + self.inner.scalar_type()?, + )) + } + + pub fn resource_shape(&self) -> Option<ResourceShape> + { + Some(ResourceShape::from_bits_retain( + self.inner.resource_shape()? as u32, + )) + } + + pub fn get_field_by_name(&self, name: &str) -> Option<VariableLayout<'a>> + { + let index = self.inner.find_field_index_by_name(name); + + if index < 0 { + return None; + } + + let index = u32::try_from(index.cast_unsigned()).expect("Should not happend"); + + let field = self.inner.field_by_index(index)?; + + Some(VariableLayout { inner: field }) + } + + pub fn parameter_category(&self) -> ParameterCategory + { + ParameterCategory::from_slang_parameter_category(self.inner.parameter_category()) + } + + pub fn binding_range_descriptor_set_index(&self, index: i64) -> i64 + { + self.inner.binding_range_descriptor_set_index(index) + } + + pub fn get_field_binding_range_offset_by_name(&self, name: &str) -> Option<u64> + { + let field_index = self.inner.find_field_index_by_name(name); + + if field_index < 0 { + return None; + } + + let field_binding_range_offset = + self.inner.field_binding_range_offset(field_index); + + if field_binding_range_offset < 0 { + return None; + } + + Some(field_binding_range_offset.cast_unsigned()) + } + + pub fn ty(&self) -> Option<TypeReflection<'a>> + { + self.inner.ty().map(|ty| TypeReflection { inner: ty }) + } + + pub fn fields(&self) -> FieldIter<'a> + { + FieldIter { + type_layout: self.clone(), + cnt: self.field_cnt(), + index: 0, + } + } + + pub fn field_cnt(&self) -> u32 + { + self.inner.field_count() + } + + pub fn element_cnt(&self) -> Option<usize> + { + self.inner.element_count() + } + + pub fn row_cnt(&self) -> Option<u32> + { + self.inner.row_count() + } + + pub fn column_cnt(&self) -> Option<u32> + { + self.inner.column_count() + } + + pub fn element_type_layout(&self) -> Option<TypeLayout<'a>> + { + self.inner + .element_type_layout() + .map(|type_layout| TypeLayout { inner: type_layout }) + } + + pub fn element_var_layout(&self) -> Option<VariableLayout<'a>> + { + self.inner + .element_var_layout() + .map(|var_layout| VariableLayout { inner: var_layout }) + } + + pub fn container_var_layout(&self) -> Option<VariableLayout<'a>> + { + self.inner + .container_var_layout() + .map(|var_layout| VariableLayout { inner: var_layout }) + } + + pub fn uniform_size(&self) -> Option<usize> + { + // tracing::debug!( + // "uniform_size: {:?} categories: {:?}", + // self.inner.name(), + // self.inner.categories().collect::<Vec<_>>(), + // ); + + if !self + .inner + .categories() + .any(|category| category == SlangParameterCategory::Uniform) + { + return None; + } + + // let category = self.inner.categories().next().unwrap(); + + // println!( + // "AARGH size Category: {category:?} Category count: {}", + // self.inner.category_count() + // ); + + // Some(self.inner.size(category)) + + Some(self.inner.size(SlangParameterCategory::Uniform)) + } + + pub fn stride(&self) -> usize + { + self.inner.stride(self.inner.categories().next().unwrap()) + } +} + +pub struct FieldIter<'a> +{ + type_layout: TypeLayout<'a>, + cnt: u32, + index: u32, +} + +impl<'a> Iterator for FieldIter<'a> +{ + type Item = VariableLayout<'a>; + + fn next(&mut self) -> Option<Self::Item> + { + if self.index == self.cnt { + return None; + } + + let Some(field) = self.type_layout.inner.field_by_index(self.index) else { + unreachable!(); + }; + + self.index += 1; + + Some(VariableLayout { inner: field }) + } + + fn size_hint(&self) -> (usize, Option<usize>) + { + let len = (self.cnt - self.index) as usize; + + (len, Some(len)) + } +} + +impl ExactSizeIterator for FieldIter<'_> +{ + fn len(&self) -> usize + { + (self.cnt - self.index) as usize + } +} + +impl DoubleEndedIterator for FieldIter<'_> +{ + fn next_back(&mut self) -> Option<Self::Item> + { + if self.cnt == 0 || self.index == self.cnt - 1 { + return None; + } + + let Some(field) = self.type_layout.inner.field_by_index(self.cnt - 1) else { + unreachable!(); + }; + + self.cnt -= 1; + + Some(VariableLayout { inner: field }) + } +} + +pub struct TypeReflection<'a> +{ + inner: &'a shader_slang::reflection::Type, +} + +impl TypeReflection<'_> +{ + pub fn kind(&self) -> TypeKind + { + TypeKind::from_slang_type_kind(self.inner.kind()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum TypeKind +{ + None, + Struct, + Enum, + Array, + Matrix, + Vector, + Scalar, + ConstantBuffer, + Resource, + SamplerState, + TextureBuffer, + ShaderStorageBuffer, + ParameterBlock, + GenericTypeParameter, + Interface, + OutputStream, + MeshOutput, + Specialized, + Feedback, + Pointer, + DynamicResource, + Count, +} + +impl TypeKind +{ + fn from_slang_type_kind(type_kind: SlangTypeKind) -> Self + { + match type_kind { + SlangTypeKind::None => Self::None, + SlangTypeKind::Struct => Self::Struct, + SlangTypeKind::Enum => Self::Enum, + SlangTypeKind::Array => Self::Array, + SlangTypeKind::Matrix => Self::Matrix, + SlangTypeKind::Vector => Self::Vector, + SlangTypeKind::Scalar => Self::Scalar, + SlangTypeKind::ConstantBuffer => Self::ConstantBuffer, + SlangTypeKind::Resource => Self::Resource, + SlangTypeKind::SamplerState => Self::SamplerState, + SlangTypeKind::TextureBuffer => Self::TextureBuffer, + SlangTypeKind::ShaderStorageBuffer => Self::ShaderStorageBuffer, + SlangTypeKind::ParameterBlock => Self::ParameterBlock, + SlangTypeKind::GenericTypeParameter => Self::GenericTypeParameter, + SlangTypeKind::Interface => Self::Interface, + SlangTypeKind::OutputStream => Self::OutputStream, + SlangTypeKind::MeshOutput => Self::MeshOutput, + SlangTypeKind::Specialized => Self::Specialized, + SlangTypeKind::Feedback => Self::Feedback, + SlangTypeKind::Pointer => Self::Pointer, + SlangTypeKind::DynamicResource => Self::DynamicResource, + SlangTypeKind::Count => Self::Count, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] +pub enum ScalarType +{ + None, + Void, + Bool, + Int32, + Uint32, + Int64, + Uint64, + Float16, + Float32, + Float64, + Int8, + Uint8, + Int16, + Uint16, + Intptr, + Uintptr, + Bfloat16, + FloatE4m3, + FloatE5m2, +} + +impl ScalarType +{ + fn from_slang_scalar_type(scalar_type: SlangScalarType) -> Self + { + match scalar_type { + SlangScalarType::None => Self::None, + SlangScalarType::Void => Self::Void, + SlangScalarType::Bool => Self::Bool, + SlangScalarType::Int32 => Self::Int32, + SlangScalarType::Uint32 => Self::Uint32, + SlangScalarType::Int64 => Self::Int64, + SlangScalarType::Uint64 => Self::Uint64, + SlangScalarType::Float16 => Self::Float16, + SlangScalarType::Float32 => Self::Float32, + SlangScalarType::Float64 => Self::Float64, + SlangScalarType::Int8 => Self::Int8, + SlangScalarType::Uint8 => Self::Uint8, + SlangScalarType::Int16 => Self::Int16, + SlangScalarType::Uint16 => Self::Uint16, + SlangScalarType::Intptr => Self::Intptr, + SlangScalarType::Uintptr => Self::Uintptr, + SlangScalarType::Bfloat16 => Self::Bfloat16, + SlangScalarType::FloatE4m3 => Self::FloatE4m3, + SlangScalarType::FloatE5m2 => Self::FloatE5m2, + #[allow(unreachable_patterns)] + _ => unimplemented!("conversion from slang scalar type"), + } + } +} + +bitflags! { +#[derive(Debug, Copy, Clone)] +pub struct ResourceShape: u32 { + const BASE = shader_slang::ResourceShape::SlangResourceBaseShapeMask as u32; + const NONE = shader_slang::ResourceShape::SlangResourceNone as u32; + const TEXTURE_1D = shader_slang::ResourceShape::SlangTexture1d as u32; + const TEXTURE_2D = shader_slang::ResourceShape::SlangTexture2d as u32; + const TEXTURE_3D = shader_slang::ResourceShape::SlangTexture3d as u32; + const TEXTURE_CUBE = shader_slang::ResourceShape::SlangTextureCube as u32; + const TEXTURE_BUFFER = shader_slang::ResourceShape::SlangTextureBuffer as u32; + const STRUCTURED_BUFFER = shader_slang::ResourceShape::SlangStructuredBuffer as u32; + const BYTE_ADDRESS_BUFFER = shader_slang::ResourceShape::SlangByteAddressBuffer as u32; + const UNKNOWN = shader_slang::ResourceShape::SlangResourceUnknown as u32; + const ACCELERATION_STRUCTURE = shader_slang::ResourceShape::SlangAccelerationStructure as u32; + const TEXTURE_SUBPASS = shader_slang::ResourceShape::SlangTextureSubpass as u32; + const EXT_SHAPE = shader_slang::ResourceShape::SlangResourceExtShapeMask as u32; + const TEXTURE_FEEDBACK_FLAG = shader_slang::ResourceShape::SlangTextureFeedbackFlag as u32; + const TEXTURE_SHADOW_FLAG = shader_slang::ResourceShape::SlangTextureShadowFlag as u32; + const TEXTURE_ARRAY_FLAG = shader_slang::ResourceShape::SlangTextureArrayFlag as u32; + const TEXTURE_MULTISAMPLE_FLAG = shader_slang::ResourceShape::SlangTextureMultisampleFlag as u32; + const TEXTURE_COMBINED_FLAG = shader_slang::ResourceShape::SlangTextureCombinedFlag as u32; + const TEXTURE_1D_ARRAY = shader_slang::ResourceShape::SlangTexture1dArray as u32; + const TEXTURE_2D_ARRAY = shader_slang::ResourceShape::SlangTexture2dArray as u32; + const TEXTURE_CUBE_ARRAY = shader_slang::ResourceShape::SlangTextureCubeArray as u32; + const TEXTURE_2D_MULTISAMPLE = shader_slang::ResourceShape::SlangTexture2dMultisample as u32; + const TEXTURE_2D_MULTISAMPLE_ARRAY = shader_slang::ResourceShape::SlangTexture2dMultisampleArray as u32; + const TEXTURE_SUBPASS_MULTISAMPLE = shader_slang::ResourceShape::SlangTextureSubpassMultisample as u32; +} +} + +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub enum ParameterCategory +{ + None, + Mixed, + ConstantBuffer, + ShaderResource, + UnorderedAccess, + VaryingInput, + VaryingOutput, + SamplerState, + Uniform, + DescriptorTableSlot, + SpecializationConstant, + PushConstantBuffer, + RegisterSpace, + Generic, + RayPayload, + HitAttributes, + CallablePayload, + ShaderRecord, + ExistentialTypeParam, + ExistentialObjectParam, + SubElementRegisterSpace, + Subpass, + MetalArgumentBufferElement, + MetalAttribute, + MetalPayload, + Count, +} + +impl ParameterCategory +{ + fn from_slang_parameter_category(parameter_category: SlangParameterCategory) -> Self + { + match parameter_category { + SlangParameterCategory::None => Self::None, + SlangParameterCategory::Mixed => Self::Mixed, + SlangParameterCategory::ConstantBuffer => Self::ConstantBuffer, + SlangParameterCategory::ShaderResource => Self::ShaderResource, + SlangParameterCategory::UnorderedAccess => Self::UnorderedAccess, + SlangParameterCategory::VaryingInput => Self::VaryingInput, + SlangParameterCategory::VaryingOutput => Self::VaryingOutput, + SlangParameterCategory::SamplerState => Self::SamplerState, + SlangParameterCategory::Uniform => Self::Uniform, + SlangParameterCategory::DescriptorTableSlot => Self::DescriptorTableSlot, + SlangParameterCategory::SpecializationConstant => { + Self::SpecializationConstant + } + SlangParameterCategory::PushConstantBuffer => Self::PushConstantBuffer, + SlangParameterCategory::RegisterSpace => Self::RegisterSpace, + SlangParameterCategory::Generic => Self::Generic, + SlangParameterCategory::RayPayload => Self::RayPayload, + SlangParameterCategory::HitAttributes => Self::HitAttributes, + SlangParameterCategory::CallablePayload => Self::CallablePayload, + SlangParameterCategory::ShaderRecord => Self::ShaderRecord, + SlangParameterCategory::ExistentialTypeParam => Self::ExistentialTypeParam, + SlangParameterCategory::ExistentialObjectParam => { + Self::ExistentialObjectParam + } + SlangParameterCategory::SubElementRegisterSpace => { + Self::SubElementRegisterSpace + } + SlangParameterCategory::Subpass => Self::Subpass, + SlangParameterCategory::MetalArgumentBufferElement => { + Self::MetalArgumentBufferElement + } + SlangParameterCategory::MetalAttribute => Self::MetalAttribute, + SlangParameterCategory::MetalPayload => Self::MetalPayload, + SlangParameterCategory::Count => Self::Count, + } + } +} + +pub struct Blob +{ + inner: SlangBlob, +} + +impl Blob +{ + pub fn as_bytes(&self) -> &[u8] + { + self.inner.as_slice() + } + + pub fn as_str(&self) -> Result<&str, Utf8Error> + { + self.inner.as_str() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum Stage +{ + None, + Vertex, + Hull, + Domain, + Geometry, + Fragment, + Compute, + RayGeneration, + Intersection, + AnyHit, + ClosestHit, + Miss, + Callable, + Mesh, + Amplification, + Dispatch, + Count, +} + +impl Stage +{ + fn from_slang_stage(stage: shader_slang::Stage) -> Self + { + match stage { + shader_slang::Stage::None => Self::None, + shader_slang::Stage::Vertex => Self::Vertex, + shader_slang::Stage::Hull => Self::Hull, + shader_slang::Stage::Domain => Self::Domain, + shader_slang::Stage::Geometry => Self::Geometry, + shader_slang::Stage::Fragment => Self::Fragment, + shader_slang::Stage::Compute => Self::Compute, + shader_slang::Stage::RayGeneration => Self::RayGeneration, + shader_slang::Stage::Intersection => Self::Intersection, + shader_slang::Stage::AnyHit => Self::AnyHit, + shader_slang::Stage::ClosestHit => Self::ClosestHit, + shader_slang::Stage::Miss => Self::Miss, + shader_slang::Stage::Callable => Self::Callable, + shader_slang::Stage::Mesh => Self::Mesh, + shader_slang::Stage::Amplification => Self::Amplification, + shader_slang::Stage::Dispatch => Self::Dispatch, + shader_slang::Stage::Count => Self::Count, + } + } +} + +builder! { +#[builder(name = SettingsBuilder, derives=(Debug))] +#[derive(Debug)] +#[non_exhaustive] +pub struct Settings +{ + link_entrypoints: EntrypointFlags, +} +} + +#[derive(Sole)] +pub struct Context +{ + _global_session: SlangGlobalSession, + session: SlangSession, + modules: HashMap<AssetId, Module>, + programs: HashMap<AssetId, Program>, +} + +impl Context +{ + pub fn get_module(&self, asset_id: &AssetId) -> Option<&Module> + { + self.modules.get(asset_id) + } + + pub fn get_program(&self, asset_id: &AssetId) -> Option<&Program> + { + self.programs.get(asset_id) + } + + #[tracing::instrument(skip_all, fields(module_file = module.file_path()))] + pub fn compose_into_program( + &self, + module: Module, + link_entrypoints: EntrypointFlags, + ) -> Result<Program, ComposeProgramError> + { + let entry_points = link_entrypoints + .iter() + .filter_map(|entrypoint_flag| { + let entrypoint_name = bitflags_match!(entrypoint_flag, { + EntrypointFlags::VERTEX => Some("vertex_main"), + EntrypointFlags::FRAGMENT => Some("fragment_main"), + _ => None + })?; + + let Some(entry_point) = module.get_entry_point(entrypoint_name) else { + return Some(Err(ComposeProgramError::EntrypointNotFoundError { + entrypoint_name, + })); + }; + + Some(Ok(entry_point)) + }) + .collect::<Result<Vec<_>, _>>()?; + + let components = entry_points + .into_iter() + .map(|entry_point| SlangComponentType::from(entry_point.inner)) + .chain([SlangComponentType::from(module.inner)]) + .collect::<Vec<_>>(); + + let program = self + .session + .create_composite_component_type(&components) + .map_err(|err| ComposeProgramError::Other(Error(err)))?; + + let vertex_desc = if link_entrypoints.contains(EntrypointFlags::VERTEX) { + // TODO: Do not have hard coded target here + let Ok(program_reflection) = program.layout(0) else { + unreachable!(); + }; + + let program_reflection = ProgramReflection { inner: program_reflection }; + + Some(VertexDescription::new( + &program_reflection + .get_entry_point_by_name("vertex_main") + .expect("Not possible"), + )?) + } else { + None + }; + + Ok(Program { + inner: program, + metadata: ProgramMetadata { vertex_desc }, + }) + } +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct ProgramMetadata +{ + /// If the program has a entry point in the vertex stage, this field will contain a + /// description of the vertex type passed to the entry point. + pub vertex_desc: Option<VertexDescription>, +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct VertexDescription +{ + pub inputs: Arc<[VertexInputDescription]>, +} + +impl VertexDescription +{ + #[tracing::instrument(skip_all, fields(vs_entry_point_name = vs_entrypoint.name()))] + pub fn new( + vs_entrypoint: &EntryPointReflection<'_>, + ) -> Result<Self, VertexDescriptionError> + { + if vs_entrypoint.stage() != Stage::Vertex { + return Err(VertexDescriptionError::EntrypointNotInVertexStage); + } + + let inputs = VarInputDfsIter::new(vs_entrypoint).inspect(|var_input| { + if var_input.type_layout.kind() != TypeKind::Struct { + return; + } + + let Some(semantic_name) = var_input.var_layout.semantic_name() else { + return; + }; + + tracing::warn!( + "Semantic name '{}' of '{}' will be inherited by it's fields", + semantic_name, + var_input.var_layout.name().unwrap_or("<unnamed>") + ); + }); + + let mut seen_inputs = HashSet::<VertexInputSemName>::new(); + + Ok(Self { + inputs: inputs + .filter(|var_input| var_input.type_layout.kind() != TypeKind::Struct) + .map(|var_input| { + let name = var_input.var_layout.name().unwrap_or("<unnamed>"); + + let semantic_name = + var_input.var_layout.semantic_name().ok_or_else(|| { + VertexDescriptionError::VertexInputMissingSemanticName { + name: name.to_owned(), + } + })?; + + let semantic_name = + VertexInputSemName::from_semantic_name(semantic_name); + + if seen_inputs.contains(&semantic_name) { + return Err( + VertexDescriptionError::VertexInputHasOccupiedSemanticName { + name: name.to_owned(), + }, + ); + } + + let scalar_type = match ( + var_input.type_layout.kind(), + var_input.type_layout.scalar_type(), + ) { + (TypeKind::Scalar, Some(scalar_type)) => scalar_type, + (TypeKind::Vector, Some(scalar_type)) => scalar_type, + _ => { + return Err( + VertexDescriptionError::UnsupportedVertexInputType { + name: name.to_owned(), + }, + ); + } + }; + + seen_inputs.insert(semantic_name.clone()); + + Ok(VertexInputDescription { + semantic_name, + index: var_input.index, + type_kind: var_input.type_layout.kind(), + scalar_type, + }) + }) + .collect::<Result<Vec<_>, _>>()? + .into(), + }) + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub struct VertexInputDescription +{ + pub semantic_name: VertexInputSemName, + pub index: usize, + pub type_kind: TypeKind, + pub scalar_type: ScalarType, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum VertexInputSemName +{ + Position, + Normal, + UvFromTopLeft, + Color, + Other(Box<str>), +} + +impl VertexInputSemName +{ + fn from_semantic_name(semantic_name: &str) -> Self + { + match semantic_name { + STD_VERT_IN_SEM_NAME_POSITION => Self::Position, + STD_VERT_IN_SEM_NAME_NORMAL => Self::Normal, + STD_VERT_IN_SEM_NAME_UV_FROM_TOP_LEFT => Self::UvFromTopLeft, + STD_VERT_IN_SEM_NAME_COLOR => Self::Color, + _ => Self::Other(semantic_name.to_lowercase().into_boxed_str()), + } + } + + pub fn matches_vertex_label(&self, vertex_label: &VertexLabel) -> bool + { + match (self, vertex_label) { + (Self::Position, VertexLabel::Position) + | (Self::Normal, VertexLabel::Normal) + | (Self::UvFromTopLeft, VertexLabel::UvFromTopLeft) + | (Self::Color, VertexLabel::Color) => true, + (Self::Other(other), VertexLabel::Other(other_vertex_label)) => { + **other == *other_vertex_label + } + _ => false, + } + } +} + +impl Display for VertexInputSemName +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + match self { + Self::Position => formatter.write_str(STD_VERT_IN_SEM_NAME_POSITION), + Self::Normal => formatter.write_str(STD_VERT_IN_SEM_NAME_NORMAL), + Self::UvFromTopLeft => { + formatter.write_str(STD_VERT_IN_SEM_NAME_UV_FROM_TOP_LEFT) + } + Self::Color => formatter.write_str(STD_VERT_IN_SEM_NAME_COLOR), + Self::Other(other) => { + for character in other.chars() { + formatter.write_char(character.to_ascii_uppercase())?; + } + + Ok(()) + } + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum VertexDescriptionError +{ + #[error("Entrypoint is not in vertex stage")] + EntrypointNotInVertexStage, + + #[error("Type of vertex input '{name}' is not supported")] + UnsupportedVertexInputType + { + name: String + }, + + #[error("Vertex input '{name}' is missing a semantic name")] + VertexInputMissingSemanticName + { + name: String + }, + + #[error( + "Vertex input '{name}' has a semantic name already used by another vertex input" + )] + VertexInputHasOccupiedSemanticName + { + name: String + }, +} + +#[derive(Debug, thiserror::Error)] +pub enum ComposeProgramError +{ + #[error("Shader module does not have a '{entrypoint_name}' entry point")] + EntrypointNotFoundError + { + entrypoint_name: &'static str + }, + + #[error("Failed to create vertex description")] + VertexDescriptionCreationFailed(#[from] VertexDescriptionError), + + #[error(transparent)] + Other(Error), +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct Error(#[from] shader_slang::Error); + +struct VarInputDfsIter<'a> +{ + stack: Vec<(VariableLayout<'a>, TypeLayout<'a>, usize)>, +} + +impl<'a> VarInputDfsIter<'a> +{ + fn new(entry_point: &EntryPointReflection<'a>) -> Self + { + Self { + stack: entry_point + .parameters() + .map(|param| { + let Some(param_type_layout) = param.type_layout() else { + // I do not know in which cases type_layout can return None + unimplemented!(); + }; + + (param, param_type_layout, 0) + }) + .collect(), + } + } +} + +impl<'a> Iterator for VarInputDfsIter<'a> +{ + type Item = VarInput<'a>; + + fn next(&mut self) -> Option<Self::Item> + { + let (var_layout, type_layout, index) = loop { + let (var_layout, type_layout, acc_offset) = self.stack.pop()?; + + if let Some(offset) = var_layout.varying_input_offset() { + break (var_layout, type_layout, acc_offset + offset); + } + }; + + if type_layout.kind() == TypeKind::Struct { + self.stack.extend(type_layout.fields().map(|field| { + let Some(field_type_layout) = field.type_layout() else { + // I do not know in which cases type_layout can return + // None + unimplemented!(); + }; + + (field, field_type_layout, index) + })); + } + + Some(VarInput { var_layout, type_layout, index }) + } +} + +struct VarInput<'a> +{ + var_layout: VariableLayout<'a>, + type_layout: TypeLayout<'a>, + index: usize, +} + +fn import_slang_asset( + asset_submitter: &mut AssetSubmitter<'_>, + file_path: &Path, + settings: Option<&'_ Settings>, +) -> Result<(), ImportError> +{ + let file_name = file_path + .file_name() + .ok_or(ImportError::NoPathFileName)? + .to_str() + .ok_or(ImportError::PathFileNameNotUtf8)?; + + let file_path_canonicalized = file_path + .canonicalize() + .map_err(ImportError::CanonicalizePathFailed)?; + + asset_submitter.submit_store(ModuleSource { + name: file_name.to_owned().into(), + file_path: file_path_canonicalized.into(), + source: std::fs::read_to_string(file_path) + .map_err(ImportError::ReadFileFailed)? + .into(), + link_entrypoints: settings + .map(|settings| settings.link_entrypoints) + .unwrap_or_default(), + }); + + Ok(()) +} + +#[derive(Debug, thiserror::Error)] +enum ImportError +{ + #[error("Failed to read file")] + ReadFileFailed(#[source] std::io::Error), + + #[error("Asset path does not have a file name")] + NoPathFileName, + + #[error("Asset path file name is not valid UTF8")] + PathFileNameNotUtf8, + + #[error("Failed to canonicalize asset path")] + CanonicalizePathFailed(#[source] std::io::Error), +} + +declare_entity! { +pub IMPORT_SHADERS_PHASE: (Phase, pair!(ChildOf, { *HANDLE_ASSETS_PHASE })); +} + +pub(super) fn prepare(collector: &mut crate::ecs::extension::Collector<'_>) +{ + let Some(global_session) = SlangGlobalSession::new() else { + tracing::error!("Unable to create global shader-slang session"); + return; + }; + + let session_options = shader_slang::CompilerOptions::default() + .optimization(shader_slang::OptimizationLevel::None) + .matrix_layout_row(true) + .debug_information(SlangDebugInfoLevel::Maximal) + .no_mangle(true); + + let target_desc = shader_slang::TargetDesc::default() + .format(shader_slang::CompileTarget::Glsl) + // .format(shader_slang::CompileTarget::Spirv) + .profile(global_session.find_profile("glsl_330")); + // .profile(global_session.find_profile("spirv_1_5")); + + let targets = [target_desc]; + + let session_desc = shader_slang::SessionDesc::default() + .targets(&targets) + .search_paths(&[""]) + .options(&session_options); + + let Some(session) = global_session.create_session(&session_desc) else { + tracing::error!("Failed to create shader-slang session"); + return; + }; + + collector + .add_sole(Context { + _global_session: global_session, + session, + modules: HashMap::new(), + programs: HashMap::new(), + }) + .ok(); + + collector.spawn_declared_entity(&IMPORT_SHADERS_PHASE); + + collector.add_system(*INIT_PHASE, initialize); + collector.add_system(*IMPORT_SHADERS_PHASE, load_modules); +} + +fn initialize(mut assets: Single<Assets>) -> Result<(), crate::Error> +{ + let assets = assets.get_mut()?; + + assets.set_importer::<_, _>(["slang"], import_slang_asset); + + Ok(()) +} + +#[tracing::instrument(skip_all)] +fn load_modules( + mut context: Single<Context>, + assets: Single<Assets>, +) -> Result<(), crate::Error> +{ + let Ok(context) = context.get_mut() else { + unreachable!(); + }; + + let assets = assets.get()?; + + for AssetEvent::Stored(asset_id, asset_label) in assets.events().last_tick_events() { + let asset_handle = AssetHandle::<ModuleSource>::from_id(*asset_id); + + if !assets.is_loaded_and_has_type(&asset_handle) { + continue; + } + + let Some(module_source) = assets.get(&asset_handle) else { + unreachable!(); + }; + + tracing::debug!(asset_label=?asset_label, "Loading shader module"); + + let module = match load_module(&context.session, module_source) { + Ok(module) => module, + Err(err) => { + tracing::error!("Failed to load shader module: {err}"); + continue; + } + }; + + context.modules.insert(*asset_id, module.clone()); + + if !module_source.link_entrypoints.is_empty() { + assert!(context.programs.get(asset_id).is_none()); + + let shader_program = match context + .compose_into_program(module, module_source.link_entrypoints) + { + Ok(shader_program) => shader_program, + Err(err) => { + tracing::error!( + "Failed to compose shader into program: {:#}", + crate::Error::new(err) + ); + continue; + } + }; + + let linked_shader_program = match shader_program.into_linked() { + Ok(linked_shader_program) => linked_shader_program, + Err(err) => { + tracing::error!("Failed to link shader: {err}"); + continue; + } + }; + + context.programs.insert(*asset_id, linked_shader_program); + } + } + + Ok(()) +} + +fn load_module( + session: &SlangSession, + module_source: &ModuleSource, +) -> Result<Module, Error> +{ + let module = session.load_module_from_source_string( + &module_source.name, + &module_source.file_path.to_string_lossy(), + &module_source.source, + )?; + + Ok(Module { inner: module }) +} diff --git a/engine/src/rendering/shader/cursor.rs b/engine/src/rendering/shader/cursor.rs new file mode 100644 index 0000000..49f6b47 --- /dev/null +++ b/engine/src/rendering/shader/cursor.rs @@ -0,0 +1,372 @@ +use std::borrow::Cow; +use std::fmt::Display; +use std::hint::cold_path; + +use circular_buffer::FixedCircularBuffer; + +use crate::color::{Color, Rgb, Rgba}; +use crate::data_types::matrix::Matrix; +use crate::data_types::vector::Vec3; +use crate::rendering::object::Id as RenderingObjectId; +use crate::rendering::shader::{ + ResourceShape, + ScalarType, + TypeKind, + TypeLayout, + VariableLayout, +}; + +/// Shader cursor +#[derive(Clone)] +pub struct Cursor<'a> +{ + type_layout: TypeLayout<'a>, + binding_location: BindingLocation, + location_path: LocationPath, +} + +impl<'a> Cursor<'a> +{ + pub fn new(var_layout: VariableLayout<'a>) -> Self + { + let binding_location = BindingLocation { + binding_index: var_layout.binding_index(), + binding_size: 0, + byte_offset: var_layout.offset(), + }; + + Self { + type_layout: var_layout.type_layout().unwrap(), + binding_location, + location_path: LocationPath::default(), + } + } + + pub fn field(&self, name: impl Into<Cow<'static, str>>) -> Self + { + let name = name.into(); + + let Some(field_var_layout) = self.type_layout.get_field_by_name(name.as_ref()) + else { + panic!("Field '{name}' does not exist"); + }; + + let field_type_kind = field_var_layout.ty().unwrap().kind(); + + let (new_var_layout, binding_index_offset) = match field_type_kind { + TypeKind::ConstantBuffer => { + let elem_var_layout = field_var_layout + .type_layout() + .expect("Constant buffer field has no type layout") + .element_var_layout() + .expect( + "Constant buffer field type layout has no element var layout", + ); + + ( + elem_var_layout, + field_var_layout.binding_index() + elem_var_layout.binding_index(), + ) + } + TypeKind::Array + | TypeKind::Matrix + | TypeKind::Scalar + | TypeKind::Vector + | TypeKind::Struct + | TypeKind::Resource => (field_var_layout, field_var_layout.binding_index()), + type_kind => unimplemented!("Type kind {type_kind:?} is not yet supported"), + }; + + let mut location_path = self.location_path.clone(); + + location_path.push(Location::Field(name)); + + Self { + type_layout: new_var_layout.type_layout().unwrap(), + binding_location: BindingLocation { + binding_index: self.binding_location.binding_index + binding_index_offset, + binding_size: if field_type_kind == TypeKind::ConstantBuffer { + new_var_layout + .type_layout() + .unwrap() + .uniform_size() + .unwrap() + } else { + self.binding_location.binding_size + }, + byte_offset: self.binding_location.byte_offset + new_var_layout.offset(), + }, + location_path, + } + } + + pub fn element(mut self, index: usize) -> Self + { + let element_type_layout = self.type_layout.element_type_layout().unwrap(); + + self.binding_location.byte_offset += index * element_type_layout.stride(); + + self.type_layout = element_type_layout; + + self.location_path.push(Location::Element(index)); + + self + } + + pub fn binding(self, value: BindingValue) -> Result<Binding, BindingError> + { + value.validate_for_shader_type(self.type_layout, self.location_path)?; + + Ok(Binding { + location: self.binding_location, + value, + }) + } +} + +/// Shader cursor location. +#[derive(Debug, Clone, Default)] +pub struct LocationPath +{ + locations: FixedCircularBuffer<Location, 16>, + is_truncated: bool, +} + +impl LocationPath +{ + fn push(&mut self, location: Location) + { + if self.locations.push_back(location).is_some() { + self.is_truncated = true; + } + } +} + +impl Display for LocationPath +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + if self.is_truncated { + write!(formatter, "(...)")?; + } + + for location in &self.locations { + match location { + Location::Field(field) => { + write!(formatter, ".{field}")?; + } + Location::Element(element) => { + write!(formatter, "[{element}]")?; + } + } + } + + Ok(()) + } +} + +/// Shader cursor location. +#[derive(Debug, Clone)] +pub enum Location +{ + Field(Cow<'static, str>), + Element(usize), +} + +impl Display for Location +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + match self { + Self::Field(field) => <Cow<'static, str> as Display>::fmt(field, formatter), + Self::Element(element) => <usize as Display>::fmt(element, formatter), + } + } +} + +#[derive(Debug, Clone)] +pub struct BindingLocation +{ + pub binding_index: u32, + pub binding_size: usize, + pub byte_offset: usize, +} + +#[derive(Debug, Clone)] +pub enum BindingValue +{ + Uint(u32), + Int(i32), + Float(f32), + FVec3(Vec3<f32>), + Color(Color<f32>), + FMat4x4(Matrix<f32, 4, 4>), + Texture(RenderingObjectId, BindingTextureKind), +} + +impl BindingValue +{ + fn validate_for_shader_type( + &self, + ty: TypeLayout<'_>, + location_path: LocationPath, + ) -> Result<(), BindingError> + { + let ty_kind = ty.kind(); + let scalar_ty = ty.scalar_type(); + + let element_scalar_ty = ty + .element_type_layout() + .and_then(|elem_ty| elem_ty.scalar_type()); + + let element_cnt = ty.element_cnt(); + + let is_valid = match self { + Self::Uint(_) => { + ty_kind == TypeKind::Scalar && scalar_ty == Some(ScalarType::Uint32) + } + Self::Int(_) => { + ty_kind == TypeKind::Scalar && scalar_ty == Some(ScalarType::Int32) + } + Self::Float(_) => { + ty_kind == TypeKind::Scalar && scalar_ty == Some(ScalarType::Float32) + } + Self::FVec3(_) => { + ty_kind == TypeKind::Vector + && element_scalar_ty == Some(ScalarType::Float32) + && element_cnt == Some(3) + } + Self::Color(Color::Rgb(_)) => { + ty_kind == TypeKind::Vector + && element_scalar_ty == Some(ScalarType::Float32) + && element_cnt == Some(3) + } + Self::Color(Color::Rgba(_)) => { + ty_kind == TypeKind::Vector + && element_scalar_ty == Some(ScalarType::Float32) + && element_cnt == Some(4) + } + Self::FMat4x4(_) => { + ty_kind == TypeKind::Matrix + && element_scalar_ty == Some(ScalarType::Float32) + && ty.row_cnt() == Some(4) + && ty.column_cnt() == Some(4) + } + Self::Texture(_, BindingTextureKind::Texture2D) => { + ty_kind == TypeKind::Resource + && ty.resource_shape().is_some_and(|res_shape| { + (res_shape & ResourceShape::BASE) + .contains(ResourceShape::TEXTURE_2D) + }) + } + Self::Texture(_, BindingTextureKind::Cube) => { + ty_kind == TypeKind::Resource + && ty.resource_shape().is_some_and(|res_shape| { + (res_shape & ResourceShape::BASE) + .contains(ResourceShape::TEXTURE_CUBE) + }) + } + }; + + if !is_valid { + cold_path(); + return Err(BindingError::IncorrectValueType { + value: self.clone(), + location_path, + }); + } + + Ok(()) + } +} + +impl From<u32> for BindingValue +{ + fn from(value: u32) -> Self + { + BindingValue::Uint(value) + } +} + +impl From<i32> for BindingValue +{ + fn from(value: i32) -> Self + { + BindingValue::Int(value) + } +} + +impl From<f32> for BindingValue +{ + fn from(value: f32) -> Self + { + BindingValue::Float(value) + } +} + +impl From<Vec3<f32>> for BindingValue +{ + fn from(vec: Vec3<f32>) -> Self + { + BindingValue::FVec3(vec) + } +} + +impl From<Color<f32>> for BindingValue +{ + fn from(color: Color<f32>) -> Self + { + BindingValue::Color(color) + } +} +impl From<Rgb<f32>> for BindingValue +{ + fn from(color: Rgb<f32>) -> Self + { + BindingValue::Color(color.into()) + } +} + +impl From<Rgba<f32>> for BindingValue +{ + fn from(color: Rgba<f32>) -> Self + { + BindingValue::Color(color.into()) + } +} + +impl From<Matrix<f32, 4, 4>> for BindingValue +{ + fn from(matrix: Matrix<f32, 4, 4>) -> Self + { + BindingValue::FMat4x4(matrix) + } +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct Binding +{ + pub location: BindingLocation, + pub value: BindingValue, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum BindingTextureKind +{ + Texture2D, + Cube, +} + +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum BindingError +{ + #[error("Value {value:?} has incorrect type for value at {location_path} in shader")] + IncorrectValueType + { + value: BindingValue, + location_path: LocationPath, + }, +} diff --git a/engine/src/scene.rs b/engine/src/scene.rs new file mode 100644 index 0000000..2a66c3f --- /dev/null +++ b/engine/src/scene.rs @@ -0,0 +1,8 @@ +use crate::ecs::Component; +use crate::reflection::Reflection; + +#[derive(Debug, Default, Clone, Copy, Component, Reflection)] +pub struct Scene; + +#[derive(Debug, Default, Clone, Copy, Component, Reflection)] +pub struct Active; diff --git a/engine/src/shader.rs b/engine/src/shader.rs deleted file mode 100644 index 89f7b7c..0000000 --- a/engine/src/shader.rs +++ /dev/null @@ -1,186 +0,0 @@ -use std::collections::hash_map::DefaultHasher; -use std::fs::read_to_string; -use std::hash::{Hash, Hasher}; -use std::path::{Path, PathBuf}; - -use ecs::Component; - -use crate::shader_preprocessor::{Error as ShaderPreprocessorError, ShaderPreprocessor}; - -const VERTEX_SHADER_FILE: &str = "vertex.glsl"; -const FRAGMENT_SHADER_FILE: &str = "fragment.glsl"; - -const SHADER_DIR: &str = "engine"; - -/// Shader program -#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Component)] -pub struct Program -{ - shaders: Vec<Shader>, -} - -impl Program -{ - /// Creates a new shader program with the default shaders. - /// - /// # Errors - /// Returns `Err` if: - /// - Reading a default shader file Fails - /// - Preprocessing a shader fails. - pub fn new() -> Result<Self, Error> - { - let mut program = Self { shaders: Vec::new() }; - - program.push_shader( - Shader::read_shader_file( - Kind::Vertex, - &Path::new(SHADER_DIR).join(VERTEX_SHADER_FILE), - )? - .preprocess()?, - ); - - program.push_shader( - Shader::read_shader_file( - Kind::Fragment, - &Path::new(SHADER_DIR).join(FRAGMENT_SHADER_FILE), - )? - .preprocess()?, - ); - - Ok(program) - } - - pub fn push_shader(&mut self, shader: Shader) - { - self.shaders.push(shader); - } - - pub fn append_shaders(&mut self, shaders: impl IntoIterator<Item = Shader>) - { - self.shaders.extend(shaders); - } - - #[must_use] - pub fn shaders(&self) -> &[Shader] - { - &self.shaders - } - - pub(crate) fn u64_hash(&self) -> u64 - { - let mut hasher = DefaultHasher::new(); - - self.hash(&mut hasher); - - hasher.finish() - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct Shader -{ - kind: Kind, - source: String, - file: PathBuf, -} - -impl Shader -{ - /// Reads a shader from the specified source file. - /// - /// # Errors - /// Will return `Err` if: - /// - Reading the file fails - /// - The shader source is not ASCII - pub fn read_shader_file(kind: Kind, shader_file: &Path) -> Result<Self, Error> - { - let source = read_to_string(shader_file).map_err(|err| Error::ReadFailed { - source: err, - shader_file: shader_file.to_path_buf(), - })?; - - if !source.is_ascii() { - return Err(Error::SourceNotAscii); - } - - Ok(Self { - kind, - source, - file: shader_file.to_path_buf(), - }) - } - - /// Preprocesses the shaders. - /// - /// # Errors - /// Returns `Err` if preprocessing fails. - pub fn preprocess(self) -> Result<Self, Error> - { - let shader_preprocessor = ShaderPreprocessor::new( - self.file - .parent() - .ok_or(Error::SourcePathHasNoParent)? - .to_path_buf(), - ); - - let source_preprocessed = shader_preprocessor - .preprocess(self.source, &self.file) - .map_err(|err| Error::PreprocessFailed { - source: err, - shader_file: self.file.clone(), - })?; - - Ok(Self { - kind: self.kind, - source: source_preprocessed, - file: self.file.clone(), - }) - } - - #[must_use] - pub fn kind(&self) -> Kind - { - self.kind - } - - #[must_use] - pub fn source(&self) -> &str - { - &self.source - } -} - -/// Shader kind. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Kind -{ - Vertex, - Fragment, -} - -/// Shader error -#[derive(Debug, thiserror::Error)] -pub enum Error -{ - #[error("Failed to read shader {}", shader_file.display())] - ReadFailed - { - #[source] - source: std::io::Error, - shader_file: PathBuf, - }, - - #[error("Shader source is not ASCII")] - SourceNotAscii, - - #[error("Failed to preprocess shader {}", shader_file.display())] - PreprocessFailed - { - #[source] - source: ShaderPreprocessorError, - shader_file: PathBuf, - }, - - #[error("Shader source path has no parent")] - SourcePathHasNoParent, -} diff --git a/engine/src/shader_preprocessor.rs b/engine/src/shader_preprocessor.rs deleted file mode 100644 index 70696ac..0000000 --- a/engine/src/shader_preprocessor.rs +++ /dev/null @@ -1,607 +0,0 @@ -use std::path::{Path, PathBuf}; -use std::string::FromUtf8Error; - -const PREINCLUDE_DIRECTIVE: &str = "#preinclude"; - -/// Preprocessor for shaders written in the OpenGL Shading Language. -pub struct ShaderPreprocessor -{ - read_file: fn(&Path) -> Result<Vec<u8>, std::io::Error>, - base_dir: PathBuf, -} - -impl ShaderPreprocessor -{ - pub fn new(base_dir: PathBuf) -> Self - { - Self { - read_file: |path| std::fs::read(path), - base_dir, - } - } - - pub fn preprocess( - &self, - shader_content: String, - shader_file_path: &Path, - ) -> Result<String, Error> - { - let mut preincludes = shader_content - .match_indices(PREINCLUDE_DIRECTIVE) - .peekable(); - - if preincludes.peek().is_none() { - // Shader content contains no preincludes - return Ok(shader_content); - }; - - let mut preprocessed = shader_content.clone(); - - let mut curr = shader_content.find(PREINCLUDE_DIRECTIVE); - - let mut last_start = 0; - let mut span_line_offset = 0; - - while let Some(preinclude_start) = curr { - let replacement_job = self.handle_preinclude( - &preprocessed, - shader_file_path, - preinclude_start, - span_line_offset, - )?; - - let path = replacement_job.path.clone(); - - let mut included = String::from_utf8( - (self.read_file)(&self.base_dir.join(replacement_job.path.clone())) - .map_err(|err| Error::ReadIncludedShaderFailed { - source: err, - path: replacement_job.path.clone(), - })?, - ) - .map_err(|err| Error::IncludedShaderInvalidUtf8 { - source: err, - path: path.clone(), - })?; - - if let Some(first_line) = included.lines().next() { - if first_line.starts_with("#version") { - included = included - .chars() - .skip_while(|character| *character != '\n') - .collect(); - } - } - - let included_preprocessed = - self.preprocess(included, &replacement_job.path)?; - - let start = replacement_job.start_index; - let end = replacement_job.end_index; - - preprocessed.replace_range(start..end, &included_preprocessed); - - curr = preprocessed[last_start + 1..] - .find(PREINCLUDE_DIRECTIVE) - .map(|index| index + 1); - - last_start = preinclude_start + included_preprocessed.len(); - - span_line_offset += included_preprocessed.lines().count(); - } - - Ok(preprocessed) - } - - fn handle_preinclude( - &self, - shader_content: &str, - shader_file_path: &Path, - preinclude_start_index: usize, - span_line_offset: usize, - ) -> Result<ReplacementJob, Error> - { - let expect_token = |token: char, index: usize| { - let token_found = shader_content.chars().nth(index).ok_or_else(|| { - Error::ExpectedToken { - expected: token, - span: Span::new( - shader_content, - &self.base_dir.join(shader_file_path), - index, - span_line_offset, - preinclude_start_index, - ), - } - })?; - - if token_found != token { - return Err(Error::InvalidToken { - expected: token, - found: token_found, - span: Span::new( - shader_content, - &self.base_dir.join(shader_file_path), - index, - span_line_offset, - preinclude_start_index, - ), - }); - } - - Ok(()) - }; - - let space_index = preinclude_start_index + PREINCLUDE_DIRECTIVE.len(); - let quote_open_index = space_index + 1; - - expect_token(' ', space_index)?; - expect_token('"', quote_open_index)?; - - let buf = shader_content[quote_open_index + 1..] - .chars() - .take_while(|character| *character != '"') - .map(|character| character as u8) - .collect::<Vec<_>>(); - - if buf.is_empty() { - return Err(Error::ExpectedToken { - expected: '"', - span: Span::new( - shader_content, - &self.base_dir.join(shader_file_path), - shader_content.len() - 1, - span_line_offset, - preinclude_start_index, - ), - }); - } - - let path_len = buf.len(); - - let path = PathBuf::from(String::from_utf8(buf).map_err(|err| { - Error::PreincludePathInvalidUtf8 { - source: err, - span: Span::new( - shader_content, - &self.base_dir.join(shader_file_path), - quote_open_index + 1, - span_line_offset, - preinclude_start_index, - ), - } - })?); - - Ok(ReplacementJob { - start_index: preinclude_start_index, - end_index: quote_open_index + 1 + path_len + 1, - path, - }) - } -} - -struct ReplacementJob -{ - start_index: usize, - end_index: usize, - path: PathBuf, -} - -/// Shader preprocessing error. -#[derive(Debug, thiserror::Error)] -pub enum Error -{ - #[error( - "Invalid token at line {}, column {} of {}. Expected '{}', found '{}'", - span.line, - span.column, - span.file.display(), - expected, - found - )] - InvalidToken - { - expected: char, - found: char, - span: Span, - }, - - #[error( - "Expected token '{}' at line {}, column {} of {}. Found eof", - expected, - span.line, - span.column, - span.file.display(), - )] - ExpectedToken - { - expected: char, span: Span - }, - - #[error( - "Preinclude path at line {}, column {} of {} is invalid UTF-8", - span.line, - span.column, - span.file.display(), - )] - PreincludePathInvalidUtf8 - { - #[source] - source: FromUtf8Error, - span: Span, - }, - - #[error("Failed to read included shader")] - ReadIncludedShaderFailed - { - #[source] - source: std::io::Error, - path: PathBuf, - }, - - #[error("Included shader is not valid UTF-8")] - IncludedShaderInvalidUtf8 - { - #[source] - source: FromUtf8Error, - path: PathBuf, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Span -{ - line: usize, - column: usize, - file: PathBuf, -} - -impl Span -{ - fn new( - file_content: &str, - file_path: &Path, - char_index: usize, - line_offset: usize, - line_start_index: usize, - ) -> Self - { - let line = find_line_of_index(file_content, char_index) + 1 - - line_offset.saturating_sub(1); - - Self { - line, - column: char_index - line_start_index + 1, - file: file_path.to_path_buf(), - } - } -} - -fn find_line_of_index(text: &str, index: usize) -> usize -{ - text.chars() - .take(index + 1) - .enumerate() - .filter(|(_, character)| *character == '\n') - .count() -} - -#[cfg(test)] -mod tests -{ - use std::ffi::OsStr; - use std::path::{Path, PathBuf}; - - use super::{Error, ShaderPreprocessor}; - - #[test] - fn preprocess_no_directives_is_same() - { - assert_eq!( - ShaderPreprocessor { - read_file: |_| { unreachable!() }, - base_dir: PathBuf::new() - } - .preprocess("#version 330 core\n".to_string(), Path::new("foo.glsl"),) - .unwrap(), - "#version 330 core\n".to_string() - ); - } - - #[test] - fn preprocess_with_directives_works() - { - assert_eq!( - ShaderPreprocessor { - read_file: |_| { Ok(b"out vec4 FragColor;".to_vec()) }, - base_dir: PathBuf::new() - } - .preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"foo.glsl\"\n", - "\n", - "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), - ) - .unwrap(), - concat!( - "#version 330 core\n", - "\n", - "out vec4 FragColor;\n", - "\n", - "void main() {}", - ) - ); - - assert_eq!( - ShaderPreprocessor { - read_file: |_| { Ok(b"out vec4 FragColor;".to_vec()) }, - base_dir: PathBuf::new() - } - .preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), - ) - .unwrap(), - concat!( - "#version 330 core\n", - "\n", - "out vec4 FragColor;\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ) - ); - - assert_eq!( - ShaderPreprocessor { - read_file: |path| { - if path == OsStr::new("bar.glsl") { - Ok(b"out vec4 FragColor;".to_vec()) - } else { - Ok(concat!( - "uniform sampler2D input_texture;\n", - "in vec2 in_texture_coords;" - ) - .as_bytes() - .to_vec()) - } - }, - base_dir: PathBuf::new() - } - .preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "#preinclude \"foo.glsl\"\n", - "\n", - "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), - ) - .unwrap(), - concat!( - "#version 330 core\n", - "\n", - "out vec4 FragColor;\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "uniform sampler2D input_texture;\n", - "in vec2 in_texture_coords;\n", - "\n", - "void main() {}", - ) - ); - } - - #[test] - fn preprocess_invalid_shader_does_not_work() - { - let path = Path::new("foo.glsl"); - - let res = ShaderPreprocessor { - read_file: |_| Ok(b"out vec4 FragColor;".to_vec()), - base_dir: PathBuf::new(), - } - .preprocess( - concat!( - "#version 330 core\n", - "\n", - // Missing " - "#preinclude foo.glsl\"\n", - "\n", - "void main() {}", - ) - .to_string(), - path, - ); - - let Err(Error::InvalidToken { expected, found, span }) = res else { - panic!( - "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}" - ); - }; - - assert_eq!(expected, '"'); - assert_eq!(found, 'f'); - assert_eq!(span.line, 3); - assert_eq!(span.column, 13); - assert_eq!(span.file, path); - } - - #[test] - fn preprocess_error_has_correct_span() - { - let path = Path::new("foo.glsl"); - - let res = ShaderPreprocessor { - read_file: |path| { - if path == OsStr::new("bar.glsl") { - Ok(concat!( - "out vec4 FragColor;\n", - "in vec2 in_texture_coords;\n", - "in float foo;" - ) - .as_bytes() - .to_vec()) - } else { - Ok(b"uniform sampler2D input_texture;".to_vec()) - } - }, - base_dir: PathBuf::new(), - } - .preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "#preinclude\"foo.glsl\"\n", - "\n", - "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), - ); - - let Err(Error::InvalidToken { expected, found, span }) = res else { - panic!( - "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}" - ); - }; - - assert_eq!(expected, ' '); - assert_eq!(found, '"'); - assert_eq!(span.line, 7); - assert_eq!(span.column, 12); - assert_eq!(span.file, path); - } - - #[test] - fn preprocess_included_shader_with_include_works() - { - assert_eq!( - ShaderPreprocessor { - read_file: |path| { - if path == OsStr::new("bar.glsl") { - Ok(concat!( - "#preinclude \"foo.glsl\"\n", - "\n", - "out vec4 FragColor;" - ) - .as_bytes() - .to_vec()) - } else { - Ok(concat!( - "uniform sampler2D input_texture;\n", - "in vec2 in_texture_coords;" - ) - .as_bytes() - .to_vec()) - } - }, - base_dir: PathBuf::new() - } - .preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), - ) - .unwrap(), - concat!( - "#version 330 core\n", - "\n", - "uniform sampler2D input_texture;\n", - "in vec2 in_texture_coords;\n", - "\n", - "out vec4 FragColor;\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ) - ); - } - - #[test] - fn preprocess_included_shader_with_include_error_span_is_correct() - { - let res = ShaderPreprocessor { - read_file: |path| { - if path == OsStr::new("bar.glsl") { - Ok(concat!( - // ' instead of " - "#preinclude 'foo.glsl\"\n", - "\n", - "out vec4 FragColor;" - ) - .as_bytes() - .to_vec()) - } else { - Ok(concat!( - "uniform sampler2D input_texture;\n", - "in vec2 in_texture_coords;" - ) - .as_bytes() - .to_vec()) - } - }, - base_dir: PathBuf::new(), - } - .preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), - ); - - let Err(Error::InvalidToken { expected, found, span }) = res else { - panic!( - "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}" - ); - }; - - assert_eq!(expected, '"'); - assert_eq!(found, '\''); - assert_eq!(span.line, 1); - assert_eq!(span.column, 13); - assert_eq!(span.file, Path::new("bar.glsl")); - } -} diff --git a/engine/src/sky_box.rs b/engine/src/sky_box.rs new file mode 100644 index 0000000..ceb82df --- /dev/null +++ b/engine/src/sky_box.rs @@ -0,0 +1,11 @@ +use crate::asset::Handle as AssetHandle; +use crate::ecs::Component; +use crate::reflection::Reflection; +use crate::texture::{CubeMapFace, Texture}; + +#[derive(Debug, Clone, Component, Reflection)] +#[non_exhaustive] +pub enum SkyBox +{ + AssetPerCubeMapFace([(CubeMapFace, AssetHandle<Texture>); 6]), +} diff --git a/engine/src/texture.rs b/engine/src/texture.rs index f82b59d..8361f12 100644 --- a/engine/src/texture.rs +++ b/engine/src/texture.rs @@ -1,152 +1,45 @@ -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; - -static NEXT_ID: AtomicU32 = AtomicU32::new(0); - -mod reexports -{ - pub use crate::opengl::texture::{Filtering, Wrapping}; -} - -pub use reexports::*; +use crate::asset::{Assets, Submitter as AssetSubmitter}; +use crate::builder; +use crate::color::Rgba; +use crate::image::{Error as ImageError, Image}; #[derive(Debug, Clone)] -pub struct Texture +pub enum Texture { - id: Id, - image: DynamicImage, - pixel_data_format: PixelDataFormat, - dimensions: Dimens<u32>, - properties: Properties, + Texture2D(Tex2D), + CubeMap(TexCubeMap), } -impl Texture +#[derive(Debug, Clone)] +pub struct Tex2D { - /// Opens a texture image. - /// - /// # Errors - /// Will return `Err` if: - /// - Opening the image fails - /// - The image data is not 8-bit/color RGB - #[allow(clippy::new_without_default)] - pub fn open(path: &Path) -> Result<Self, 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::UnsupportedImageDataKind); - } - }; - - let dimensions = Dimens { - width: image.width(), - height: image.height(), - }; - - let id = NEXT_ID.fetch_add(1, Ordering::Relaxed); - - Ok(Self { - id: Id::new(id), - image, - pixel_data_format, - dimensions, - properties: Properties::default(), - }) - } - - #[must_use] - pub fn new_from_color(dimensions: &Dimens<u32>, color: &Color<u8>) -> 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); - - Self { - id: Id::new(id), - image: image.into(), - pixel_data_format: PixelDataFormat::Rgb8, - dimensions: *dimensions, - properties: Properties::default(), - } - } - - #[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 - } + pub image: Image, + pub properties: Properties, } -impl Drop for Texture +#[derive(Debug, Clone)] +pub struct TexCubeMap { - fn drop(&mut self) - { - NEXT_ID.fetch_sub(1, Ordering::Relaxed); - } + pub images: [(CubeMapFace, Image); 6], + pub properties: Properties, } -/// Texture error. -#[derive(Debug, thiserror::Error)] -pub enum Error +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum CubeMapFace { - #[error("Failed to open texture image")] - OpenImageFailed(#[source] std::io::Error), - - #[error("Failed to decode texture image")] - DecodeImageFailed(#[source] ImageError), - - #[error("Unsupported image data kind")] - UnsupportedImageDataKind, + PositiveX, + NegativeX, + PositiveY, + NegativeY, + PositiveZ, + NegativeZ, } +builder! { /// Texture properties +#[builder(name = PropertiesBuilder, derives=(Debug, Clone))] #[derive(Debug, Clone)] #[non_exhaustive] pub struct Properties @@ -154,6 +47,16 @@ pub struct Properties pub wrap: Wrapping, pub magnifying_filter: Filtering, pub minifying_filter: Filtering, + pub border: Option<Border>, +} +} + +impl Properties +{ + pub fn builder() -> PropertiesBuilder + { + PropertiesBuilder::default() + } } impl Default for Properties @@ -164,34 +67,88 @@ impl Default for Properties wrap: Wrapping::Repeat, magnifying_filter: Filtering::Linear, minifying_filter: Filtering::Nearest, + border: None, } } } -/// Texture ID. +impl Default for PropertiesBuilder +{ + fn default() -> Self + { + Properties::default().into() + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Id +#[non_exhaustive] +pub enum Filtering { - id: u32, + Nearest, + Linear, } -impl Id +/// Texture wrapping. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum Wrapping { - fn new(id: u32) -> Self - { - Self { id } - } + Repeat, + MirroredRepeat, + ClampToEdge, + ClampToBorder, +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum Border +{ + RgbaF32(Rgba<f32>), + Depth(f32), +} - pub(crate) fn into_inner(self) -> u32 +builder! { +#[builder(name = ImportSettingsBuilder, derives=(Debug, Clone))] +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct ImportSettings { + properties: Properties, +} +} + +impl ImportSettings +{ + pub fn builder() -> ImportSettingsBuilder { - self.id + ImportSettingsBuilder::default() } } -impl Display for Id +impl Default for ImportSettingsBuilder { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + fn default() -> Self { - self.id.fmt(formatter) + ImportSettings::default().into() } } + +pub(crate) fn initialize(assets: &mut Assets) +{ + assets.set_importer::<_, _>(["png", "jpg"], import); +} + +fn import( + asset_submitter: &mut AssetSubmitter<'_>, + path: &Path, + settings: Option<&'_ ImportSettings>, +) -> Result<(), ImageError> +{ + asset_submitter.submit_store(Texture::Texture2D(Tex2D { + image: Image::open(path)?, + properties: settings + .map(|settings| settings.properties.clone()) + .unwrap_or_default(), + })); + + Ok(()) +} diff --git a/engine/src/transform.rs b/engine/src/transform.rs index 5e5e296..8e768a6 100644 --- a/engine/src/transform.rs +++ b/engine/src/transform.rs @@ -1,43 +1,49 @@ -use ecs::Component; - +use crate::data_types::dimens::Dimens3; +use crate::ecs::Component; +use crate::matrix::Matrix; +use crate::reflection::Reflection; use crate::vector::Vec3; -/// A position in 3D space. -#[derive(Debug, Default, Clone, Copy, Component)] -pub struct Position +#[derive(Debug, Clone, Component, Reflection)] +#[non_exhaustive] +pub struct Transform { pub position: Vec3<f32>, + pub scale: Dimens3<f32>, } -impl From<Vec3<f32>> for Position +impl Transform { - fn from(position: Vec3<f32>) -> Self + pub fn position(mut self, position: Vec3<f32>) -> Self { - Self { position } + self.position = position; + self } -} -/// Scaling of a 3D object. -#[derive(Debug, Clone, Copy, Component)] -pub struct Scale -{ - pub scale: Vec3<f32>, -} + pub fn scale(mut self, scale: Dimens3<f32>) -> Self + { + self.scale = scale; + self + } -impl From<Vec3<f32>> for Scale -{ - fn from(scale: Vec3<f32>) -> Self + pub fn to_matrix(&self) -> Matrix<f32, 4, 4> { - Self { scale } + let mut matrix = Matrix::new_identity(); + + matrix.translate(&self.position); + matrix.scale(&self.scale); + + matrix } } -impl Default for Scale +impl Default for Transform { fn default() -> Self { Self { - scale: Vec3 { x: 1.0, y: 1.0, z: 1.0 }, + position: Vec3 { x: 0.0, y: 0.0, z: 0.0 }, + scale: Dimens3 { width: 1.0, height: 1.0, depth: 1.0 }, } } } diff --git a/engine/src/ui.rs b/engine/src/ui.rs new file mode 100644 index 0000000..626c810 --- /dev/null +++ b/engine/src/ui.rs @@ -0,0 +1,2 @@ +pub mod dear_imgui; +pub mod view; diff --git a/engine/src/ui/dear_imgui.rs b/engine/src/ui/dear_imgui.rs new file mode 100644 index 0000000..0a50ff7 --- /dev/null +++ b/engine/src/ui/dear_imgui.rs @@ -0,0 +1,1081 @@ +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::LazyLock; + +use dear_imgui_rs::{ + DrawCmd as ImguiDrawCmd, + Key as ImguiKey, + MouseButton as ImguiMouseButton, +}; +use ecs::component::local::Local; +use ecs::component::Component; +use ecs::error::Context as _; +use ecs::event::component::{Changed, EventMatchExt}; +use ecs::pair::Pair; +use ecs::query::term::With; +use ecs::sole::Single; +use ecs::system::initializable::Initializable; +use ecs::system::observer::Observe; +use ecs::system::Into; +use ecs::time::Time; +use ecs::{Component, Query, Sole}; +use intmap::IntMap; + +use crate::asset::{Assets, Handle as AssetHandle, Label as AssetLabel}; +use crate::data_types::dimens::Dimens; +use crate::input::keyboard::{Key, Keyboard}; +use crate::input::mouse::{Button as MouseButton, Buttons as MouseButtons, Mouse}; +use crate::mesh::vertex_buffer::{ + NamedVertexAttr as MeshNamedVertexAttr, + VertexAttrInfo as MeshVertexAttrInfo, + VertexBuffer as MeshVertexBuffer, + VertexLabel, +}; +use crate::mesh::{Mesh, VertexAttrType as MeshVertexAttrType}; +use crate::projection::{ + ClipVolume as ProjectionClipVolume, + Orthographic as OrthographicProjection, +}; +use crate::reflection::Reflection; +use crate::rendering::blending::{ + Config as RenderingBlendingConfiguration, + Equation as RenderingBlendingEquation, + Factor as RenderingBlendingFactor, +}; +use crate::rendering::object::Id as RenderingObjectId; +use crate::rendering::shader::cursor::{ + BindingTextureKind as ShaderBindingTextureKind, + BindingValue as ShaderBindingValue, + Cursor as ShaderCursor, +}; +use crate::rendering::shader::{ + Context as ShaderContext, + EntrypointFlags as ShaderEntrypointFlags, + ModuleSource as ShaderModuleSource, +}; +use crate::rendering::{ + AssetOrValue, + Command as RenderingCommand, + DrawMeshOptions, + DrawProperties, + DrawPropertiesUpdateFlags, + MeshUsage, + RenderPass, + RenderPasses, + ScissorBox, + SrgbaTextureDataType, + Surface, + SurfaceId, + TextureCreation, + TexturePixelDataFormat, + TextureUpdate, + PRE_RENDER_PHASE, +}; +use crate::texture::Properties as TextureProperties; +use crate::vector::Vec2; +use crate::windowing::dpi::PhysicalSize; +use crate::windowing::window::Window; + +mod reexports +{ + pub use dear_imgui_rs as bindings; +} + +pub use reexports::*; + +pub static SHADER_ASSET_LABEL: LazyLock<AssetLabel> = LazyLock::new(|| AssetLabel { + path: Path::new("").into(), + name: Some("imgui_shader".into()), +}); + +/// Dear Imgui context +#[derive(Sole)] +pub struct Context +{ + pub enabled: bool, + ctx: inner_context_wrapper::InnerContextWrapper, + texture_lookup: IntMap<TextureLookupId, RenderingObjectId>, + texture_id_lookup: + HashMap<dear_imgui_rs::SnapshotTextureId, dear_imgui_rs::TextureId>, +} + +impl Context +{ + pub fn frame(&mut self) -> Option<&mut bindings::Ui> + { + if !self.enabled { + return None; + } + + self.ctx.get_frame() + } + + pub fn register_texture( + &mut self, + texture_data: bindings::OwnedTextureData, + ) -> bindings::ManagedTextureId + { + self.ctx.register_texture(texture_data) + } +} + +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct Extension +{ + start_enabled: bool, + settings_ini_file_path: Option<PathBuf>, +} + +impl Extension +{ + pub fn with_start_enabled(mut self, start_enabled: bool) -> Self + { + self.start_enabled = start_enabled; + self + } + + pub fn with_settings_ini_file( + mut self, + settings_ini_file_path: Option<PathBuf>, + ) -> Self + { + self.settings_ini_file_path = settings_ini_file_path; + self + } +} + +impl ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: ecs::extension::Collector<'_>) + { + let mut context = Context { + enabled: self.start_enabled, + ctx: inner_context_wrapper::InnerContextWrapper::new(), + texture_lookup: IntMap::with_capacity(8), + texture_id_lookup: HashMap::with_capacity(8), + }; + + if let Err(err) = context + .ctx + .set_settings_ini_file_path(self.settings_ini_file_path) + { + tracing::error!("Failed to set path of imgui settings ini file: {err}"); + } + + assert!( + dear_imgui_rs::HAS_FREETYPE, + "Freetype font rasterizer support is not enabled" + ); + + unsafe { + context.ctx.font_atlas().add_font_from_memory_ttf( + include_bytes!("../../res/FiraMono-Regular.ttf"), + 14.0, + Some( + &dear_imgui_rs::FontConfig::default() + .name("FiraMono-Regular.ttf") + .font_loader_flags( + dear_imgui_rs::FontLoaderFlags::BITMAP + | dear_imgui_rs::FontLoaderFlags::FORCE_AUTOHINT, + ), + ), + None, + ); + } + + collector.add_sole(context).ok(); + + collector.add_system( + *PRE_RENDER_PHASE, + update.into_system().initialize((State::default(),)), + ); + + collector.add_observer(handle_window_changed); + } +} + +#[tracing::instrument(skip_all)] +fn handle_window_changed( + observe: Observe<Pair<Changed, Window>>, + mut context: Single<Context>, +) +{ + let Ok(context) = context.get_mut() else { + unreachable!(); + }; + + let Some(event_match) = observe + .iter() + .find(|event_match| event_match.get_entity().has_component(TargetWindow::id())) + else { + return; + }; + + let window = event_match.get_ent_target_comp(); + + let hidpi_factor = window.scale_factor().round(); + + context + .ctx + .get_io_mut() + .set_display_framebuffer_scale([hidpi_factor as f32, hidpi_factor as f32]); + + let window_size_logical = PhysicalSize { + width: window.inner_size.width as f64, + height: window.inner_size.height as f64, + } + .to_logical::<f64>(hidpi_factor); + + context.ctx.get_io_mut().set_display_size([ + window_size_logical.width as f32, + window_size_logical.height as f32, + ]); +} + +fn update( + target_window_query: Query<(&Window, &Surface, With<TargetWindow>)>, + mut context: Single<Context>, + time: Single<Time>, + mut assets: Single<Assets>, + mut render_passes: Single<RenderPasses>, + shader_context: Single<ShaderContext>, + keyboard: Single<Keyboard>, + mouse: Single<Mouse>, + mouse_buttons: Single<MouseButtons>, + mut state: Local<State>, +) -> Result<(), crate::Error> +{ + let Ok(context) = context.get_mut() else { + unreachable!(); + }; + + let assets = assets.get_mut()?; + let render_passes = render_passes.get_mut()?; + let shader_context = shader_context.get()?; + let keyboard = keyboard.get()?; + let mouse = mouse.get()?; + let mouse_buttons = mouse_buttons.get()?; + + let Some((window, window_surface)) = target_window_query.iter().next() else { + return Ok(()); + }; + + let curr_state = std::mem::replace(&mut *state, State::Unreachable); + + match curr_state { + State::NotInitialized => { + let Some((shader_asset, mesh_obj_id, mesh)) = initialize_context( + context, + &window, + window_surface.id, + assets, + render_passes, + &shader_context, + ) else { + *state = State::NotInitialized; + return Ok(()); + }; + + context.ctx.new_frame(); + + *state = State::Ready { shader_asset, mesh_obj_id, mesh }; + } + State::Ready { shader_asset, mesh_obj_id, mut mesh } => { + if !context.enabled { + *state = State::Ready { shader_asset, mesh_obj_id, mesh }; + return Ok(()); + } + + context + .ctx + .get_io_mut() + .set_delta_time(time.delta_time.as_secs_f32()); + + update_inputs(context, &window, keyboard, mouse, mouse_buttons); + + add_drawing_render_pass( + context, + render_passes, + shader_context, + window_surface.id, + shader_asset.clone(), + mesh_obj_id, + &mut mesh, + )?; + + context.ctx.new_frame(); + + *state = State::Ready { shader_asset, mesh_obj_id, mesh }; + } + State::Unreachable => unreachable!(), + } + + Ok(()) +} + +#[derive(Debug, Default, Clone, Copy, Component, Reflection)] +pub struct TargetWindow; + +#[derive(Debug, Default, Component)] +enum State +{ + #[default] + NotInitialized, + Ready + { + shader_asset: AssetHandle<ShaderModuleSource>, + mesh_obj_id: RenderingObjectId, + mesh: Mesh, + }, + Unreachable, +} + +fn initialize_context( + context: &mut Context, + window: &Window, + window_surface_id: SurfaceId, + assets: &mut Assets, + render_passes: &mut RenderPasses, + shader_context: &ShaderContext, +) -> Option<(AssetHandle<ShaderModuleSource>, RenderingObjectId, Mesh)> +{ + let shader_asset = if let Some(shader_asset) = + assets.get_handle_to_loaded::<ShaderModuleSource>(SHADER_ASSET_LABEL.clone()) + { + shader_asset + } else { + assets.store_with_label( + SHADER_ASSET_LABEL.clone(), + ShaderModuleSource { + name: "imgui_shader.slang".into(), + file_path: Path::new("@engine/imgui_shader").into(), + source: include_str!("../../res/imgui_shader.slang").into(), + link_entrypoints: ShaderEntrypointFlags::VERTEX + | ShaderEntrypointFlags::FRAGMENT, + }, + ); + + // We want to wait with initializing until the shader have been processed by the + // shader context + return None; + }; + + let hidpi_factor = window.scale_factor().round(); + + context + .ctx + .get_io_mut() + .set_display_framebuffer_scale([hidpi_factor as f32, hidpi_factor as f32]); + + let window_size = Dimens { + width: window.inner_size.width as f32, + height: window.inner_size.height as f32, + }; + + let window_logical_size = window_size / (hidpi_factor as f32); + + context + .ctx + .get_io_mut() + .set_display_size([window_logical_size.width, window_logical_size.height]); + + let mesh_obj_id = RenderingObjectId::new_sequential(); + + let mesh = Mesh::builder() + .vertices(MeshVertexBuffer::with_capacity( + &[ + MeshVertexAttrInfo { + label: VertexLabel::Position, + ty: MeshVertexAttrType::Float32Array { length: 3 }, + }, + MeshVertexAttrInfo { + label: VertexLabel::Color, + ty: MeshVertexAttrType::Float32Array { length: 4 }, + }, + MeshVertexAttrInfo { + label: VertexLabel::UvFromTopLeft, + ty: MeshVertexAttrType::Float32Array { length: 2 }, + }, + ], + 128, + )) + .indices([]) + .build(); + + render_passes.passes.push_back(RenderPass { + commands: vec![ + RenderingCommand::MakeCurrent(window_surface_id), + RenderingCommand::CreateShaderProgram( + RenderingObjectId::Asset(shader_asset.id()), + shader_context + .get_program(&shader_asset.id()) + .expect("Not possible") + .clone(), + ), + RenderingCommand::ActivateShader(RenderingObjectId::Asset(shader_asset.id())), + RenderingCommand::CreateMesh { + obj_id: mesh_obj_id, + mesh: AssetOrValue::Value(mesh.clone()), + usage: MeshUsage::Stream, + }, + ], + draw_properties: DrawProperties::default(), + }); + + Some((shader_asset, mesh_obj_id, mesh)) +} + +fn update_inputs( + context: &mut Context, + window: &Window, + keyboard: &Keyboard, + mouse: &Mouse, + mouse_buttons: &MouseButtons, +) +{ + let io = context.ctx.get_io_mut(); + + for (key, key_state) in keyboard.new_key_states() { + if let Some(key) = key_to_imgui_key(key) { + io.add_key_event(key, key_state.is_pressed()); + } + } + + for character in keyboard.text_keys().chars() { + if character == '\u{7f}' { + continue; + } + + io.add_input_character(character); + } + + io.add_mouse_source_event(dear_imgui_rs::input::MouseSource::Mouse); + + let mouse_pos = mouse.position.to_logical::<f64>(window.scale_factor()); + + io.add_mouse_pos_event([mouse_pos.x as f32, mouse_pos.y as f32]); + + if !mouse.curr_tick_scroll_delta.is_zero() { + io.add_mouse_wheel_event([ + mouse.curr_tick_scroll_delta.hor_lines, + mouse.curr_tick_scroll_delta.vert_lines, + ]); + } + + for (mouse_button, mouse_button_state) in mouse_buttons.all_current() { + if let Some(mouse_button) = mouse_button_to_imgui_mouse_button(mouse_button) { + io.add_mouse_button_event(mouse_button, mouse_button_state.is_pressed()); + } + } +} + +fn add_drawing_render_pass( + context: &mut Context, + render_passes: &mut RenderPasses, + shader_context: &ShaderContext, + window_surface_id: SurfaceId, + shader_asset: AssetHandle<ShaderModuleSource>, + mesh_obj_id: RenderingObjectId, + mesh: &mut Mesh, +) -> Result<(), crate::Error> +{ + let render_pass = render_passes.passes.push_back_mut(RenderPass { + commands: Vec::new(), + draw_properties: DrawProperties { + blending_enabled: true, + blending_config: RenderingBlendingConfiguration { + equation: RenderingBlendingEquation::Add, + source_factor: RenderingBlendingFactor::SrcAlpha, + destination_factor: RenderingBlendingFactor::OneMinusSrcAlpha, + }, + depth_test_enabled: false, + face_culling_enabled: false, + scissor_test_enabled: true, + ..Default::default() + }, + }); + + let shader_program = shader_context + .get_program(&shader_asset.id()) + .expect("Not possible"); + + let shader_cursor = ShaderCursor::new( + shader_program + .reflection(0) + .unwrap() + .global_params_var_layout() + .unwrap(), + ); + + let Context { + enabled: _, + ctx, + texture_lookup, + texture_id_lookup, + } = context; + + let mut render_frame = ctx.render(); + + let mut tex_feedbacks = Vec::with_capacity(render_frame.texture_requests().len()); + + for texture_request in render_frame.texture_requests() { + match texture_request.operation() { + dear_imgui_rs::TextureOp::Create { + format, + width, + height, + row_pitch: _, + pixels, + } => { + if !matches!(format, dear_imgui_rs::TextureFormat::RGBA32) { + unimplemented!(); + } + + let texture_lookup_id = TextureLookupId::new(); + + tracing::trace!( + snapshot_tex_id = ?texture_request.texture(), + tex_lookup_id = ?texture_lookup_id, + "Performing texture operation: Create" + ); + + let texture_object_id = RenderingObjectId::new_sequential(); + + render_pass.commands.push(RenderingCommand::CreateTexture { + obj_id: texture_object_id, + pixel_data_format: TexturePixelDataFormat::Srgba( + SrgbaTextureDataType::UnsignedByte, + ), + creation: TextureCreation::Texture2D { + size: Dimens { width: *width, height: *height }, + image: Some(pixels.clone().into_boxed_slice()), + }, + properties: TextureProperties::default(), + }); + + texture_lookup.insert(texture_lookup_id, texture_object_id); + + texture_id_lookup + .insert(texture_request.texture(), texture_lookup_id.into()); + + let texture_feedback = + match texture_request.uploaded(texture_lookup_id.into()) { + Ok(texture_feedback) => texture_feedback, + Err( + dear_imgui_rs::TextureFeedbackError::UploadForDestroy + | dear_imgui_rs::TextureFeedbackError::DestroyForUpload, + ) => unreachable!(), + }; + + tex_feedbacks.push(texture_feedback); + } + dear_imgui_rs::TextureOp::Update { format, width: _, height: _, rects } => { + if !matches!(format, dear_imgui_rs::TextureFormat::RGBA32) { + unimplemented!(); + } + + let texture_lookup_id = + TextureLookupId::from(texture_id_lookup[&texture_request.texture()]); + + tracing::trace!( + snapshot_tex_id = ?texture_request.texture(), + tex_lookup_id = ?texture_lookup_id, + "Performing texture operation: Update" + ); + + let Some(texture_object_id) = texture_lookup.get(texture_lookup_id) + else { + tracing::error!( + snapshot_tex_id = ?texture_request.texture(), + tex_lookup_id = ?texture_lookup_id, + "Unknown texture. Skipping update", + ); + continue; + }; + + render_pass.commands.reserve(rects.len()); + + for rect in rects { + render_pass.commands.push(RenderingCommand::UpdateTexture { + obj_id: *texture_object_id, + pixels: rect.data.clone().into_boxed_slice(), + pixel_data_format: TexturePixelDataFormat::Srgba( + SrgbaTextureDataType::UnsignedByte, + ), + update: TextureUpdate::Texture2D { + size: Dimens::<u32> { + width: rect.rect.w.into(), + height: rect.rect.h.into(), + }, + offset: Vec2 { + x: rect.rect.x as u32, + y: rect.rect.y as u32, + }, + }, + }); + } + + let texture_feedback = + match texture_request.uploaded(texture_lookup_id.into()) { + Ok(texture_feedback) => texture_feedback, + Err( + dear_imgui_rs::TextureFeedbackError::UploadForDestroy + | dear_imgui_rs::TextureFeedbackError::DestroyForUpload, + ) => unreachable!(), + }; + + tex_feedbacks.push(texture_feedback); + } + dear_imgui_rs::TextureOp::Destroy => { + let texture_lookup_id = + TextureLookupId::from(texture_id_lookup[&texture_request.texture()]); + + tracing::trace!( + snapshot_tex_id = ?texture_request.texture(), + tex_lookup_id = ?texture_lookup_id, + "Performing texture operation: Destroy" + ); + + let Some(texture_object_id) = texture_lookup.remove(texture_lookup_id) + else { + tracing::error!( + texture = ?texture_lookup_id, + "Unknown texture. Skipping destroyal", + ); + continue; + }; + + render_pass + .commands + .push(RenderingCommand::RemoveTexture(texture_object_id)); + + let texture_feedback = match texture_request.destroyed() { + Ok(texture_feedback) => texture_feedback, + Err( + dear_imgui_rs::TextureFeedbackError::UploadForDestroy + | dear_imgui_rs::TextureFeedbackError::DestroyForUpload, + ) => unreachable!(), + }; + + tex_feedbacks.push(texture_feedback); + } + } + } + + render_frame + .reconcile_texture_feedback(tex_feedbacks) + .with_context(|| "Failed to reconcile texture feedback")?; + + let draw_data = render_frame.draw_data(); + + let [display_width, display_height] = draw_data.display_size(); + + // let [scale_width, scale_height] = draw_data.framebuffer_scale; + + let shader_object_id = RenderingObjectId::Asset(shader_asset.id()); + + render_pass.commands.extend([ + RenderingCommand::MakeCurrent(window_surface_id), + RenderingCommand::ActivateShader(shader_object_id), + RenderingCommand::SetShaderBinding( + shader_object_id, + shader_cursor + .field("Uniforms") + .field("projection") + .binding( + OrthographicProjection::builder() + .near(1.0) + .far(-1.0) + .viewport_origin(Vec2 { x: 0.0, y: 1.0 }) + .size(crate::projection::OrthographicSize::FixedSize(Dimens { + width: display_width, + height: -display_height, + })) + .build() + .to_matrix_rh( + // Window size can be 0x0 since it will not be used + PhysicalSize::default(), + ProjectionClipVolume::NegOneToOne, + ) + .into(), + )?, + ), + ]); + + for draw_list in draw_data.draw_lists() { + mesh.vertex_buf_mut().clear(); + + for vertex in draw_list.vtx_buffer() { + mesh.vertex_buf_mut().push(( + MeshNamedVertexAttr::<[f32; 3]> { + label: VertexLabel::Position, + value: [vertex.pos[0], vertex.pos[1], 0.0], + }, + MeshNamedVertexAttr { + label: VertexLabel::Color, + value: vertex.rgba().map(|elem| (elem as f32) / 255.0), + }, + MeshNamedVertexAttr { + label: VertexLabel::UvFromTopLeft, + value: vertex.uv, + }, + )); + } + + mesh.set_indices(draw_list.idx_buffer().iter().map(|index| (*index).into())); + + render_pass.commands.push(RenderingCommand::UpdateMesh { + obj_id: mesh_obj_id, + mesh: mesh.clone(), + usage: MeshUsage::Stream, + }); + + for command in draw_list.commands() { + match command { + ImguiDrawCmd::Elements { count, cmd_params } => { + let Some(scissor_box) = + calc_draw_cmd_scissor_box(draw_data, &cmd_params) + else { + continue; + }; + + let tex_id = cmd_params.texture_id; + + let Some(texture_object_id) = + texture_lookup.get(TextureLookupId::from(tex_id)) + else { + tracing::error!( + "Unknown texture {}. Skipping skipping draw command", + tex_id.id() + ); + continue; + }; + + render_pass.commands.extend([ + RenderingCommand::UpdateDrawProperties( + DrawProperties { scissor_box, ..Default::default() }, + DrawPropertiesUpdateFlags::SCISSOR_BOX, + ), + RenderingCommand::SetShaderBinding( + shader_object_id, + shader_cursor.field("main_texture").binding( + ShaderBindingValue::Texture( + *texture_object_id, + ShaderBindingTextureKind::Texture2D, + ), + )?, + ), + RenderingCommand::DrawMesh( + mesh_obj_id, + DrawMeshOptions::builder() + .element_offset(cmd_params.idx_offset.try_into().unwrap()) + .vertex_offset(cmd_params.vtx_offset.try_into().unwrap()) + .element_cnt(count.try_into().unwrap()) + .build(), + ), + ]); + } + _ => {} + } + } + } + + Ok(()) +} + +fn calc_draw_cmd_scissor_box( + draw_data: &dear_imgui_rs::DrawData, + cmd_params: &dear_imgui_rs::DrawCmdParams, +) -> Option<ScissorBox> +{ + let clip_rect = cmd_params.clip_rect; + + let display_size = draw_data.display_size(); + let display_pos = draw_data.display_pos(); + let framebuffer_scale = draw_data.framebuffer_scale(); + + let clip_min_x = (clip_rect[0] - display_pos[0]) * framebuffer_scale[0]; + + let clip_min_y = (clip_rect[1] - display_pos[1]) * framebuffer_scale[1]; + + let clip_max_x = (clip_rect[2] - display_pos[0]) * framebuffer_scale[0]; + + let clip_max_y = (clip_rect[3] - display_pos[1]) * framebuffer_scale[1]; + + if clip_max_x <= clip_min_x || clip_max_y <= clip_min_y { + return None; + } + + let fb_height = display_size[1] * framebuffer_scale[1]; + + Some(ScissorBox { + size: Some(Dimens { + width: (clip_max_x - clip_min_x) as u16, + height: (clip_max_y - clip_min_y) as u16, + }), + lower_left_corner_pos: Vec2 { + x: clip_min_x as u16, + y: (fb_height - clip_max_y) as u16, + }, + }) +} + +fn key_to_imgui_key(key: Key) -> Option<ImguiKey> +{ + match key { + Key::Backquote => Some(ImguiKey::GraveAccent), + Key::Backslash => Some(ImguiKey::Backslash), + Key::BracketLeft => Some(ImguiKey::LeftBracket), + Key::BracketRight => Some(ImguiKey::RightBracket), + Key::Comma => Some(ImguiKey::Comma), + Key::Digit0 => Some(ImguiKey::Key0), + Key::Digit1 => Some(ImguiKey::Key1), + Key::Digit2 => Some(ImguiKey::Key2), + Key::Digit3 => Some(ImguiKey::Key3), + Key::Digit4 => Some(ImguiKey::Key4), + Key::Digit5 => Some(ImguiKey::Key5), + Key::Digit6 => Some(ImguiKey::Key6), + Key::Digit7 => Some(ImguiKey::Key7), + Key::Digit8 => Some(ImguiKey::Key8), + Key::Digit9 => Some(ImguiKey::Key9), + Key::Equal => Some(ImguiKey::Equal), + Key::A => Some(ImguiKey::A), + Key::B => Some(ImguiKey::B), + Key::C => Some(ImguiKey::C), + Key::D => Some(ImguiKey::D), + Key::E => Some(ImguiKey::E), + Key::F => Some(ImguiKey::F), + Key::G => Some(ImguiKey::G), + Key::H => Some(ImguiKey::H), + Key::I => Some(ImguiKey::I), + Key::J => Some(ImguiKey::J), + Key::K => Some(ImguiKey::K), + Key::L => Some(ImguiKey::L), + Key::M => Some(ImguiKey::M), + Key::N => Some(ImguiKey::N), + Key::O => Some(ImguiKey::O), + Key::P => Some(ImguiKey::P), + Key::Q => Some(ImguiKey::Q), + Key::R => Some(ImguiKey::R), + Key::S => Some(ImguiKey::S), + Key::T => Some(ImguiKey::T), + Key::U => Some(ImguiKey::U), + Key::V => Some(ImguiKey::V), + Key::W => Some(ImguiKey::W), + Key::X => Some(ImguiKey::X), + Key::Y => Some(ImguiKey::Y), + Key::Z => Some(ImguiKey::Z), + Key::Minus => Some(ImguiKey::Minus), + Key::Period => Some(ImguiKey::Period), + Key::Quote => Some(ImguiKey::Apostrophe), + Key::Semicolon => Some(ImguiKey::Semicolon), + Key::Slash => Some(ImguiKey::Slash), + Key::AltLeft => Some(ImguiKey::LeftAlt), + Key::AltRight => Some(ImguiKey::RightAlt), + Key::Backspace => Some(ImguiKey::Backspace), + Key::CapsLock => Some(ImguiKey::CapsLock), + Key::ControlLeft => Some(ImguiKey::LeftCtrl), + Key::ControlRight => Some(ImguiKey::RightCtrl), + Key::Enter => Some(ImguiKey::Enter), + Key::SuperLeft => Some(ImguiKey::LeftSuper), + Key::SuperRight => Some(ImguiKey::RightSuper), + Key::ShiftLeft => Some(ImguiKey::LeftShift), + Key::ShiftRight => Some(ImguiKey::RightShift), + Key::Space => Some(ImguiKey::Space), + Key::Tab => Some(ImguiKey::Tab), + Key::Delete => Some(ImguiKey::Delete), + Key::End => Some(ImguiKey::End), + Key::Home => Some(ImguiKey::Home), + Key::Insert => Some(ImguiKey::Insert), + Key::PageDown => Some(ImguiKey::PageDown), + Key::PageUp => Some(ImguiKey::PageUp), + Key::ArrowDown => Some(ImguiKey::DownArrow), + Key::ArrowLeft => Some(ImguiKey::LeftArrow), + Key::ArrowRight => Some(ImguiKey::RightArrow), + Key::ArrowUp => Some(ImguiKey::UpArrow), + Key::NumLock => Some(ImguiKey::NumLock), + Key::Numpad0 => Some(ImguiKey::Keypad0), + Key::Numpad1 => Some(ImguiKey::Keypad1), + Key::Numpad2 => Some(ImguiKey::Keypad2), + Key::Numpad3 => Some(ImguiKey::Keypad3), + Key::Numpad4 => Some(ImguiKey::Keypad4), + Key::Numpad5 => Some(ImguiKey::Keypad5), + Key::Numpad6 => Some(ImguiKey::Keypad6), + Key::Numpad7 => Some(ImguiKey::Keypad7), + Key::Numpad8 => Some(ImguiKey::Keypad8), + Key::Numpad9 => Some(ImguiKey::Keypad9), + Key::NumpadAdd => Some(ImguiKey::KeypadAdd), + Key::NumpadDecimal => Some(ImguiKey::KeypadDecimal), + Key::NumpadDivide => Some(ImguiKey::KeypadDivide), + Key::NumpadEnter => Some(ImguiKey::KeypadEnter), + Key::NumpadEqual => Some(ImguiKey::KeypadEqual), + Key::NumpadMultiply => Some(ImguiKey::KeypadMultiply), + Key::NumpadSubtract => Some(ImguiKey::KeypadSubtract), + Key::Escape => Some(ImguiKey::Escape), + Key::PrintScreen => Some(ImguiKey::PrintScreen), + Key::ScrollLock => Some(ImguiKey::ScrollLock), + Key::Pause => Some(ImguiKey::Pause), + Key::Meta => Some(ImguiKey::ModSuper), + Key::F1 => Some(ImguiKey::F1), + Key::F2 => Some(ImguiKey::F2), + Key::F3 => Some(ImguiKey::F3), + Key::F4 => Some(ImguiKey::F4), + Key::F5 => Some(ImguiKey::F5), + Key::F6 => Some(ImguiKey::F6), + Key::F7 => Some(ImguiKey::F7), + Key::F8 => Some(ImguiKey::F8), + Key::F9 => Some(ImguiKey::F9), + Key::F10 => Some(ImguiKey::F10), + Key::F11 => Some(ImguiKey::F11), + Key::F12 => Some(ImguiKey::F12), + _ => None, + } +} + +fn mouse_button_to_imgui_mouse_button( + mouse_button: MouseButton, +) -> Option<ImguiMouseButton> +{ + match mouse_button { + MouseButton::Left | MouseButton::Other(0) => Some(ImguiMouseButton::Left), + MouseButton::Right | MouseButton::Other(1) => Some(ImguiMouseButton::Right), + MouseButton::Middle | MouseButton::Other(2) => Some(ImguiMouseButton::Middle), + MouseButton::Other(3) => Some(ImguiMouseButton::Extra1), + MouseButton::Other(4) => Some(ImguiMouseButton::Extra2), + _ => None, + } +} + +mod inner_context_wrapper +{ + use std::path::PathBuf; + use std::pin::Pin; + use std::ptr::null_mut; + + pub struct InnerContextWrapper + { + ctx: Pin<Box<dear_imgui_rs::Context>>, + frame: *mut dear_imgui_rs::Ui, + _renderer_consumer: dear_imgui_rs::RendererConsumer, + } + + impl InnerContextWrapper + { + pub fn new() -> Self + { + let mut ctx = Box::pin(dear_imgui_rs::Context::create()); + + let _ = ctx.set_platform_name(Some("engine-windowing")); + let _ = ctx.set_renderer_name(Some("engine-rendering")); + + ctx.io_mut() + .set_backend_flags(dear_imgui_rs::BackendFlags::RENDERER_HAS_TEXTURES); + + let _renderer_consumer = ctx.create_renderer_consumer().unwrap(); + + Self { + ctx, + frame: null_mut(), + _renderer_consumer, + } + } + + pub fn set_settings_ini_file_path( + &mut self, + path: Option<PathBuf>, + ) -> Result<(), dear_imgui_rs::ImGuiError> + { + self.ctx.set_ini_filename(path) + } + + pub fn register_texture( + &mut self, + texture_data: dear_imgui_rs::texture::OwnedTextureData, + ) -> dear_imgui_rs::ManagedTextureId + { + self.ctx.register_texture(texture_data) + } + + pub fn get_io_mut(&mut self) -> &mut dear_imgui_rs::Io + { + self.ctx.io_mut() + } + + pub fn font_atlas(&self) -> &dear_imgui_rs::FontAtlas + { + self.ctx.font_atlas() + } + + // pub fn create_renderer_consumer(&mut self) -> dear_imgui_rs::RendererConsumer + // { + // self.ctx.create_renderer_consumer().unwrap() + // } + + pub fn render(&mut self) -> dear_imgui_rs::RenderedFrame<'_> + { + self.ctx.render() + } + + pub fn new_frame(&mut self) + { + let frame = &raw mut *self.ctx.frame(); + + self.frame = frame; + } + + pub fn get_frame(&mut self) -> Option<&mut dear_imgui_rs::Ui> + { + unsafe { self.frame.as_mut() } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct TextureLookupId +{ + inner: u64, +} + +impl TextureLookupId +{ + fn new() -> Self + { + static NEXT: AtomicU64 = AtomicU64::new(1); + + Self { + inner: NEXT.fetch_add(1, Ordering::Relaxed), + } + } +} + +impl intmap::IntKey for TextureLookupId +{ + type Int = u64; + + const PRIME: Self::Int = <u64 as intmap::IntKey>::PRIME; + + fn into_int(self) -> Self::Int + { + self.inner + } +} + +impl From<dear_imgui_rs::TextureId> for TextureLookupId +{ + fn from(texture_id: dear_imgui_rs::TextureId) -> Self + { + assert!(!texture_id.is_null(), "Bad texture ID: is null"); + + Self { inner: texture_id.id() } + } +} + +impl From<TextureLookupId> for dear_imgui_rs::TextureId +{ + fn from(texture_id: TextureLookupId) -> Self + { + Self::new(texture_id.inner) + } +} diff --git a/engine/src/ui/view.rs b/engine/src/ui/view.rs new file mode 100644 index 0000000..42601a3 --- /dev/null +++ b/engine/src/ui/view.rs @@ -0,0 +1,2 @@ +pub mod transform_3d; +pub mod world; diff --git a/engine/src/ui/view/transform_3d.rs b/engine/src/ui/view/transform_3d.rs new file mode 100644 index 0000000..a426b2a --- /dev/null +++ b/engine/src/ui/view/transform_3d.rs @@ -0,0 +1,128 @@ +use dear_imguizmo::GuizmoExt; + +use crate::camera::{Active as ActiveCamera, Camera}; +use crate::data_types::dimens::Dimens3; +use crate::ecs::pair::ChildOf; +use crate::ecs::query::term::{Traverse, TraverseUp, With, Without}; +use crate::ecs::sole::Single; +use crate::ecs::Query; +use crate::projection::ClipVolume as ProjectionClipVolume; +use crate::scene::{Active as ActiveScene, Scene}; +use crate::transform::Transform; +use crate::ui::dear_imgui::{ + Context as DearImguiContext, + TargetWindow as DearImguiTargetWindow, +}; +use crate::vector::Vec3; +use crate::windowing::window::{Closed as WindowClosed, Window}; + +pub fn show( + subject_query: Query<( + &mut Transform, + Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>, + Without<ActiveCamera>, + )>, + camera_query: Query<( + &Camera, + &Transform, + Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>, + With<ActiveCamera>, + )>, + target_window_query: Query<( + &Window, + With<DearImguiTargetWindow>, + Without<WindowClosed>, + )>, + mut dear_imgui_context: Single<DearImguiContext>, +) -> Result<(), crate::Error> +{ + let dear_imgui_context = dear_imgui_context.get_mut()?; + + let Some(frame) = dear_imgui_context.frame() else { + return Ok(()); + }; + + let Some((camera, camera_world_pos)) = camera_query.iter().next() else { + return Ok(()); + }; + + let Some((target_window,)) = target_window_query.iter().next() else { + return Ok(()); + }; + + let gizmo = frame.guizmo(); + + let [display_width, display_height] = frame.io().display_size(); + + gizmo.set_rect(0.0, 0.0, display_width, display_height); + + // The widgets should appear under dear imgui windows + gizmo.set_drawlist_background(); + + let col_major_view = flatten_matrix( + camera + .to_view_matrix(camera_world_pos.position) + .to_column_major(), + ); + + let col_major_projection = flatten_matrix( + camera + .projection + .to_matrix_rh(target_window.inner_size, ProjectionClipVolume::NegOneToOne) + .to_column_major(), + ); + + for (subject_index, (mut subject_transform,)) in subject_query.iter().enumerate() { + let subject_index = match i32::try_from(subject_index) { + Ok(subject_index) => subject_index, + Err(err) => { + tracing::error!("Invalid subject index {subject_index}: {err}"); + break; + } + }; + + // No rotation + let rotation = [0.0, 0.0, 0.0]; + + let mut col_major_model_mat = dear_imguizmo::recompose_matrix( + &subject_transform.position.into(), + &rotation, + &subject_transform.scale.into(), + ); + + let _id_token = gizmo.push_id(subject_index); + + let used = gizmo + .manipulate_config( + &col_major_view, + &col_major_projection, + &mut col_major_model_mat, + ) + .operation( + dear_imguizmo::Operation::TRANSLATE | dear_imguizmo::Operation::SCALE, + ) + .mode(dear_imguizmo::Mode::World) + .build(); + + if used { + let (translation, _rotation, scale) = + dear_imguizmo::decompose_matrix(&col_major_model_mat); + + subject_transform.position = Vec3::from(translation); + subject_transform.scale = Dimens3::from(scale); + } + } + + Ok(()) +} + +fn flatten_matrix(mat: [[f32; 4]; 4]) -> [f32; 4 * 4] +{ + let flattened = mat.as_flattened(); + + let Some(flattened) = flattened.as_array::<{ 4 * 4 }>() else { + unreachable!(); + }; + + flattened.clone() +} diff --git a/engine/src/ui/view/world.rs b/engine/src/ui/view/world.rs new file mode 100644 index 0000000..81c2377 --- /dev/null +++ b/engine/src/ui/view/world.rs @@ -0,0 +1,2314 @@ +use std::any::{Any, TypeId}; +use std::borrow::Cow; +use std::io::Cursor; +use std::ops::{Deref, DerefMut}; + +use crate::color::{Rgb, Rgba}; +use crate::ecs::actions::Actions; +use crate::ecs::component::local::Local; +use crate::ecs::component::{ + Handle as ComponentHandle, + Info as ComponentInfo, + Parts as ComponentParts, +}; +use crate::ecs::entity::{Handle as EntityHandle, Name as EntityName}; +use crate::ecs::sole::Single; +use crate::ecs::uid::Uid; +use crate::ecs::util::Either; +use crate::ecs::{Component, Query, World}; +use crate::image::{Format as ImageFormat, Image}; +use crate::reflection::{ + Enum as EnumReflection, + EnumVariant as EnumVariantReflection, + GetError as ReflectionGetError, + LiteralType, + Struct as StructReflection, + Type as TypeReflection, + Visibility, +}; +use crate::ui::dear_imgui::{ + bindings as dear_imgui_bindings, + Context as DearImguiContext, +}; + +const BUTTON_RED_NORMAL: [f32; 4] = [124.0, 52.0, 39.0, 255.0]; +const BUTTON_RED_HOVERED: [f32; 4] = [181.0, 76.0, 57.0, 255.0]; +const BUTTON_RED_ACTIVE: [f32; 4] = [162.0, 68.0, 51.0, 255.0]; + +#[derive(Default, Component)] +pub struct State +{ + spawning_entity: Option<Uid>, + popup_state: Option<PopupState>, + textures: Option<Textures>, +} + +pub fn show( + query: Query<()>, + mut dear_imgui_context: Single<DearImguiContext>, + mut state: Local<State>, + world: &World, + mut actions: Actions, +) -> Result<(), crate::Error> +{ + let dear_imgui_context = dear_imgui_context.get_mut()?; + + let State { + spawning_entity, + popup_state, + textures, + } = &mut *state; + + let textures = textures.get_or_insert_with(|| Textures::new(dear_imgui_context)); + + let Some(frame) = dear_imgui_context.frame() else { + return Ok(()); + }; + + frame + .window("World") + .size([600.0, 400.0], dear_imgui_bindings::Condition::Once) + .build(|| { + create_spawn_button_widgets( + frame, + spawning_entity, + popup_state, + world, + &mut actions, + ); + + frame.spacing(); + frame.spacing(); + + for ent_handle in &query.into_flexible_query() { + create_entity_widgets( + frame, + popup_state, + textures, + &ent_handle, + world, + &mut actions, + ); + } + + let popup_closed = match popup_state { + Some(PopupState::AddComponent(popup_state)) => { + show_add_component_popup(frame, popup_state, &mut actions) + } + Some(PopupState::RenameEntity(popup_state)) => { + show_rename_entity_popup(frame, popup_state, &mut actions, world) + } + _ => false, + }; + + if popup_closed { + *popup_state = None; + } + }); + + Ok(()) +} + +fn create_texture( + dear_imgui_context: &mut DearImguiContext, + image: Image, +) -> dear_imgui_bindings::ManagedTextureId +{ + let image = image.into_rgba8(); + + let mut texture_data = dear_imgui_bindings::OwnedTextureData::new(); + + texture_data.create( + dear_imgui_bindings::TextureFormat::RGBA32, + image.dimensions().width, + image.dimensions().height, + ); + + texture_data.set_data(image.as_bytes()); + + dear_imgui_context.register_texture(texture_data) +} + +fn create_spawn_button_widgets( + frame: &dear_imgui_bindings::Ui, + spawning_entity: &mut Option<Uid>, + popup_state: &mut Option<PopupState>, + world: &World, + actions: &mut Actions, +) +{ + let _disabled_token = frame.begin_disabled_with_cond(spawning_entity.is_some()); + + if frame.button("Spawn") { + let new_ent_id = actions.spawn(()); + + *spawning_entity = Some(new_ent_id); + } + + if let Some(spawning_ent) = &spawning_entity { + if let Some(spawned_ent) = world.get_entity(*spawning_ent) { + let spawned_ent_title = create_entity_title(&spawned_ent); + + if enter_add_component_popup_state( + popup_state, + &spawned_ent, + &spawned_ent_title, + world, + ) { + *spawning_entity = None; + } + } + } +} + +fn create_entity_widgets( + frame: &dear_imgui_bindings::Ui, + popup_state: &mut Option<PopupState>, + textures: &Textures, + ent_handle: &EntityHandle, + world: &World, + actions: &mut Actions, +) +{ + if ent_handle.component_ids().next().is_some() + && !ent_handle.component_ids().any(|comp_id| { + world.get_entity(comp_id).is_some_and(|comp_ent| { + comp_ent + .get::<ComponentInfo>() + .is_some_and(|comp_info| comp_info.type_reflection.is_some()) + }) + }) + { + return; + } + + let ent_title = create_entity_title(&ent_handle); + + let mut is_open = false; + + frame + .table(&format!("ent_{}_table", ent_handle.uid())) + .headers(false) + .sizing_policy(dear_imgui_bindings::TableSizingPolicy::FixedFit) + .columns([ + dear_imgui_bindings::TableColumnSetup::new("a") + .fixed_width(frame.content_region_avail_width() * 0.8), + dear_imgui_bindings::TableColumnSetup::new("b"), + dear_imgui_bindings::TableColumnSetup::new("c"), + dear_imgui_bindings::TableColumnSetup::new("d"), + ]) + .build(|frame| { + frame.table_next_row(); + + frame.table_next_column(); + + is_open = frame + .collapsing_header(&ent_title, dear_imgui_bindings::TreeNodeFlags::NONE); + + frame.table_next_column(); + + create_add_component_button_widget( + frame, + &ent_handle, + &ent_title, + popup_state, + world, + ); + + frame.table_next_column(); + + create_despawn_button_widget(frame, textures, ent_handle, actions); + + frame.table_next_column(); + + create_rename_entity_button_widget( + frame, + textures, + ent_handle, + &ent_title, + popup_state, + ); + }); + + if is_open { + for component_id in ent_handle.component_ids() { + if component_id.is_pair() { + create_pair_component_widgets( + frame, + textures, + ent_handle, + component_id, + world, + actions, + ); + + frame.spacing(); + + continue; + } + + create_component_widgets(frame, ent_handle, component_id, world, actions); + } + } +} + +fn create_component_widgets( + frame: &dear_imgui_bindings::Ui, + ent_handle: &EntityHandle, + component_id: Uid, + world: &World, + actions: &mut Actions, +) +{ + let Some(component_info) = world + .get_entity(component_id) + .and_then(|comp_ent| comp_ent.get::<ComponentInfo>()) + else { + return; + }; + + let Some(component_type) = component_info.type_reflection else { + return; + }; + + let Some(mut component) = ent_handle.get_any_by_id_mut(component_id) else { + unreachable!(); + }; + + { + let _color_token = frame.push_style_color( + dear_imgui_bindings::StyleColor::Button, + BUTTON_RED_NORMAL.map(|num| num / 255.0), + ); + + let _color_token_b = frame.push_style_color( + dear_imgui_bindings::StyleColor::ButtonHovered, + BUTTON_RED_HOVERED.map(|num| num / 255.0), + ); + + let _color_token_c = frame.push_style_color( + dear_imgui_bindings::StyleColor::ButtonActive, + BUTTON_RED_ACTIVE.map(|num| num / 255.0), + ); + + if frame.small_button(format!( + "X##world_view_entity_{}_remove_component_button_{}", + ent_handle.uid(), + component_info.name + )) { + actions.remove_components(ent_handle.uid(), [component_id]); + } + } + + frame.same_line(); + + let mut component_changed = false; + + create_item_title_widget( + frame, + component_info.name, + &ItemType::Reflected(component_type), + None, + ); + + let item_type = ItemType::Reflected(component_type); + + let _token = item_type.indent_or_same_line(frame); + + add_item_to_frame( + frame, + ItemRef::Mutable(&mut *component), + &mut component_changed, + ItemInfo { + item_title: component_info.name, + item_tag: &format!( + "world_view_entity_{}_component_{}", + ent_handle.uid(), + component_info.name, + ), + item_type_name: None, + item_type, + item_is_read_only: false, + }, + &[], + ); + + frame.spacing(); + + if component_changed { + component.set_changed(); + } +} + +fn create_pair_component_widgets( + frame: &dear_imgui_bindings::Ui, + textures: &Textures, + ent_handle: &EntityHandle, + pair_id: Uid, + world: &World, + actions: &mut Actions, +) +{ + { + let _color_token = frame.push_style_color( + dear_imgui_bindings::StyleColor::Button, + BUTTON_RED_NORMAL.map(|num| num / 255.0), + ); + + let _color_token_b = frame.push_style_color( + dear_imgui_bindings::StyleColor::ButtonHovered, + BUTTON_RED_HOVERED.map(|num| num / 255.0), + ); + + let _color_token_c = frame.push_style_color( + dear_imgui_bindings::StyleColor::ButtonActive, + BUTTON_RED_ACTIVE.map(|num| num / 255.0), + ); + + if frame.small_button(format!( + "X##world_view_entity_{}_remove_component_button_{}", + ent_handle.uid(), + pair_id + )) { + actions.remove_components(ent_handle.uid(), [pair_id]); + } + } + + frame.same_line(); + + let relation_comp_info_or_ent_name = + get_comp_info_or_ent_name(world, pair_id.relation()); + + let relation_name: Cow<'_, str> = match &relation_comp_info_or_ent_name { + Some(Either::A(comp_info)) => comp_info.name.into(), + Some(Either::B(ent_name)) => format!("🌐 {}", ent_name.name).into(), + None => format!("<unnamed> {}", pair_id.relation()).into(), + }; + + let target_comp_info_or_ent_name = get_comp_info_or_ent_name(world, pair_id.target()); + + let target_name: Cow<'_, str> = match &target_comp_info_or_ent_name { + Some(Either::A(comp_info)) => comp_info.name.into(), + Some(Either::B(ent_name)) => format!("🌐 {}", ent_name.name).into(), + None => format!("<unnamed> {}", pair_id.target()).into(), + }; + + // TODO: Make relation & target ids editable + + frame.text(format!("({relation_name}, {target_name})")); + + let Some(mut pair_data) = ent_handle.get_any_by_id_mut(pair_id) else { + unreachable!(); + }; + + let pair_data_ty_id = (*pair_data).type_id(); + + let Some(pair_data_comp_info) = [ + &relation_comp_info_or_ent_name, + &target_comp_info_or_ent_name, + ] + .map(Option::as_ref) + .map(|item| item.and_then(Either::as_a)) + .into_iter() + .flatten() + .find(|comp_info| comp_info.ty_id == pair_data_ty_id) else { + return; + }; + + let Some(pair_data_ty) = pair_data_comp_info.type_reflection else { + frame.same_line(); + + frame.image(textures.warning_icon_tex_id, [16.0, 16.0]); + + if frame.is_item_hovered() { + frame.tooltip_text(format!( + "Pair data type {} does not have reflection", + pair_data_comp_info.name + )); + } + + return; + }; + + let _indent = frame.begin_indent(); + + let mut component_changed = false; + + create_item_title_widget( + frame, + pair_data_comp_info.name, + &ItemType::Reflected(pair_data_ty), + None, + ); + + let item_type = ItemType::Reflected(pair_data_ty); + + let _token = item_type.indent_or_same_line(frame); + + add_item_to_frame( + frame, + ItemRef::Mutable(&mut *pair_data), + &mut component_changed, + ItemInfo { + item_title: pair_data_comp_info.name, + item_tag: &format!( + "world_view_entity_{}_pair_component_{pair_id}", + ent_handle.uid(), + ), + item_type_name: None, + item_type, + item_is_read_only: false, + }, + &[], + ); + + // The pair component is not set as changed here since events do not support + // pairs as event targets +} + +fn get_comp_info_or_ent_name( + world: &World, + id: Uid, +) -> Option<Either<ComponentHandle<'_, ComponentInfo>, ComponentHandle<'_, EntityName>>> +{ + let ent = world.get_entity(id)?; + + if let Some(component_info) = ent.get::<ComponentInfo>() { + return Some(Either::A(component_info)); + } + + if let Some(entity_name) = ent.get::<EntityName>() { + return Some(Either::B(entity_name)); + } + + None +} + +fn create_add_component_button_widget( + frame: &dear_imgui_bindings::Ui, + ent_handle: &EntityHandle, + ent_title: &str, + popup_state: &mut Option<PopupState>, + world: &World, +) +{ + let _color_token = frame.push_style_color( + dear_imgui_bindings::StyleColor::Button, + [49.0, 89.0, 28.0, 255.0].map(|num| num / 255.0), + ); + + let _color_token_b = frame.push_style_color( + dear_imgui_bindings::StyleColor::ButtonHovered, + [100.0, 181.0, 57.0, 255.0].map(|num| num / 255.0), + ); + + let _color_token_c = frame.push_style_color( + dear_imgui_bindings::StyleColor::ButtonActive, + [90.0, 162.0, 51.0, 255.0].map(|num| num / 255.0), + ); + + let is_button_clicked = frame.button(format!( + "+##add_component_to_entity_button_{}", + ent_handle.uid() + )); + + if is_button_clicked { + enter_add_component_popup_state(popup_state, ent_handle, ent_title, world); + } +} + +fn create_despawn_button_widget( + frame: &dear_imgui_bindings::Ui, + textures: &Textures, + ent_handle: &EntityHandle, + actions: &mut Actions, +) +{ + let _color_token = frame.push_style_color( + dear_imgui_bindings::StyleColor::Button, + BUTTON_RED_NORMAL.map(|num| num / 255.0), + ); + + let _color_token_b = frame.push_style_color( + dear_imgui_bindings::StyleColor::ButtonHovered, + BUTTON_RED_HOVERED.map(|num| num / 255.0), + ); + + let _color_token_c = frame.push_style_color( + dear_imgui_bindings::StyleColor::ButtonActive, + BUTTON_RED_ACTIVE.map(|num| num / 255.0), + ); + + if frame + .image_button_config( + &format!("ent_{}_despawn_button", ent_handle.uid()), + textures.despawn_icon_tex_id, + [14.0, 14.0], + ) + .build() + { + actions.despawn(ent_handle.uid()); + } +} + +fn create_rename_entity_button_widget( + frame: &dear_imgui_bindings::Ui, + textures: &Textures, + ent_handle: &EntityHandle, + ent_title: &str, + popup_state: &mut Option<PopupState>, +) +{ + if frame + .image_button_config( + &format!("ent_{}_rename_button", ent_handle.uid()), + textures.edit_icon_tex_id, + [16.0, 16.0], + ) + .build() + { + enter_rename_entity_popup_state(popup_state, ent_handle, ent_title); + } +} + +fn enter_rename_entity_popup_state( + popup_state: &mut Option<PopupState>, + ent_handle: &EntityHandle, + ent_title: &str, +) -> bool +{ + if popup_state.is_some() { + return false; + } + + *popup_state = Some(PopupState::RenameEntity(RenameEntityPopupState { + popup_name: format!("Rename entity {ent_title}"), + target_entity_id: ent_handle.uid(), + new_entity_name: String::with_capacity(32), + })); + + true +} + +fn show_rename_entity_popup( + frame: &dear_imgui_bindings::Ui, + popup_state: &mut RenameEntityPopupState, + actions: &mut Actions, + world: &World, +) -> bool +{ + frame.open_popup(&popup_state.popup_name); + + let mut is_opened = true; + + let should_close = + frame.modal_popup_with_opened(&popup_state.popup_name, &mut is_opened, || { + frame.set_window_size([ + frame.calc_text_size(&popup_state.popup_name)[0] + 32.0, + 120.0, + ]); + + frame.spacing(); + frame.spacing(); + + frame.text("Name:"); + frame.same_line(); + + frame + .input_text( + "##rename_entity_popup_new_name_input", + &mut popup_state.new_entity_name, + ) + .build(); + + frame.spacing(); + frame.spacing(); + + let rename_text_size = frame.calc_text_size("Rename"); + + if frame.button_with_size( + "Rename##rename_entity_popup_rename_button", + [rename_text_size[0] * 2.0, rename_text_size[1] * 2.0], + ) { + let Some(target_ent) = world.get_entity(popup_state.target_entity_id) + else { + tracing::error!( + entity = ?popup_state.target_entity_id, + "Cannot rename entity: entity no longer exists" + ); + return true; + }; + + if let Some(mut target_ent_name) = target_ent.get_mut::<EntityName>() { + target_ent_name.name = popup_state.new_entity_name.clone().into(); + } else { + actions.add_components( + popup_state.target_entity_id, + (EntityName { + name: popup_state.new_entity_name.clone().into(), + },), + ); + } + + return true; + } + + false + }); + + if let Some(true) = should_close { + return true; + } + + if !is_opened { + return true; + } + + false +} + +fn enter_add_component_popup_state( + popup_state: &mut Option<PopupState>, + ent_handle: &EntityHandle, + ent_title: &str, + world: &World, +) -> bool +{ + if popup_state.is_some() { + return false; + } + + let popup_name = format!("Add component to {ent_title}"); + + let mut searchable_components = world + .query::<(&ComponentInfo,)>() + .iter_with_euids() + .map(|(id, (component_info,))| { + let user_creatable = if component_info.is_sole { + ComponentUserCreatable::No { reason: "is a sole" } + } else if component_info.type_reflection.is_none() { + ComponentUserCreatable::No { reason: "no type reflection" } + } else if component_info + .type_reflection + .is_some_and(|ty| !ty.has_default_value()) + { + ComponentUserCreatable::No { reason: "no default value" } + } else if !get_whole_type_is_user_editable( + component_info.type_reflection, + component_info.ty_id, + ) { + ComponentUserCreatable::No { + reason: "whole component is not user editable", + } + } else { + ComponentUserCreatable::Yes + }; + + (id, component_info.clone(), user_creatable) + }) + .collect::<Vec<_>>(); + + searchable_components.sort_by_key(|(_, _, user_creatable)| *user_creatable); + + *popup_state = Some(PopupState::AddComponent(AddComponentPopupState { + popup_name: popup_name.clone(), + target_entity_id: ent_handle.uid(), + search_text: String::with_capacity(20), + searched_components: searchable_components.clone(), + searchable_components, + selected_search_result: -1, + new_component: None, + })); + + true +} + +fn show_add_component_popup( + frame: &dear_imgui_bindings::Ui, + add_component_popup_state: &mut AddComponentPopupState, + actions: &mut Actions, +) -> bool +{ + frame.open_popup(&add_component_popup_state.popup_name); + + let mut is_opened = true; + + let should_close = frame.modal_popup_with_opened( + &add_component_popup_state.popup_name, + &mut is_opened, + || { + frame.set_window_size([700.0, 600.0]); + + frame.spacing(); + frame.spacing(); + + frame.text("Search:"); + frame.same_line(); + + if frame + .input_text( + "##add_component_popup_search_input", + &mut add_component_popup_state.search_text, + ) + .build() + { + add_component_popup_state.searched_components = add_component_popup_state + .searchable_components + .iter() + .cloned() + .filter(|(_, searchable_comp_info, _)| { + searchable_comp_info + .name + .contains(&add_component_popup_state.search_text) + }) + .collect(); + + add_component_popup_state.selected_search_result = -1; + } + + frame.spacing(); + frame.spacing(); + + let _item_width_token = frame.push_item_width(-1.0); + + if frame + .list_box_config("##add_component_popup_search_results") + .build_extended( + frame, + &mut add_component_popup_state.selected_search_result, + &add_component_popup_state.searched_components, + &|(_, comp_info, comp_user_creatable)| ListBoxItem { + label: comp_info.name.into(), + disabled: matches!( + comp_user_creatable, + ComponentUserCreatable::No { .. } + ), + hover_tooltip: match comp_user_creatable { + ComponentUserCreatable::Yes => None, + ComponentUserCreatable::No { reason } => Some( + format!("Component cannot be selected: {reason}").into(), + ), + }, + }, + ) + { + let (selected_comp_id, selected_comp_info, _) = add_component_popup_state + .searched_components + [add_component_popup_state.selected_search_result as usize] + .clone(); + + let Some(selected_component_type) = selected_comp_info.type_reflection + else { + unreachable!(); + }; + + let Some(selected_comp_default_value) = + selected_component_type.default_value() + else { + unreachable!(); // Only components with default values are selectable + }; + + add_component_popup_state.new_component = Some(( + selected_comp_default_value, + selected_comp_info, + selected_comp_id, + )); + } + + frame.spacing(); + frame.spacing(); + frame.spacing(); + + if let Some((new_component, new_component_info, _)) = + add_component_popup_state.new_component.as_mut() + { + let mut component_changed = false; + + let Some(new_component_type) = new_component_info.type_reflection else { + unreachable!(); + }; + + create_item_title_widget( + frame, + new_component_info.name, + &ItemType::Reflected(new_component_type), + None, + ); + + let item_type = ItemType::Reflected(new_component_type); + + let _token = item_type.indent_or_same_line(frame); + + add_item_to_frame( + frame, + ItemRef::Mutable(&mut **new_component), + &mut component_changed, + ItemInfo { + item_title: new_component_info.name, + item_tag: &format!( + "add_component_popup_component_{}", + new_component_info.name, + ), + item_type_name: None, + item_type, + item_is_read_only: false, + }, + &[], + ); + + frame.spacing(); + frame.spacing(); + frame.spacing(); + } + + let _disabled = frame.begin_disabled_with_cond( + add_component_popup_state.new_component.is_none(), + ); + + let add_text_size = frame.calc_text_size("Add"); + + if frame.button_with_size( + "Add##add_component_popup_add_button", + [add_text_size[0] * 2.0, add_text_size[1] * 2.0], + ) { + if let Some((new_component_data, new_component_info, new_component_id)) = + add_component_popup_state.new_component.take() + { + actions.add_components( + add_component_popup_state.target_entity_id, + [ComponentParts::from_component_info( + &new_component_info, + new_component_id, + new_component_data, + )], + ); + + return true; + } + } + + false + }, + ); + + if let Some(true) = should_close { + return true; + } + + if !is_opened { + return true; + } + + false +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum ComponentUserCreatable +{ + Yes, + No + { + reason: &'static str, + }, +} + +struct ItemInfo<'a> +{ + item_title: &'a str, + item_tag: &'a str, + item_type_name: Option<&'static str>, + item_type: ItemType, + item_is_read_only: bool, +} + +enum ItemType +{ + Reflected(&'static TypeReflection), + String, + CowStr, + Color(ColorItemType, LiteralType), +} + +impl ItemType +{ + fn spans_multiple_rows(&self) -> bool + { + match self { + Self::Reflected( + TypeReflection::Struct(_) + | TypeReflection::Array(_) + | TypeReflection::Slice(_), + ) => true, + Self::Reflected(TypeReflection::Enum(enum_ty)) => !enum_ty.is_unit_only, + Self::Reflected(TypeReflection::Reference(ref_ty)) => { + ItemType::Reflected(ref_ty.ty).spans_multiple_rows() + } + Self::Color(..) => false, + Self::Reflected(TypeReflection::Literal(_)) | Self::String | Self::CowStr => { + false + } + Self::Reflected(_) => unimplemented!(), + } + } + + fn indent_or_same_line<'frame>( + &self, + frame: &'frame dear_imgui_bindings::Ui, + ) -> Option<dear_imgui_bindings::IndentToken<'frame>> + { + if self.spans_multiple_rows() { + return Some(frame.begin_indent()); + } + + frame.same_line(); + + None + } +} + +#[derive(Debug)] +enum ColorItemType +{ + Rgb, + Rgba, +} + +fn get_item_type(ty: Option<&'static TypeReflection>, type_id: TypeId) + -> Option<ItemType> +{ + if let Some(TypeReflection::Struct(_)) = ty { + if type_id == TypeId::of::<Rgb<f32>>() { + return Some(ItemType::Color(ColorItemType::Rgb, LiteralType::F32)); + } + if type_id == TypeId::of::<Rgb<u8>>() { + return Some(ItemType::Color(ColorItemType::Rgb, LiteralType::U8)); + } + + if type_id == TypeId::of::<Rgba<f32>>() { + return Some(ItemType::Color(ColorItemType::Rgba, LiteralType::F32)); + } + if type_id == TypeId::of::<Rgba<u8>>() { + return Some(ItemType::Color(ColorItemType::Rgba, LiteralType::U8)); + } + } + + if let Some(ty) = ty { + return Some(ItemType::Reflected(ty)); + } else if type_id == TypeId::of::<String>() { + Some(ItemType::String) + } else if type_id == TypeId::of::<Cow<'static, str>>() { + Some(ItemType::CowStr) + } else { + None + } +} + +enum ItemRef<'item> +{ + Mutable(&'item mut dyn Any), + Immutable(&'item dyn Any), +} + +impl<'item> ItemRef<'item> +{ + fn get_struct_field( + &mut self, + ty: &StructReflection, + field_index: usize, + ) -> Result<ItemRef<'_>, ReflectionGetError> + { + match self { + ItemRef::Mutable(item) => { + (ty.try_get_field_mut)(*item, field_index).map(ItemRef::Mutable) + } + ItemRef::Immutable(item) => { + (ty.try_get_field)(*item, field_index).map(ItemRef::Immutable) + } + } + } + + fn get_enum_variant_field( + &mut self, + variant: &EnumVariantReflection, + field_index: usize, + ) -> Result<ItemRef<'_>, ReflectionGetError> + { + match self { + ItemRef::Mutable(item) => { + (variant.try_get_field_mut)(*item, field_index).map(ItemRef::Mutable) + } + ItemRef::Immutable(item) => { + (variant.try_get_field)(*item, field_index).map(ItemRef::Immutable) + } + } + } + + fn into_writable<T: Clone + 'static>(self) -> OwnedOrRefMut<'item, T> + { + match self { + ItemRef::Mutable(item) => OwnedOrRefMut::Ref( + (*item) + .downcast_mut::<T>() + .expect("Cannot downcast: wrong item type"), + ), + ItemRef::Immutable(item) => OwnedOrRefMut::Owned( + (*item) + .downcast_ref::<T>() + .expect("Cannot downcast: wrong item type") + .clone(), + ), + } + } +} + +impl AsRef<dyn Any> for ItemRef<'_> +{ + fn as_ref(&self) -> &dyn Any + { + match self { + Self::Mutable(item) => &**item, + Self::Immutable(item) => *item, + } + } +} + +#[derive(Debug)] +pub enum OwnedOrRefMut<'a, Value> +{ + Ref(&'a mut Value), + Owned(Value), +} + +impl<Value> Deref for OwnedOrRefMut<'_, Value> +{ + type Target = Value; + + fn deref(&self) -> &Self::Target + { + match self { + Self::Ref(value) => *value, + Self::Owned(value) => value, + } + } +} + +impl<Value> DerefMut for OwnedOrRefMut<'_, Value> +{ + fn deref_mut(&mut self) -> &mut Self::Target + { + match self { + Self::Ref(value) => *value, + Self::Owned(value) => value, + } + } +} + +fn create_item_title_widget( + frame: &dear_imgui_bindings::Ui, + item_title: &str, + item_type: &ItemType, + item_type_name: Option<&str>, +) +{ + if !matches!(item_type, ItemType::Reflected(TypeReflection::Reference(_))) { + frame.text(&item_title); + + if let Some(item_type_name) = item_type_name { + if frame.is_item_hovered() { + frame.tooltip_text(item_type_name); + } + } + } +} + +fn add_item_to_frame<'a>( + frame: &dear_imgui_bindings::Ui, + mut item: ItemRef<'_>, + data_changed: &mut bool, + ItemInfo { + item_title, + item_tag, + item_type_name, + item_type, + item_is_read_only, + }: ItemInfo<'a>, + prev_item_tags: &[&'a str], +) +{ + let _disabled = frame.begin_disabled_with_cond( + !matches!( + item_type, + ItemType::Reflected( + TypeReflection::Struct(_) + | TypeReflection::Enum(EnumReflection { is_unit_only: false, .. }) + | TypeReflection::Array(_) + | TypeReflection::Slice(_) + ) + ) && item_is_read_only, + ); + + match item_type { + ItemType::Reflected(TypeReflection::Struct(struct_ty)) => { + create_struct_widgets( + frame, + &mut item, + data_changed, + item_tag, + item_is_read_only, + struct_ty, + prev_item_tags, + ); + } + ItemType::Reflected(TypeReflection::Enum(enum_type)) => { + create_enum_widgets( + frame, + &mut item, + data_changed, + item_tag, + item_is_read_only, + enum_type, + prev_item_tags, + ); + } + ItemType::Reflected(TypeReflection::Literal(literal_reflection)) => { + match literal_reflection.ty { + LiteralType::U8 => create_scalar_item_input::<u8>( + frame, + &mut item, + data_changed, + &item_tag, + &prev_item_tags, + ), + LiteralType::I8 => create_scalar_item_input::<i8>( + frame, + &mut item, + data_changed, + &item_tag, + &prev_item_tags, + ), + LiteralType::U16 => create_scalar_item_input::<u16>( + frame, + &mut item, + data_changed, + &item_tag, + &prev_item_tags, + ), + LiteralType::I16 => create_scalar_item_input::<i16>( + frame, + &mut item, + data_changed, + &item_tag, + &prev_item_tags, + ), + LiteralType::U32 => create_scalar_item_input::<u32>( + frame, + &mut item, + data_changed, + &item_tag, + &prev_item_tags, + ), + LiteralType::I32 => create_scalar_item_input::<i32>( + frame, + &mut item, + data_changed, + &item_tag, + &prev_item_tags, + ), + LiteralType::U64 => create_scalar_item_input::<u64>( + frame, + &mut item, + data_changed, + &item_tag, + &prev_item_tags, + ), + LiteralType::I64 => create_scalar_item_input::<i64>( + frame, + &mut item, + data_changed, + &item_tag, + &prev_item_tags, + ), + LiteralType::F32 => create_scalar_item_input::<f32>( + frame, + &mut item, + data_changed, + &item_tag, + &prev_item_tags, + ), + LiteralType::F64 => create_scalar_item_input::<f64>( + frame, + &mut item, + data_changed, + &item_tag, + &prev_item_tags, + ), + LiteralType::Usize => create_scalar_item_input::<usize>( + frame, + &mut item, + data_changed, + &item_tag, + &prev_item_tags, + ), + LiteralType::Isize => create_scalar_item_input::<isize>( + frame, + &mut item, + data_changed, + &item_tag, + &prev_item_tags, + ), + LiteralType::U128 => { + let item = match item { + ItemRef::Mutable(item) => { + let Some(item) = (*item).downcast_mut::<u128>() else { + unreachable!(); + }; + + item + } + ItemRef::Immutable(item) => { + let Some(item) = (*item).downcast_ref::<u128>() else { + unreachable!(); + }; + + &mut item.clone() + } + }; + + frame.text(item.to_string()); + } + LiteralType::I128 => { + let item = match item { + ItemRef::Mutable(item) => { + let Some(item) = (*item).downcast_mut::<i128>() else { + unreachable!(); + }; + + item + } + ItemRef::Immutable(item) => { + let Some(item) = (*item).downcast_ref::<i128>() else { + unreachable!(); + }; + + &mut item.clone() + } + }; + + frame.text(item.to_string()); + } + LiteralType::Bool => { + let item = match item { + ItemRef::Mutable(item) => { + let Some(item) = (*item).downcast_mut::<bool>() else { + unreachable!(); + }; + + item + } + ItemRef::Immutable(item) => { + let Some(item) = (*item).downcast_ref::<bool>() else { + unreachable!(); + }; + + &mut item.clone() + } + }; + + if frame.checkbox( + create_item_label(&item_tag, "value_input", &prev_item_tags), + item, + ) { + *data_changed = true; + } + } + LiteralType::Str => { + let item = match item { + ItemRef::Mutable(item) => { + let Some(item) = (*item).downcast_ref::<&'static str>() + else { + unreachable!(); + }; + + *item + } + ItemRef::Immutable(item) => { + let Some(item) = (*item).downcast_ref::<&'static str>() + else { + unreachable!(); + }; + + *item + } + }; + + frame.text(item); + } + _ => unimplemented!(), + } + } + ItemType::Reflected(TypeReflection::Array(array_type)) => { + let mut prev_item_tags = prev_item_tags.to_vec(); + + prev_item_tags.push(item_tag); + + for array_item_index in 0..array_type.length { + let array_item_name = array_item_index.to_string(); + + let Ok(item_item) = (match &mut item { + ItemRef::Mutable(item) => { + (array_type.try_get_item_mut)(*item, array_item_index) + .map(ItemRef::Mutable) + } + ItemRef::Immutable(item) => { + (array_type.try_get_item)(*item, array_item_index) + .map(ItemRef::Immutable) + } + }) else { + unreachable!(); + }; + + create_item_title_widget( + frame, + &array_item_name, + &ItemType::Reflected(array_type.item_type), + Some(array_type.item_type_name()), + ); + + let array_item_type = ItemType::Reflected(array_type.item_type); + + let _token = array_item_type.indent_or_same_line(frame); + + add_item_to_frame( + frame, + item_item, + data_changed, + ItemInfo { + item_title: array_item_name.as_ref(), + item_tag: array_item_name.as_ref(), + item_type_name: Some(array_type.item_type_name()), + item_type: array_item_type, + item_is_read_only, + }, + &prev_item_tags, + ); + } + } + ItemType::Reflected(TypeReflection::Slice(slice_type)) => { + let mut prev_item_tags = prev_item_tags.to_vec(); + + prev_item_tags.push(item_tag); + + let Some(len) = (slice_type.try_get_len)(item.as_ref()) else { + unreachable!(); + }; + + for index in 0..len { + let item_name = index.to_string(); + + let Ok(item_item) = (slice_type.try_get_item)(item.as_ref(), index) + else { + unreachable!(); + }; + + let mut slice_item_changed = false; + + create_item_title_widget( + frame, + &item_name, + &ItemType::Reflected(slice_type.item_type), + Some(slice_type.item_type_name()), + ); + + let slice_item_type = ItemType::Reflected(slice_type.item_type); + + let _token = slice_item_type.indent_or_same_line(frame); + + add_item_to_frame( + frame, + ItemRef::Immutable(item_item), + &mut slice_item_changed, + ItemInfo { + item_title: &item_name, + item_tag: &item_name, + item_type_name: Some(slice_type.item_type_name()), + item_type: slice_item_type, + item_is_read_only: true, + }, + &prev_item_tags, + ); + + assert!(!slice_item_changed); + } + } + ItemType::Reflected(TypeReflection::Reference(ref_type)) => { + let mut derefed_changed = false; + + let Some(derefed_item) = (ref_type.try_deref)(item.as_ref()) else { + unreachable!(); + }; + + create_item_title_widget( + frame, + item_title, + &ItemType::Reflected(ref_type.ty), + item_type_name, + ); + + let ref_item_type = ItemType::Reflected(ref_type.ty); + + let _token = ref_item_type.indent_or_same_line(frame); + + add_item_to_frame( + frame, + ItemRef::Immutable(derefed_item), + &mut derefed_changed, + ItemInfo { + item_title, + item_tag: "derefed".into(), + item_type_name, + item_type: ref_item_type, + item_is_read_only: true, + }, + prev_item_tags, + ); + + assert!(!derefed_changed); + } + ItemType::String => { + let item = match item { + ItemRef::Mutable(item) => { + let Some(item) = (*item).downcast_mut::<String>() else { + unreachable!(); + }; + + item + } + ItemRef::Immutable(item) => { + let Some(item) = (*item).downcast_ref::<String>() else { + unreachable!(); + }; + + &mut item.clone() + } + }; + + if frame + .input_text( + create_item_label(&item_tag, "value_input", &prev_item_tags), + item, + ) + .build() + { + *data_changed = true; + } + } + ItemType::CowStr => { + let item = match item { + ItemRef::Mutable(item) => { + let Some(item) = (*item).downcast_mut::<Cow<'static, str>>() else { + unreachable!(); + }; + + item + } + ItemRef::Immutable(item) => { + let Some(item) = (*item).downcast_ref::<Cow<'static, str>>() else { + unreachable!(); + }; + + &mut item.clone() + } + }; + + if frame + .input_text( + create_item_label(&item_tag, "value_input", &prev_item_tags), + item.to_mut(), + ) + .build() + { + *data_changed = true; + } + } + ItemType::Color(ColorItemType::Rgb, LiteralType::F32) => { + let mut item = item.into_writable::<Rgb<f32>>(); + + let mut scratch = [item.r, item.g, item.b]; + + if frame + .color_edit3_config( + create_item_label(&item_tag, "value_input", &prev_item_tags), + &mut scratch, + ) + .display_mode(dear_imgui_bindings::ColorDisplayMode::Hex) + .build() + { + let [new_r, new_g, new_b] = scratch; + + *item = Rgb { r: new_r, g: new_g, b: new_b }; + + *data_changed = true; + } + } + ItemType::Color(ColorItemType::Rgb, LiteralType::U8) => { + let mut item = item.into_writable::<Rgb<u8>>(); + + let mut scratch = [ + item.r as f32 / 255.0, + item.g as f32 / 255.0, + item.b as f32 / 255.0, + ]; + + if frame + .color_edit3_config( + create_item_label(&item_tag, "value_input", &prev_item_tags), + &mut scratch, + ) + .display_mode(dear_imgui_bindings::ColorDisplayMode::Hex) + .build() + { + let [new_r, new_g, new_b] = scratch; + + *item = Rgb { + r: (new_r * 255.0) as u8, + g: (new_g * 255.0) as u8, + b: (new_b * 255.0) as u8, + }; + + *data_changed = true; + } + } + ItemType::Color(ColorItemType::Rgba, LiteralType::F32) => { + let mut item = item.into_writable::<Rgba<f32>>(); + + let mut scratch = [item.r, item.g, item.b, item.a]; + + if frame + .color_edit4_config( + create_item_label(&item_tag, "value_input", &prev_item_tags), + &mut scratch, + ) + .display_mode(dear_imgui_bindings::ColorDisplayMode::Hex) + .build() + { + let [new_r, new_g, new_b, new_a] = scratch; + + *item = Rgba { + r: new_r, + g: new_g, + b: new_b, + a: new_a, + }; + + *data_changed = true; + } + } + ItemType::Color(ColorItemType::Rgba, LiteralType::U8) => { + let mut item = item.into_writable::<Rgba<u8>>(); + + let mut scratch = [ + item.r as f32 / 255.0, + item.g as f32 / 255.0, + item.b as f32 / 255.0, + item.a as f32 / 255.0, + ]; + + if frame + .color_edit4_config( + create_item_label(&item_tag, "value_input", &prev_item_tags), + &mut scratch, + ) + .display_mode(dear_imgui_bindings::ColorDisplayMode::Hex) + .build() + { + let [new_r, new_g, new_b, new_a] = scratch; + + *item = Rgba { + r: (new_r * 255.0) as u8, + g: (new_g * 255.0) as u8, + b: (new_b * 255.0) as u8, + a: (new_a * 255.0) as u8, + }; + + *data_changed = true; + } + } + ItemType::Color(ColorItemType::Rgb, _) => unreachable!(), + ItemType::Color(ColorItemType::Rgba, _) => unreachable!(), + ItemType::Reflected(_) => unimplemented!(), + } +} + +fn create_scalar_item_input<Scalar>( + frame: &dear_imgui_bindings::Ui, + item: &mut ItemRef<'_>, + data_changed: &mut bool, + item_tag: &str, + prev_item_tags: &[&str], +) where + Scalar: dear_imgui_bindings::internal::DataTypeKind + Clone + 'static, +{ + let item = match item { + ItemRef::Mutable(item) => { + let Some(item) = (*item).downcast_mut::<Scalar>() else { + unreachable!(); + }; + + item + } + ItemRef::Immutable(item) => { + let Some(item) = (*item).downcast_ref::<Scalar>() else { + unreachable!(); + }; + + &mut item.clone() + } + }; + + if frame + .input_scalar( + create_item_label(item_tag, "value_input", &prev_item_tags), + item, + ) + .build() + { + *data_changed = true; + } +} + +fn item_layout_table<'frame>( + frame: &'frame dear_imgui_bindings::Ui, + prev_item_tags: &[&str], +) -> dear_imgui_bindings::TableBuilder<'frame> +{ + frame + .table(create_item_label("item_layout", "table", &prev_item_tags)) + .headers(false) + .sizing_policy(dear_imgui_bindings::TableSizingPolicy::StretchSame) + .columns([ + dear_imgui_bindings::TableColumnSetup::new("a"), + dear_imgui_bindings::TableColumnSetup::new("b").stretch_weight(2.0), + ]) +} + +fn create_struct_widgets( + frame: &dear_imgui_bindings::Ui, + item: &mut ItemRef<'_>, + data_changed: &mut bool, + item_tag: &str, + item_is_read_only: bool, + struct_ty: &StructReflection, + prev_item_tags: &[&str], +) +{ + let mut prev_item_tags = prev_item_tags.to_vec(); + + prev_item_tags.push(item_tag.as_ref()); + + let struct_fields = + struct_ty + .fields + .iter() + .enumerate() + .filter_map(|(field_index, field)| { + let field_item_type = + get_item_type(field.type_reflection(), field.type_id)?; + + Some((field_index, field, field_item_type)) + }); + + item_layout_table(frame, &prev_item_tags).build(|frame| { + for (field_index, field, field_item_type) in struct_fields + .clone() + .filter(|(.., field_item_type)| !field_item_type.spans_multiple_rows()) + { + frame.table_next_row(); + frame.table_next_column(); + + let field_name = field + .name + .map(Cow::Borrowed) + .unwrap_or_else(|| field_index.to_string().into()); + + let Ok(field_item) = item.get_struct_field(struct_ty, field_index) else { + unreachable!(); + }; + + create_item_title_widget( + frame, + &field_name, + &field_item_type, + Some(field.type_name()), + ); + + frame.table_next_column(); + + add_item_to_frame( + frame, + field_item, + data_changed, + ItemInfo { + item_title: field_name.as_ref(), + item_tag: field_name.as_ref(), + item_type_name: Some(field.type_name()), + item_type: field_item_type, + item_is_read_only: item_is_read_only + || matches!( + field.visibility, + Visibility::Private | Visibility::PubScoped(_) + ), + }, + &prev_item_tags, + ); + } + }); + + for (field_index, field, field_item_type) in struct_fields + .filter(|(.., field_item_type)| field_item_type.spans_multiple_rows()) + { + let field_name = field + .name + .map(Cow::Borrowed) + .unwrap_or_else(|| field_index.to_string().into()); + + let Ok(field_item) = item.get_struct_field(struct_ty, field_index) else { + unreachable!(); + }; + + create_item_title_widget( + frame, + &field_name, + &field_item_type, + Some(field.type_name()), + ); + + let _token = frame.begin_indent(); + + add_item_to_frame( + frame, + field_item, + data_changed, + ItemInfo { + item_title: field_name.as_ref(), + item_tag: field_name.as_ref(), + item_type_name: Some(field.type_name()), + item_type: field_item_type, + item_is_read_only: item_is_read_only + || matches!( + field.visibility, + Visibility::Private | Visibility::PubScoped(_) + ), + }, + &prev_item_tags, + ); + } +} + +fn create_enum_widgets( + frame: &dear_imgui_bindings::Ui, + item: &mut ItemRef<'_>, + data_changed: &mut bool, + item_tag: &str, + item_is_read_only: bool, + enum_type: &EnumReflection, + prev_item_tags: &[&str], +) +{ + let Some(mut curr_variant_index) = (enum_type.get_variant_index)(item.as_ref()) + else { + unreachable!(); + }; + + if create_combo_box( + frame, + create_item_label(&item_tag, "variant_select", &prev_item_tags), + &mut curr_variant_index, + enum_type.variants, + |variant| { + let disabled = variant.fields.as_ref().and_then(|fields| { + fields.fields().iter().find_map(|field| { + let field_name = match field.name { + Some(field_name) => Either::A(field_name), + None => Either::B(field.index), + }; + + let Some(field_type) = field.type_reflection() else { + return Some((field_name, "has no type reflection")); + }; + + if !field_type.has_default_value() { + return Some((field_name, "has no default value")); + } + + None + }) + }); + + ComboBoxItem { + label: variant.name.into(), + disabled: disabled.is_some(), + hover_tooltip: disabled.map(|(bad_field_name, bad_field_reason)| { + format!( + "Variant cannot be selected since field {} {}", + bad_field_name, bad_field_reason + ) + .into() + }), + } + }, + ) { + 'used: { + let ItemRef::Mutable(item) = item else { + break 'used; + }; + + let new_variant = &enum_type.variants[curr_variant_index]; + + let Ok(()) = (new_variant.try_write_new_to)( + *item, + &mut new_variant + .fields + .iter() + .map(|fields| fields.fields()) + .flatten() + .map(|field| { + let Some(field_type) = field.type_reflection() else { + // Variants with any field that is missing type reflection is + // disabled so that the user cannot select it + unreachable!(); + }; + + let Some(default_value) = field_type.default_value() else { + unreachable!(); + }; + + default_value + }), + ) else { + unreachable!(); + }; + + *data_changed = true; + } + } + + let mut prev_item_tags = prev_item_tags.to_vec(); + + prev_item_tags.push(item_tag); + + let curr_variant = &enum_type.variants[curr_variant_index]; + + let Some(curr_variant_fields) = &curr_variant.fields else { + return; + }; + + let variant_fields = curr_variant_fields.fields().iter().enumerate().filter_map( + |(field_index, field)| { + let field_item_type = get_item_type(field.type_reflection(), field.type_id)?; + + Some((field_index, field, field_item_type)) + }, + ); + + let variant_field_cnt = variant_fields.clone().count(); + + item_layout_table(frame, &prev_item_tags).build(|_| { + for (field_index, field, field_item_type) in variant_fields + .clone() + .filter(|(.., field_item_type)| !field_item_type.spans_multiple_rows()) + { + frame.table_next_row(); + frame.table_next_column(); + + let field_name: Cow<str> = match field.name { + Some(field_name) => field_name.into(), + None => field_index.to_string().into(), + }; + + let Ok(field_item) = item.get_enum_variant_field(curr_variant, field_index) + else { + unreachable!(); + }; + + if variant_field_cnt != 1 || field.name.is_some() { + create_item_title_widget( + frame, + &field_name, + &field_item_type, + Some(field.type_name()), + ); + + frame.table_next_column(); + } + + add_item_to_frame( + frame, + field_item, + data_changed, + ItemInfo { + item_title: field_name.as_ref(), + item_tag: field_name.as_ref(), + item_type_name: Some(field.type_name()), + item_type: field_item_type, + item_is_read_only: item_is_read_only, + }, + &prev_item_tags, + ); + } + }); + + for (field_index, field, field_item_type) in variant_fields + .filter(|(.., field_item_type)| field_item_type.spans_multiple_rows()) + { + let field_name: Cow<str> = match field.name { + Some(field_name) => field_name.into(), + None => field_index.to_string().into(), + }; + + let Ok(field_item) = item.get_enum_variant_field(curr_variant, field_index) + else { + unreachable!(); + }; + + if variant_field_cnt != 1 || field.name.is_some() { + create_item_title_widget( + frame, + &field_name, + &field_item_type, + Some(field.type_name()), + ); + + let _token = frame.begin_indent(); + } + + add_item_to_frame( + frame, + field_item, + data_changed, + ItemInfo { + item_title: field_name.as_ref(), + item_tag: field_name.as_ref(), + item_type_name: Some(field.type_name()), + item_type: field_item_type, + item_is_read_only: item_is_read_only, + }, + &prev_item_tags, + ); + } +} + +fn create_item_label(item_tag: &str, value_name: &str, prev_item_tags: &[&str]) + -> String +{ + let mut item_label = String::with_capacity( + 2 + item_tag.len() + + prev_item_tags.len() + + prev_item_tags.iter().max().unwrap_or(&"".into()).len(), + ); + + item_label.push_str("##"); + + for prev_item_tag in prev_item_tags { + item_label.push('/'); + item_label.push_str(prev_item_tag); + } + + item_label.push('/'); + item_label.push_str(item_tag); + item_label.push(':'); + item_label.push_str(value_name); + + item_label +} + +fn get_whole_type_is_user_editable( + ty: Option<&'static TypeReflection>, + type_id: TypeId, +) -> bool +{ + let Some(item_type) = get_item_type(ty, type_id) else { + return false; + }; + + match item_type { + ItemType::Reflected(TypeReflection::Struct(struct_ty)) => { + for field in struct_ty.fields { + if matches!( + field.visibility, + Visibility::Private | Visibility::PubScoped(_) + ) { + continue; + } + + if !get_whole_type_is_user_editable( + field.type_reflection(), + field.type_id, + ) { + return false; + } + } + + true + } + ItemType::Reflected(TypeReflection::Enum(enum_ty)) => { + enum_ty.variants.iter().any(|variant| { + for field in variant.fields.iter().flat_map(|fields| fields.fields()) { + if !get_whole_type_is_user_editable( + field.type_reflection(), + field.type_id, + ) { + return false; + } + } + + true + }) + } + ItemType::Reflected( + TypeReflection::Slice(_) + | TypeReflection::Array(_) + | TypeReflection::Literal(_), + ) + | ItemType::String + | ItemType::CowStr + | ItemType::Color(..) => true, + ItemType::Reflected(_) => unimplemented!(), + } +} + +struct ComboBoxItem<'b> +{ + label: Cow<'b, str>, + disabled: bool, + hover_tooltip: Option<Cow<'b, str>>, +} + +fn create_combo_box<Item, ItemFn>( + frame: &dear_imgui_bindings::Ui, + label: impl AsRef<str>, + current_item: &mut usize, + items: &[Item], + item_fn: ItemFn, +) -> bool +where + for<'b> ItemFn: Fn(&'b Item) -> ComboBoxItem<'b>, +{ + let item_fn = &item_fn; + + let mut result = false; + + let preview_label = items.get(*current_item).map(|item| item_fn(item).label); + + if let Some(combo_token) = frame.begin_combo( + label, + preview_label + .as_ref() + .map(|preview_label| preview_label.as_ref()) + .unwrap_or(""), + ) { + for (idx, item) in items.iter().enumerate() { + let is_selected = idx == *current_item; + + if is_selected { + frame.set_item_default_focus(); + } + + let ComboBoxItem { + label: item_label, + disabled: item_disabled, + hover_tooltip: item_hover_tooltip, + } = item_fn(item); + + let disabled_token = frame.begin_disabled_with_cond(item_disabled); + + let clicked = frame.selectable(item_label.as_ref()); + + if clicked { + *current_item = idx; + result = true; + } + + disabled_token.end(); + + if let Some(item_hover_tooltip) = item_hover_tooltip { + if frame.is_item_hovered_with_flags( + dear_imgui_bindings::ItemHoveredFlags::ALLOW_WHEN_DISABLED, + ) { + frame.tooltip_text(item_hover_tooltip.as_ref()); + } + } + } + + combo_token.end(); + } + + result +} + +struct ListBoxItem<'b> +{ + label: Cow<'b, str>, + disabled: bool, + hover_tooltip: Option<Cow<'b, str>>, +} + +trait ListBoxExt +{ + fn build_extended<Item, ItemFn>( + self, + ui: &dear_imgui_bindings::Ui, + current_item: &mut i32, + items: &[Item], + item_fn: &ItemFn, + ) -> bool + where + for<'b> ItemFn: Fn(&'b Item) -> ListBoxItem<'b>; +} + +impl<Label> ListBoxExt for dear_imgui_bindings::ListBox<Label> +where + Label: AsRef<str>, +{ + fn build_extended<Item, ItemFn>( + self, + ui: &dear_imgui_bindings::Ui, + current_item: &mut i32, + items: &[Item], + item_fn: &ItemFn, + ) -> bool + where + for<'b> ItemFn: Fn(&'b Item) -> ListBoxItem<'b>, + { + let mut result = false; + + let lb = self; + + if let Some(_cb) = lb.begin(ui) { + for (idx, item) in items.iter().enumerate() { + if idx > i32::MAX as usize { + break; + } + + let idx_i32 = idx as i32; + + let ListBoxItem { + label: item_label, + disabled: item_disabled, + hover_tooltip: item_hover_tooltip, + } = item_fn(item); + + let is_selected = idx_i32 == *current_item; + + let disabled_token = ui.begin_disabled_with_cond(item_disabled); + + if ui + .selectable_config(&item_label) + .selected(is_selected) + .build() + { + *current_item = idx_i32; + result = true; + } + + disabled_token.end(); + + if let Some(item_hover_tooltip) = item_hover_tooltip { + if ui.is_item_hovered_with_flags( + dear_imgui_bindings::ItemHoveredFlags::ALLOW_WHEN_DISABLED, + ) { + ui.tooltip_text(item_hover_tooltip.as_ref()); + } + } + } + } + + result + } +} + +fn create_entity_title(ent_handle: &EntityHandle) -> String +{ + let ent_name = ent_handle.get::<EntityName>(); + + format!( + "{} {}", + ent_name + .as_ref() + .map(|ent_name| ent_name.name.as_ref()) + .unwrap_or("<unnamed>"), + ent_handle.uid() + ) +} + +struct Textures +{ + despawn_icon_tex_id: dear_imgui_bindings::ManagedTextureId, + warning_icon_tex_id: dear_imgui_bindings::ManagedTextureId, + edit_icon_tex_id: dear_imgui_bindings::ManagedTextureId, +} + +impl Textures +{ + fn new(dear_imgui_context: &mut DearImguiContext) -> Self + { + let despawn_icon_tex_id = create_texture( + dear_imgui_context, + Image::from_reader( + Cursor::new(include_bytes!("../../../res/ui/delete.png")), + ImageFormat::Png, + ) + .unwrap(), + ); + + let warning_icon_tex_id = create_texture( + dear_imgui_context, + Image::from_reader( + Cursor::new(include_bytes!("../../../res/ui/warning.png")), + ImageFormat::Png, + ) + .unwrap(), + ); + + let edit_icon_tex_id = create_texture( + dear_imgui_context, + Image::from_reader( + Cursor::new(include_bytes!("../../../res/ui/edit.png")), + ImageFormat::Png, + ) + .unwrap(), + ); + + Self { + despawn_icon_tex_id, + warning_icon_tex_id, + edit_icon_tex_id, + } + } +} + +#[derive(Debug)] +struct AddComponentPopupState +{ + popup_name: String, + target_entity_id: Uid, + search_text: String, + searched_components: Vec<(Uid, ComponentInfo, ComponentUserCreatable)>, + searchable_components: Vec<(Uid, ComponentInfo, ComponentUserCreatable)>, + selected_search_result: i32, + new_component: Option<(Box<dyn Any>, ComponentInfo, Uid)>, +} + +#[derive(Debug)] +struct RenameEntityPopupState +{ + popup_name: String, + target_entity_id: Uid, + new_entity_name: String, +} + +#[derive(Debug)] +enum PopupState +{ + AddComponent(AddComponentPopupState), + RenameEntity(RenameEntityPopupState), +} diff --git a/engine/src/util.rs b/engine/src/util.rs index a505a38..f016ad3 100644 --- a/engine/src/util.rs +++ b/engine/src/util.rs @@ -1,18 +1,265 @@ -macro_rules! try_option { - ($expr: expr) => { - match $expr { - Ok(value) => value, - Err(err) => { - return Some(Err(err.into())); - } +use std::fmt::Debug; + +use crate::ecs::util::StreamingIterator; + +pub trait OptionExt<T> +{ + /// Substitute for the currently experimental function + /// [`Option::get_or_try_insert_with`]. + /// See https://github.com/rust-lang/rust/issues/143648 + fn get_or_try_insert_with_fn<Err>( + &mut self, + func: impl Fn() -> Result<T, Err>, + ) -> Result<&mut T, Err>; +} + +impl<T> OptionExt<T> for Option<T> +{ + fn get_or_try_insert_with_fn<Err>( + &mut self, + func: impl FnOnce() -> Result<T, Err>, + ) -> Result<&mut T, Err> + { + if let None = self { + *self = Some(func()?); } - }; + + Ok(unsafe { self.as_mut().unwrap_unchecked() }) + } +} + +#[derive(Debug)] +pub struct BitArray<const SIZE: usize, const BITS_PER_ITEM: usize> +{ + inner: [u8; SIZE], +} + +impl<const SIZE: usize, const BITS_PER_ITEM: usize> BitArray<SIZE, BITS_PER_ITEM> +{ + const ITEM_MASK: u8 = !(u8::MAX << BITS_PER_ITEM); + + pub fn new() -> Self + { + assert!(BITS_PER_ITEM > 1); + assert!(BITS_PER_ITEM <= 8); + assert_eq!(BITS_PER_ITEM % 2, 0); + + Self { inner: [0; SIZE] } + } + + pub fn get(&self, item_index: usize) -> u8 + { + let bit_index = item_index * BITS_PER_ITEM; + + let byte_index = bit_index / 8; + + let bit_index_in_byte = bit_index - (byte_index * 8); + + (self.inner[byte_index] >> (bit_index_in_byte)) & Self::ITEM_MASK + } + + #[tracing::instrument(skip(self))] + pub fn clear_and_set(&mut self, item_index: usize, item_bits: u8, clear_mask: u8) + { + let item_bits = item_bits & Self::ITEM_MASK; + + let clear_mask = clear_mask & Self::ITEM_MASK; + + let bit_index = item_index * BITS_PER_ITEM; + + let byte_index = bit_index / 8; + + let bit_index_in_byte = bit_index - (byte_index * 8); + + tracing::trace!( + bit_index, + byte_index, + bit_index_in_byte, + "Setting item bits" + ); + + self.inner[byte_index] &= !(clear_mask << bit_index_in_byte); + + self.inner[byte_index] |= item_bits << bit_index_in_byte; + } + + pub fn iter_occupied(&self) -> BitArrayOccupiedIter<'_, BITS_PER_ITEM> + { + BitArrayOccupiedIter { + inner: self.inner.iter().enumerate().peekable(), + mask: u8::MAX, + } + } + + pub fn iter_occupied_mut(&mut self) -> BitArrayOccupiedMutIter<'_, BITS_PER_ITEM> + { + BitArrayOccupiedMutIter { + inner: self.inner.iter_mut().enumerate().peekable(), + mask: u8::MAX, + } + } + + pub fn bytes_mut(&mut self) -> &mut [u8] + { + &mut self.inner + } +} + +impl<const SIZE: usize, const BITS_PER_ITEM: usize> Default + for BitArray<SIZE, BITS_PER_ITEM> +{ + fn default() -> Self + { + Self::new() + } } -use std::mem::ManuallyDrop; -use std::ops::{Deref, DerefMut}; +pub struct BitArrayOccupiedIter<'a, const BITS_PER_ITEM: usize> +{ + inner: std::iter::Peekable<std::iter::Enumerate<std::slice::Iter<'a, u8>>>, + mask: u8, +} + +impl<const BITS_PER_ITEM: usize> BitArrayOccupiedIter<'_, BITS_PER_ITEM> +{ + const ITEM_MASK: u8 = !(u8::MAX << BITS_PER_ITEM); +} + +impl<const BITS_PER_ITEM: usize> Iterator for BitArrayOccupiedIter<'_, BITS_PER_ITEM> +{ + type Item = (usize, u8); + + fn next(&mut self) -> Option<Self::Item> + { + let (byte_masked, byte_index, item_bit_index_in_byte) = loop { + let current = self.inner.peek().and_then(|(byte_index, byte)| { + let byte_masked = **byte & self.mask; + + let lowest_one = byte_masked.lowest_one()?; + + let item_bit_index_in_byte = match lowest_one as usize % BITS_PER_ITEM { + 0 => lowest_one as usize, + remainder => lowest_one as usize - remainder, + }; + + Some((byte_masked, byte_index, item_bit_index_in_byte)) + }); + + let Some((byte_masked, byte_index, item_bit_index_in_byte)) = current else { + let _ = self.inner.next()?; + + self.mask = u8::MAX; + + continue; + }; + + break (byte_masked, byte_index, item_bit_index_in_byte); + }; + + let item_bits = (byte_masked >> (item_bit_index_in_byte)) & Self::ITEM_MASK; + + self.mask &= (!Self::ITEM_MASK) << item_bit_index_in_byte; -pub(crate) use try_option; + let item_index_in_byte = item_bit_index_in_byte as usize / BITS_PER_ITEM; + + let prev_bytes_item_cnt = (byte_index * 8) / BITS_PER_ITEM; + + let index = prev_bytes_item_cnt + item_index_in_byte; + + Some((index, item_bits)) + } +} + +pub struct BitArrayOccupiedMutIter<'a, const BITS_PER_ITEM: usize> +{ + inner: std::iter::Peekable<std::iter::Enumerate<std::slice::IterMut<'a, u8>>>, + mask: u8, +} + +impl<const BITS_PER_ITEM: usize> BitArrayOccupiedMutIter<'_, BITS_PER_ITEM> +{ + const ITEM_MASK: u8 = !(u8::MAX << BITS_PER_ITEM); +} + +impl<const BITS_PER_ITEM: usize> StreamingIterator + for BitArrayOccupiedMutIter<'_, BITS_PER_ITEM> +{ + type Item<'a> + = BitArrayItemMut<'a, BITS_PER_ITEM> + where + Self: 'a; + + fn streaming_next(&mut self) -> Option<Self::Item<'_>> + { + let (byte_masked, byte_index, item_bit_index_in_byte) = loop { + let current = self.inner.peek().and_then(|(byte_index, byte)| { + let byte_masked = **byte & self.mask; + + let lowest_one = byte_masked.lowest_one()?; + + let item_bit_index_in_byte = match lowest_one as usize % BITS_PER_ITEM { + 0 => lowest_one as usize, + remainder => lowest_one as usize - remainder, + }; + + Some((byte_masked, byte_index, item_bit_index_in_byte)) + }); + + let Some((byte_masked, byte_index, item_bit_index_in_byte)) = current else { + let _ = self.inner.next()?; + + self.mask = u8::MAX; + + continue; + }; + + break (byte_masked, byte_index, item_bit_index_in_byte); + }; + + let item_bits = (byte_masked >> (item_bit_index_in_byte)) & Self::ITEM_MASK; + + self.mask &= (!Self::ITEM_MASK) << item_bit_index_in_byte; + + let item_index_in_byte = item_bit_index_in_byte as usize / BITS_PER_ITEM; + + let prev_bytes_item_cnt = (byte_index * 8) / BITS_PER_ITEM; + + let index = prev_bytes_item_cnt + item_index_in_byte; + + Some(BitArrayItemMut { + index, + bits: item_bits, + byte: self.inner.peek_mut().unwrap().1, + bit_index_in_byte: item_bit_index_in_byte, + }) + } +} + +pub struct BitArrayItemMut<'a, const BITS_PER_ITEM: usize> +{ + pub index: usize, + pub bits: u8, + byte: &'a mut u8, + bit_index_in_byte: usize, +} + +impl<'a, const BITS_PER_ITEM: usize> BitArrayItemMut<'a, BITS_PER_ITEM> +{ + const ITEM_MASK: u8 = !(u8::MAX << BITS_PER_ITEM); + + pub fn clear_and_set(&mut self, new_bits: u8, clear_mask: u8) + { + let new_bits = new_bits & Self::ITEM_MASK; + + let clear_mask = clear_mask & Self::ITEM_MASK; + + *self.byte &= !(clear_mask << self.bit_index_in_byte); + + *self.byte |= new_bits << self.bit_index_in_byte; + + self.bits = new_bits; + } +} macro_rules! or { (($($tt: tt)+) else ($($else_tt: tt)*)) => { @@ -26,6 +273,18 @@ macro_rules! or { pub(crate) use or; +#[macro_export] +macro_rules! expand_map_opt { + ($in: tt, no_occurance=($($no_occurance: tt)*), occurance=($($occurance: tt)*)) => { + $($occurance)* + }; + + (, no_occurance=($($no_occurance: tt)*), occurance=($($occurance: tt)*)) => { + $($no_occurance)* + }; +} + +#[macro_export] macro_rules! builder { ( $(#[doc = $doc: literal])* @@ -37,7 +296,8 @@ macro_rules! builder { $visibility: vis struct $name: ident { $( - $(#[$field_attr: meta])* + $(#[doc = $field_doc: literal])* + $(#[builder(skip_generate_fn$($field_skip_generate_fn: tt)?)])? $field_visibility: vis $field: ident: $field_type: ty, )* } @@ -47,7 +307,7 @@ macro_rules! builder { $visibility struct $name { $( - $(#[$field_attr])* + $(#[doc = $field_doc])* $field_visibility $field: $field_type, )* } @@ -63,16 +323,23 @@ macro_rules! builder { impl $builder_name { $( - #[must_use] - $visibility fn $field(mut self, $field: $field_type) -> Self - { - self.$field = $field; - self - } + $crate::expand_map_opt!( + $(true $($field_skip_generate_fn)?)?, + no_occurance=( + #[must_use] + $visibility fn $field(mut self, $field: $field_type) -> Self + { + self.$field = $field; + self + } + ), + occurance=() + ); )* #[must_use] - $visibility fn build(self) -> $name { + $visibility const fn build(self) -> $name + { $name { $( $field: self.$field, @@ -83,6 +350,7 @@ macro_rules! builder { impl From<$name> for $builder_name { + #[allow(unused_variables)] fn from(built: $name) -> Self { Self { @@ -94,39 +362,3 @@ macro_rules! builder { } }; } - -pub(crate) use builder; - -/// Wrapper that ensures the contained value will never be dropped. -#[derive(Debug)] -pub struct NeverDrop<Value> -{ - value: ManuallyDrop<Value>, -} - -impl<Value> NeverDrop<Value> -{ - #[must_use] - pub fn new(value: Value) -> Self - { - Self { value: ManuallyDrop::new(value) } - } -} - -impl<Value> Deref for NeverDrop<Value> -{ - type Target = Value; - - fn deref(&self) -> &Self::Target - { - &self.value - } -} - -impl<Value> DerefMut for NeverDrop<Value> -{ - fn deref_mut(&mut self) -> &mut Self::Target - { - &mut self.value - } -} diff --git a/engine/src/vertex.rs b/engine/src/vertex.rs deleted file mode 100644 index 897ee97..0000000 --- a/engine/src/vertex.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::mem::size_of; - -use crate::util::builder; -use crate::vector::{Vec2, Vec3}; - -builder! { -#[builder(name = Builder, derives = (Debug, Default))] -#[derive(Debug, Clone, Default)] -#[repr(C)] -pub struct Vertex -{ - pos: Vec3<f32>, - texture_coords: Vec2<f32>, - normal: Vec3<f32>, -} -} - -impl Vertex -{ - pub(crate) fn attrs() -> &'static [Attribute] - { - #[allow(clippy::cast_possible_truncation)] - &[ - Attribute { - index: 0, - component_type: AttributeComponentType::Float, - component_cnt: AttributeComponentCnt::Three, - component_size: size_of::<f32>() as u32, - }, - Attribute { - index: 1, - component_type: AttributeComponentType::Float, - component_cnt: AttributeComponentCnt::Two, - component_size: size_of::<f32>() as u32, - }, - Attribute { - index: 2, - component_type: AttributeComponentType::Float, - component_cnt: AttributeComponentCnt::Three, - component_size: size_of::<f32>() as u32, - }, - ] - } -} - -pub(crate) struct Attribute -{ - pub(crate) index: u32, - pub(crate) component_type: AttributeComponentType, - pub(crate) component_cnt: AttributeComponentCnt, - pub(crate) component_size: u32, -} - -pub(crate) enum AttributeComponentType -{ - Float, -} - -#[derive(Debug, Clone, Copy)] -#[repr(u32)] -#[allow(dead_code)] -pub(crate) enum AttributeComponentCnt -{ - One = 1, - Two = 2, - Three = 3, - Four = 4, -} diff --git a/engine/src/window.rs b/engine/src/window.rs deleted file mode 100644 index ccc1b8d..0000000 --- a/engine/src/window.rs +++ /dev/null @@ -1,318 +0,0 @@ -use std::borrow::Cow; -use std::ffi::{CStr, CString}; - -use ecs::actions::Actions; -use ecs::extension::Collector as ExtensionCollector; -use ecs::sole::Single; -use ecs::Sole; -use glfw::WindowSize; - -use crate::data_types::dimens::Dimens; -use crate::event::{Conclude as ConcludeEvent, Start as StartEvent}; -use crate::vector::Vec2; - -mod reexports -{ - pub use glfw::window::{ - CursorMode, - Hint as CreationHint, - HintValue as CreationHintValue, - InputMode, - Key, - KeyModifiers, - KeyState, - }; -} - -pub use reexports::*; - -#[derive(Debug, Sole)] -/// Has to be dropped last since it holds the OpenGL context. -#[sole(drop_last)] -pub struct Window -{ - inner: glfw::Window, -} - -impl Window -{ - /// Returns a new Window builder. - #[must_use] - pub fn builder() -> Builder - { - Builder::default() - } - - /// Sets the value of a input mode. - /// - /// # Errors - /// Returns `Err` if the input mode is unsupported on the current system. - pub fn set_input_mode( - &self, - input_mode: InputMode, - enabled: bool, - ) -> Result<(), Error> - { - Ok(self.inner.set_input_mode(input_mode, enabled)?) - } - - /// Sets the cursor mode. - /// - /// # Errors - /// If a platform error occurs. - pub fn set_cursor_mode(&self, cursor_mode: CursorMode) -> Result<(), Error> - { - Ok(self.inner.set_cursor_mode(cursor_mode)?) - } - - /// Returns whether or not the window should close. Will return true when the user has - /// attempted to close the window. - #[must_use] - pub fn should_close(&self) -> bool - { - self.inner.should_close() - } - - /// Processes all pending events. - /// - /// # Errors - /// If a platform error occurs. - pub fn poll_events(&self) -> Result<(), Error> - { - Ok(self.inner.poll_events()?) - } - - /// Swaps the front and back buffers of the window. - /// - /// # Errors - /// Will return `Err` if a platform error occurs or if no OpenGL window context - /// is present. - pub fn swap_buffers(&self) -> Result<(), Error> - { - Ok(self.inner.swap_buffers()?) - } - - /// Returns the size of the window. - /// - /// # Errors - /// Will return `Err` if a platform error occurs. - pub fn size(&self) -> Result<Dimens<u32>, Error> - { - let size = self.inner.size()?; - - Ok(Dimens { - width: size.width, - height: size.height, - }) - } - - /// Returns the address of the specified OpenGL function, if it is supported by the - /// current OpenGL context. - /// - /// # Errors - /// Will return `Err` if a platform error occurs or if no current context has - /// been set. - /// - /// # Panics - /// Will panic if the `proc_name` argument contains a nul byte. - pub fn get_proc_address( - &self, - proc_name: &str, - ) -> Result<unsafe extern "C" fn(), Error> - { - let proc_name_c: Cow<CStr> = CStr::from_bytes_with_nul(proc_name.as_bytes()) - .map(Cow::Borrowed) - .or_else(|_| CString::new(proc_name).map(Cow::Owned)) - .expect("OpenGL function name contains a nul byte"); - - Ok(self.inner.get_proc_address(&proc_name_c)?) - } - - /// Makes the OpenGL context of the window current for the calling thread. - /// - /// # Errors - /// Will return `Err` if a platform error occurs or if no OpenGL context is - /// present. - pub fn make_context_current(&self) -> Result<(), Error> - { - Ok(self.inner.make_context_current()?) - } - - /// Sets the window's framebuffer size callback. - pub fn set_framebuffer_size_callback(&self, callback: impl Fn(Dimens<u32>) + 'static) - { - self.inner.set_framebuffer_size_callback(move |size| { - callback(Dimens { - width: size.width, - height: size.height, - }); - }); - } - - /// Sets the window's key size callback. - pub fn set_key_callback( - &self, - callback: impl Fn(Key, i32, KeyState, KeyModifiers) + 'static, - ) - { - self.inner.set_key_callback(callback); - } - - /// Sets the window's cursor position callback. - pub fn set_cursor_pos_callback(&self, callback: impl Fn(Vec2<f64>) + 'static) - { - self.inner - .set_cursor_pos_callback(move |pos| callback(Vec2 { x: pos.x, y: pos.y })); - } - - /// Sets the window's close callback. - pub fn set_close_callback(&self, callback: impl Fn() + 'static) - { - self.inner.set_close_callback(callback); - } - - /// Sets the window's focus callback. The callback is called when the window loses or - /// gains input focus. - pub fn set_focus_callback(&self, callback: impl Fn(bool) + 'static) - { - self.inner.set_focus_callback(callback); - } -} - -/// [`Window`] builder. -#[derive(Debug, Clone, Default)] -pub struct Builder -{ - inner: glfw::WindowBuilder, -} - -impl Builder -{ - #[must_use] - pub fn creation_hint(mut self, hint: CreationHint, value: CreationHintValue) -> Self - { - self.inner = self.inner.hint(hint, value); - - self - } - - /// Creates a new window. - /// - /// # Errors - /// Will return `Err` if the title contains a internal nul byte or if a platform error - /// occurs. - pub fn create(&self, size: Dimens<u32>, title: &str) -> Result<Window, Error> - { - let builder = self.inner.clone().hint( - CreationHint::OpenGLDebugContext, - CreationHintValue::Bool(cfg!(feature = "debug")), - ); - - let window = builder.create( - &WindowSize { - width: size.width, - height: size.height, - }, - title, - )?; - - Ok(Window { inner: window }) - } -} - -#[derive(Debug)] -pub struct Extension -{ - window_builder: Builder, - window_size: Dimens<u32>, - window_title: String, -} - -impl Extension -{ - #[must_use] - pub fn new(window_builder: Builder) -> Self - { - Self { window_builder, ..Default::default() } - } - - #[must_use] - pub fn window_size(mut self, window_size: Dimens<u32>) -> Self - { - self.window_size = window_size; - - self - } - - #[must_use] - pub fn window_title(mut self, window_title: impl Into<String>) -> Self - { - self.window_title = window_title.into(); - - self - } -} - -impl ecs::extension::Extension for Extension -{ - fn collect(self, mut collector: ExtensionCollector<'_>) - { - collector.add_system(StartEvent, initialize); - collector.add_system(ConcludeEvent, update); - - let window = self - .window_builder - .create(self.window_size, &self.window_title) - .unwrap(); - - window.set_cursor_mode(CursorMode::Normal).unwrap(); - - collector.add_sole(window).ok(); - } -} - -impl Default for Extension -{ - fn default() -> Self - { - Self { - window_builder: Builder::default(), - window_size: Dimens { width: 1920, height: 1080 }, - window_title: String::new(), - } - } -} - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct Error(glfw::Error); - -impl From<glfw::Error> for Error -{ - fn from(err: glfw::Error) -> Self - { - Self(err) - } -} - -fn initialize(window: Single<Window>, actions: Actions) -{ - let actions_weak_ref = actions.to_weak_ref(); - - window.set_close_callback(move || { - let actions_weak_ref = actions_weak_ref.clone(); - - let actions_ref = actions_weak_ref.access().expect("No world"); - - actions_ref.to_actions().stop(); - }); -} - -fn update(window: Single<Window>) -{ - window - .swap_buffers() - .expect("Failed to swap window buffers"); - - window.poll_events().expect("Failed to poll window events"); -} diff --git a/engine/src/windowing.rs b/engine/src/windowing.rs new file mode 100644 index 0000000..2493851 --- /dev/null +++ b/engine/src/windowing.rs @@ -0,0 +1,1169 @@ +use std::collections::VecDeque; +use std::hint::cold_path; +use std::panic::{catch_unwind, AssertUnwindSafe}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Condvar, Mutex, Weak}; +use std::thread::Builder as ThreadBuilder; +use std::time::Duration; + +use bitflags::{bitflags, bitflags_match, Flags}; +use crossbeam_queue::ArrayQueue; +use intmap::IntMap; +use raw_window_handle::{DisplayHandle, HandleError, HasDisplayHandle, WindowHandle}; +use winit::application::ApplicationHandler; +use winit::error::EventLoopError; +use winit::event::{DeviceEvent, DeviceId, StartCause, WindowEvent}; +use winit::event_loop::{ + ActiveEventLoop, + ControlFlow as EventLoopControlFlow, + EventLoop, + OwnedDisplayHandle, +}; +use winit::keyboard::PhysicalKey; +use winit::window::{Window as WinitWindow, WindowId as WinitWindowId}; + +use crate::ecs::actions::Actions; +use crate::ecs::component::Component; +use crate::ecs::entity::obtainer::Obtainer as EntityObtainer; +use crate::ecs::event::component::{Added, Changed, EventMatchExt, Removed}; +use crate::ecs::pair::{ChildOf, Pair}; +use crate::ecs::phase::{Phase, PRE_UPDATE as PRE_UPDATE_PHASE}; +use crate::ecs::sole::Single; +use crate::ecs::system::observer::Observe; +use crate::ecs::uid::Uid; +use crate::ecs::util::StreamingIterator; +use crate::ecs::{declare_entity, pair, Query, Sole}; +use crate::util::BitArray; +use crate::vector::Vec2; +use crate::windowing::dpi::{PhysicalPosition, PhysicalSize, Position}; +use crate::windowing::keyboard::{Key, KeyState, Keyboard, UnknownKeyCodeError}; +use crate::windowing::monitor::Handle as MonitorHandle; +use crate::windowing::mouse::{ + Button as MouseButton, + ButtonState as MouseButtonState, + Buttons as MouseButtons, + Mouse, + ScrollDelta as MouseScrollDelta, +}; +use crate::windowing::window::{ + Closed as WindowClosed, + CreationAttributes as WindowCreationAttributes, + CreationReady as WindowCreationReady, + CursorGrabMode, + Id as WindowId, + Window, +}; +use crate::{error, Error as EngineError}; + +pub mod dpi; +pub mod keyboard; +pub mod monitor; +pub mod mouse; +pub mod window; + +const MESSAGE_FROM_APP_QUEUE_SIZE: usize = 512; + +const MESSAGE_TO_APP_QUEUE_SIZE: usize = 16; // Increase if more messages are added + +const TEXT_KEY_QUEUE_SIZE: usize = 255; + +static CONTEXT_CREATED: AtomicBool = AtomicBool::new(false); + +declare_entity! { +pub PHASE: (Phase, pair!(ChildOf, { *PRE_UPDATE_PHASE })); +} + +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct Extension {} + +impl crate::ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: crate::ecs::extension::Collector<'_>) + { + if !CONTEXT_CREATED.load(Ordering::Relaxed) { + collector.add_sole(Context::new()).ok(); + } + + collector.add_sole(Keyboard::default()).ok(); + collector.add_sole(Mouse::default()).ok(); + collector.add_sole(MouseButtons::default()).ok(); + + collector.spawn_declared_entity(&PHASE); + + collector.add_system(*PHASE, update_stuff); + + collector.add_observer(handle_window_changed); + collector.add_observer(handle_window_removed); + collector.add_observer(handle_window_creation_ready); + } +} + +fn handle_window_creation_ready( + observe: Observe<Pair<Added, WindowCreationReady>>, + context: Single<Context>, +) +{ + let Ok(context) = context.get() else { + unreachable!(); + }; + + for evt_match in &observe { + let Some(ent) = evt_match.try_get_entity() else { + unreachable!(); + }; + + if ent.has_component(Window::id()) || ent.has_component(WindowClosed::id()) { + continue; + } + + let Some(window_creation_attrs) = ent.get::<WindowCreationAttributes>() else { + unreachable!(); + }; + + context.try_send_message_to_app(MessageToApp::CreateWindow( + ent.uid(), + window_creation_attrs.clone(), + )); + } +} + +#[tracing::instrument(skip_all)] +fn update_stuff( + mut context: Single<Context>, + mut keyboard: Single<Keyboard>, + mut mouse: Single<Mouse>, + mut mouse_buttons: Single<MouseButtons>, + mut actions: Actions, + entity_obtainer: EntityObtainer, +) -> Result<(), EngineError> +{ + let Ok(context) = context.get_mut() else { + unreachable!(); + }; + + let keyboard = keyboard.get_mut()?; + let mouse = mouse.get_mut()?; + let mouse_buttons = mouse_buttons.get_mut()?; + + if context.display_handle.is_none() { + cold_path(); + actions.stop(); + return Ok(()); + } + + keyboard.make_key_states_previous(); + + { + let Some(mut input) = context + .shared_state + .input + .try_lock_for(Duration::from_millis(100)) + else { + tracing::error!("Locking input mutex timed out after 100ms"); + return Ok(()); + }; + + mouse.curr_tick_position_delta = input.relative_mouse_pos_delta; + mouse.position = input.absolute_mouse_pos.clone(); + mouse.curr_tick_scroll_delta = input.mouse_scroll_delta.clone(); + + for (mouse_button, mouse_button_input) in input.mouse_buttons.iter_mut() { + mouse_buttons.set_previous_to_current(mouse_button); + + if mouse_button_input.flags.is_all() { + match mouse_buttons.get_previous(mouse_button) { + MouseButtonState::Pressed => { + mouse_buttons.set(mouse_button, MouseButtonState::Released); + + mouse_button_input.flags.remove(MouseButtonFlags::RELEASED); + } + MouseButtonState::Released => { + mouse_buttons.set(mouse_button, MouseButtonState::Pressed); + + mouse_button_input.flags.remove(MouseButtonFlags::PRESSED); + } + } + + continue; + } + + let mouse_button_state = if mouse_button_input.flags.is_empty() { + let Some(mouse_button_state) = mouse_button_input.lagged.pop_front() + else { + continue; + }; + + mouse_button_state + } else { + bitflags_match!(mouse_button_input.flags, { + MouseButtonFlags::PRESSED => MouseButtonState::Pressed, + MouseButtonFlags::RELEASED => MouseButtonState::Released, + _ => unreachable!() + }) + }; + + mouse_buttons.set(mouse_button, mouse_button_state); + + mouse_button_input.flags.clear(); + } + + let mut key_updates = input.keys.iter_occupied_mut(); + + while let Some(mut key_item) = key_updates.streaming_next() { + let key = Key::KEYS[key_item.index]; + + if key_item.bits == KEY_PRESSED_BITS | KEY_RELEASED_BITS { + match keyboard.get_key_state(key) { + KeyState::Pressed => { + keyboard.set_key_state(key, KeyState::Released); + + key_item.clear_and_set(KEY_PRESSED_BITS, u8::MAX); + } + KeyState::Released => { + keyboard.set_key_state(key, KeyState::Pressed); + + key_item.clear_and_set(KEY_RELEASED_BITS, u8::MAX); + } + } + + continue; + } + + let key_state = match key_item.bits { + KEY_PRESSED_BITS => KeyState::Pressed, + KEY_RELEASED_BITS => KeyState::Released, + _ => unreachable!(), + }; + + keyboard.set_key_state(key, key_state); + + key_item.clear_and_set(0, u8::MAX); + } + + input.relative_mouse_pos_delta = Vec2 { x: 0.0, y: 0.0 }; + input.mouse_scroll_delta = MouseScrollDelta { vert_lines: 0.0, hor_lines: 0.0 }; + }; + + keyboard.set_text_keys(iter_array_queue(&context.shared_state.text_keys)); + + let Context { + ref mut windows, ref shared_state, .. + } = *context; + + for message in iter_array_queue(&shared_state.message_from_app_queue) { + tracing::trace!(message=?message, "Received message from app"); + + match message { + MessageFromApp::WindowCreated( + window_ent_id, + winit_window, + window_creation_attrs, + ) => { + actions.add_components( + window_ent_id, + (Window::new(&winit_window, &window_creation_attrs),), + ); + + actions.remove_comps::<(WindowCreationReady,)>(window_ent_id); + + let window_id = WindowId::from_inner(winit_window.id()); + + windows.insert(window_id, (winit_window, window_ent_id)); + + tracing::info!( + window_id = ?window_id, + window_title = %window_creation_attrs.title, + "Window creation completed" + ); + } + MessageFromApp::WindowResized(window_id, new_window_size) => { + tracing::trace!( + window_id = ?window_id, + "Received window resized message" + ); + + let Some(window_ent_id) = + windows.get(window_id).map(|(_, ent_id)| ent_id) + else { + tracing::error!( + wid = ?window_id, + "Window does not exist in windowing context" + ); + continue; + }; + + let Some(window_ent) = entity_obtainer.get_entity(*window_ent_id) else { + continue; + }; + + let Some(mut window) = window_ent.get_mut::<Window>() else { + continue; + }; + + window.inner_size = new_window_size; + + window.set_changed(); + } + MessageFromApp::WindowCloseRequested(window_id) => { + let Some(window_ent_id) = + windows.get(window_id).map(|(_, ent_id)| ent_id) + else { + tracing::error!( + wid = ?window_id, + "Window does not exist in windowing context" + ); + continue; + }; + + actions.remove_comps::<(Window,)>(*window_ent_id); + } + MessageFromApp::WindowScaleFactorChanged(window_id, scale_factor) => { + let Some(window_ent_id) = + windows.get(window_id).map(|(_, ent_id)| ent_id) + else { + tracing::error!( + wid = ?window_id, + "Window does not exist in windowing context" + ); + continue; + }; + + let Some(window_ent) = entity_obtainer.get_entity(*window_ent_id) else { + continue; + }; + + let Some(mut window) = window_ent.get_mut::<Window>() else { + continue; + }; + + window.set_scale_factor(scale_factor); + + window.set_changed(); + } + } + } + + if shared_state.thread_panicked.load(Ordering::Relaxed) { + cold_path(); + return Err(error!("Windowing app thread panicked")); + } + + if shared_state.has_thread_err.load(Ordering::Relaxed) { + cold_path(); + + let Ok(mut err) = shared_state.thread_err.try_lock() else { + // The windowing app thread always unlocks the thread_err mutex before + // setting the has_thread_err atomic + unreachable!(); + }; + + let Some(err) = err.take() else { + // The has_thread_err atomic is set to true so this option is always + // Some + unreachable!(); + }; + + return Err(err.into()); + } + + Ok(()) +} + +fn handle_window_changed( + observe: Observe<'_, Pair<Changed, Window>>, + context: Single<Context>, +) +{ + let Ok(context) = context.get() else { + unreachable!(); + }; + + for evt_match in &observe { + let window_ent_id = evt_match.entity_id(); + + let mut window = evt_match.get_ent_target_comp_mut(); + + let Some((winit_window, _)) = context.windows.get(window.wid()) else { + tracing::error!( + wid = ?window.wid(), + entity_id = %window_ent_id, + "Window does not exist in windowing context", + ); + continue; + }; + + let window_apply_results = window.apply(winit_window); + + if let Err(actual_window_inner_size) = window_apply_results.inner_size_request { + window.inner_size = actual_window_inner_size; + } + + context.try_send_message_to_app(MessageToApp::SetWindowCursorGrabMode( + window.wid(), + window.cursor_grab_mode, + )); + } +} + +fn handle_window_removed( + observe: Observe<Pair<Removed, Window>>, + window_query: Query<(&Window,)>, + mut context: Single<Context>, + mut actions: Actions, +) +{ + let Ok(context) = context.get_mut() else { + unreachable!(); + }; + + for evt_match in &observe { + let window = evt_match.get_ent_target_comp(); + + context.windows.remove(window.wid()); + + actions.add_components(evt_match.entity_id(), (WindowClosed,)); + } + + if window_query.iter().count() == 1 { + actions.stop(); + } +} + +#[derive(Debug, Sole)] +pub struct Context +{ + shared_state: Arc<SharedState>, + display_handle: Option<OwnedDisplayHandle>, + windows: IntMap<WindowId, (Arc<WinitWindow>, Uid)>, + available_monitors: Vec<MonitorHandle>, + primary_monitor: Option<MonitorHandle>, +} + +impl Context +{ + pub fn display_handle(&self) -> Option<DisplayHandle<'_>> + { + self.display_handle.as_ref()?.display_handle().ok() + } + + /// Returns the specified window as a window handle, if it exists. + /// + /// # Safety + /// The Window handle must only be used with thread safe APIs. + pub unsafe fn get_window_as_handle( + &self, + window_id: &WindowId, + ) -> Option<Result<WindowHandle<'_>, HandleError>> + { + self.windows.get(*window_id).map(|(winit_window, _)| { + #[cfg(windows)] + { + use winit::platform::windows::WindowExtWindows; + + // SAFETY: I don't care + unsafe { winit_window.window_handle_any_thread() } + } + + #[cfg(not(windows))] + { + use raw_window_handle::HasWindowHandle; + + winit_window.window_handle() + } + }) + } + + /// Returns the last known primary monitor. This monitor may or may not exist any + /// longer and it may or may not be the primary monitor any longer. + pub fn get_primary_monitor(&self) -> Option<&MonitorHandle> + { + self.primary_monitor.as_ref() + } + + /// Returns an iterator of the last known available monitors. These monitors may or + /// may not exist any longer. + pub fn available_monitors(&self) -> impl Iterator<Item = &MonitorHandle> + { + self.available_monitors.iter() + } + + fn try_send_message_to_app(&self, message: MessageToApp) + { + if self + .shared_state + .message_to_app_queue + .push(message) + .is_err() + { + tracing::error!( + "Failed to send message. Queue for messages to windowing app is full" + ); + } + } +} + +impl Context +{ + /// Returns a new windowing context. + /// + /// # Panics + /// Will panic if a windowing context have already been created. + pub fn new() -> Self + { + if CONTEXT_CREATED.swap(true, Ordering::Relaxed) { + panic!("A windowing context have already been created"); + } + + let shared_state = Arc::new(SharedState::default()); + let shared_state_b = shared_state.clone(); + + if let Err(err) = ThreadBuilder::new() + .name("windowing app".to_string()) + .spawn(move || { + let shared_state_c = shared_state_b.clone(); + + match catch_unwind(move || { + let mut app = App { + shared_state: shared_state_b, + windows: IntMap::with_capacity(1), + }; + + let event_loop = match create_event_loop() { + Ok(event_loop) => event_loop, + Err(err) => { + return Err((app, AppThreadError::CreateEventLoop(err))); + } + }; + + event_loop.set_control_flow(EventLoopControlFlow::Poll); + + event_loop + .run_app(&mut app) + .map_err(|err| (app, AppThreadError::StartEventLoop(err)))?; + + Ok(()) + }) { + Ok(Ok(())) => {} + Ok(Err((app, err))) => { + { + let Ok(mut thread_err) = + app.shared_state.thread_err.try_lock() + else { + // The thread_err mutex is only locked by the main thread + // when the has_thread_err atomic is set to true, which + // it can not be set to yet + unreachable!(); + }; + + *thread_err = Some(err); + } + + app.shared_state + .has_thread_err + .store(true, Ordering::Relaxed); + + app.shared_state.init_data_cond.notify_one(); + } + Err(_) => { + shared_state_c + .thread_panicked + .store(true, Ordering::Relaxed); + + shared_state_c.init_data_cond.notify_one(); + } + }; + }) + { + tracing::error!( + "Failed to create windowing app thread: {:#}", + crate::Error::new(err) + ); + + return Self { + shared_state, + display_handle: None, + windows: IntMap::with_capacity(1), + available_monitors: Vec::with_capacity(2), + primary_monitor: None, + }; + } + + let mut init_data_lock = shared_state.init_data.lock().unwrap_or_else(|err| { + let mut lock = err.into_inner(); + *lock = None; + lock + }); + + while init_data_lock.is_none() + && !shared_state.has_thread_err.load(Ordering::Relaxed) + && !shared_state.thread_panicked.load(Ordering::Relaxed) + { + init_data_lock = shared_state + .init_data_cond + .wait(init_data_lock) + .unwrap_or_else(|err| { + let mut lock = err.into_inner(); + *lock = None; + lock + }); + } + + let init_data = init_data_lock.take(); + + drop(init_data_lock); + + let Some(init_data) = init_data else { + if shared_state.thread_panicked.load(Ordering::Relaxed) { + tracing::error!("Windowing app thread panicked"); + } else if shared_state.has_thread_err.load(Ordering::Relaxed) { + let Ok(mut err) = shared_state.thread_err.try_lock() else { + // The windowing app thread always unlocks the thread_err mutex before + // setting the has_thread_err atomic + unreachable!(); + }; + + let Some(err) = err.take() else { + // The has_thread_err atomic is set to true so this option is always + // Some + unreachable!(); + }; + + tracing::error!( + "Error in windowing app thread: {:#}", + crate::Error::new(err) + ); + } else { + tracing::error!("Windowing app initialization aborted unexpectedly"); + } + + return Self { + shared_state, + display_handle: None, + windows: IntMap::with_capacity(1), + available_monitors: Vec::with_capacity(2), + primary_monitor: None, + }; + }; + + Self { + shared_state, + display_handle: Some(init_data.display), + windows: IntMap::with_capacity(1), + available_monitors: init_data.available_monitors, + primary_monitor: init_data.primary_monitor, + } + } +} + +impl Drop for Context +{ + fn drop(&mut self) + { + self.shared_state.is_dropped.store(true, Ordering::Relaxed); + } +} + +fn create_event_loop() -> Result<EventLoop<()>, EventLoopError> +{ + let mut event_loop_builder = EventLoop::builder(); + + cfg_select! { + target_os = "linux" => { + winit::platform::x11::EventLoopBuilderExtX11::with_any_thread( + &mut event_loop_builder, + true, + ); + } + windows => { + winit::platform::windows::EventLoopBuilderExtWindows::with_any_thread( + &mut event_loop_builder, + true, + ); + } + _ => { + compile_error!("Unsupported platform") + } + } + + event_loop_builder.build() +} + +#[derive(Debug, thiserror::Error)] +enum AppThreadError +{ + #[error("Event loop creation failed")] + CreateEventLoop(#[source] winit::error::EventLoopError), + + #[error("Starting event loop failed")] + StartEventLoop(#[source] winit::error::EventLoopError), +} + +#[derive(Debug)] +enum MessageFromApp +{ + WindowCreated(Uid, Arc<WinitWindow>, WindowCreationAttributes), + WindowResized(WindowId, PhysicalSize<u32>), + WindowCloseRequested(WindowId), + WindowScaleFactorChanged(WindowId, f64), +} + +#[derive(Debug)] +enum MessageToApp +{ + CreateWindow(Uid, WindowCreationAttributes), + SetWindowCursorGrabMode(WindowId, CursorGrabMode), +} + +#[derive(Debug)] +struct SharedState +{ + message_from_app_queue: ArrayQueue<MessageFromApp>, + message_to_app_queue: ArrayQueue<MessageToApp>, + input: AssertUnwindSafe<parking_lot::Mutex<Input>>, + text_keys: ArrayQueue<char>, + is_dropped: AtomicBool, + thread_panicked: AtomicBool, + thread_err: Mutex<Option<AppThreadError>>, + has_thread_err: AtomicBool, + init_data: Mutex<Option<InitData>>, + init_data_cond: Condvar, +} + +impl Default for SharedState +{ + fn default() -> Self + { + Self { + message_from_app_queue: ArrayQueue::new(MESSAGE_FROM_APP_QUEUE_SIZE), + message_to_app_queue: ArrayQueue::new(MESSAGE_TO_APP_QUEUE_SIZE), + input: AssertUnwindSafe(parking_lot::Mutex::default()), + text_keys: ArrayQueue::new(TEXT_KEY_QUEUE_SIZE), + is_dropped: AtomicBool::new(false), + thread_panicked: AtomicBool::new(false), + thread_err: Mutex::new(None), + has_thread_err: AtomicBool::new(false), + init_data: Mutex::new(None), + init_data_cond: Condvar::new(), + } + } +} + +#[derive(Debug)] +struct Input +{ + relative_mouse_pos_delta: Vec2<f64>, + absolute_mouse_pos: PhysicalPosition<f64>, + mouse_scroll_delta: MouseScrollDelta, + mouse_buttons: IntMap<MouseButton, MouseButtonInput>, + keys: BitArray<{ (Key::KEYS.len() * BITS_PER_KEY).div_ceil(8) }, BITS_PER_KEY>, +} + +impl Default for Input +{ + fn default() -> Self + { + Self { + relative_mouse_pos_delta: Vec2::default(), + absolute_mouse_pos: PhysicalPosition::default(), + mouse_scroll_delta: MouseScrollDelta::default(), + mouse_buttons: IntMap::from_iter([ + (MouseButton::Left, MouseButtonInput::default()), + (MouseButton::Right, MouseButtonInput::default()), + (MouseButton::Middle, MouseButtonInput::default()), + (MouseButton::Back, MouseButtonInput::default()), + (MouseButton::Forward, MouseButtonInput::default()), + ]), + keys: BitArray::new(), + } + } +} + +#[derive(Debug, Default)] +struct MouseButtonInput +{ + flags: MouseButtonFlags, + lagged: VecDeque<MouseButtonState>, +} + +bitflags! { +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +struct MouseButtonFlags: u8 +{ + const PRESSED = 1 << 0; + const RELEASED = 1 << 1; +} +} + +const BITS_PER_KEY: usize = 2; + +const KEY_PRESSED_BITS: u8 = 0b10; +const KEY_RELEASED_BITS: u8 = 0b01; + +#[derive(Debug)] +struct InitData +{ + display: OwnedDisplayHandle, + available_monitors: Vec<MonitorHandle>, + primary_monitor: Option<MonitorHandle>, +} + +#[derive(Debug)] +struct App +{ + shared_state: Arc<SharedState>, + windows: IntMap<WindowId, (Weak<WinitWindow>, WindowSettings)>, +} + +impl App +{ + #[tracing::instrument(skip_all)] + fn handle_received_messages(&mut self, event_loop: &ActiveEventLoop) + { + for message in iter_array_queue(&self.shared_state.message_to_app_queue) { + match message { + MessageToApp::CreateWindow(window_ent_id, window_creation_attrs) => { + tracing::info!( + "Creating window with title {}", + window_creation_attrs.title + ); + + let winit_window = Arc::new( + match event_loop.create_window( + window_creation_attrs.clone().into_window_attrs(), + ) { + Ok(window) => window, + Err(err) => { + tracing::error!( + "Failed to create window: {:#}", + crate::Error::new(err) + ); + continue; + } + }, + ); + + window_creation_attrs.apply_extra_attrs_to_window(&winit_window); + + self.windows.insert( + WindowId::from_inner(winit_window.id()), + ( + Arc::downgrade(&winit_window), + WindowSettings { + cursor_grab_mode: window_creation_attrs.cursor_grab_mode, + }, + ), + ); + + self.send_message(MessageFromApp::WindowCreated( + window_ent_id, + winit_window, + window_creation_attrs, + )); + } + MessageToApp::SetWindowCursorGrabMode(window_id, cursor_grab_mode) => { + let Some((_, window_settings)) = self.windows.get_mut(window_id) + else { + tracing::warn!( + window_id=?window_id, + "Cannot set window cursor grab mode. Window not found" + ); + + continue; + }; + + window_settings.cursor_grab_mode = cursor_grab_mode; + } + } + } + } + + #[tracing::instrument(skip_all)] + fn send_message(&self, message: MessageFromApp) + { + if self.shared_state.message_from_app_queue.is_full() { + tracing::warn!( + "Queue for messages to windowing app is full! Dropping oldest message" + ); + } + + self.shared_state.message_from_app_queue.force_push(message); + } + + fn lock_input(&self) -> Option<parking_lot::MutexGuard<'_, Input>> + { + self.shared_state + .input + .try_lock_for(Duration::from_millis(100)) + } +} + +impl ApplicationHandler for App +{ + fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) + { + match cause { + StartCause::Init => { + let available_monitors = event_loop + .available_monitors() + .map(MonitorHandle::from_winit_monitor_handle) + .collect::<Vec<_>>(); + + let Ok(mut init_data) = self.shared_state.init_data.lock() else { + tracing::error!("Init data mutex is poisoned, exiting event loop"); + event_loop.exit(); + return; + }; + + *init_data = Some(InitData { + display: event_loop.owned_display_handle(), + available_monitors: available_monitors, + primary_monitor: event_loop + .primary_monitor() + .map(|monitor| MonitorHandle::from_winit_monitor_handle(monitor)), + }); + + self.shared_state.init_data_cond.notify_one(); + } + StartCause::Poll => { + if self.shared_state.is_dropped.load(Ordering::Relaxed) { + event_loop.exit(); + return; + } + + self.handle_received_messages(event_loop); + } + _ => {} + } + } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) + { + for (window, _) in self.windows.values() { + let Some(window) = window.upgrade() else { + continue; + }; + + window.request_redraw(); + } + } + + fn resumed(&mut self, _event_loop: &ActiveEventLoop) {} + + #[tracing::instrument(skip_all)] + fn window_event( + &mut self, + _event_loop: &ActiveEventLoop, + window_id: WinitWindowId, + event: WindowEvent, + ) + { + match event { + WindowEvent::Resized(new_window_size) => { + self.send_message(MessageFromApp::WindowResized( + WindowId::from_inner(window_id), + new_window_size.into(), + )); + } + WindowEvent::CloseRequested => { + self.send_message(MessageFromApp::WindowCloseRequested( + WindowId::from_inner(window_id), + )); + } + WindowEvent::KeyboardInput { + device_id: _, + event: keyboard_event, + is_synthetic: _, + } => { + if let Some(key_text) = keyboard_event + .text + .filter(|_| keyboard_event.state.is_pressed()) + { + for character in key_text.chars() { + if self.shared_state.text_keys.is_full() { + cold_path(); + tracing::warn!( + "Text key queue is full. Dropping oldest character" + ); + } + + self.shared_state.text_keys.force_push(character); + } + } + + if keyboard_event.repeat { + return; + } + + let key_code = match keyboard_event.physical_key { + PhysicalKey::Code(key_code) => key_code, + PhysicalKey::Unidentified(native_key) => { + tracing::warn!("Ignoring unidentified key: {native_key:?}"); + return; + } + }; + + let key: Key = match key_code.try_into() { + Ok(key) => key, + Err(UnknownKeyCodeError) => { + tracing::warn!("Ignoring key with unknown key code {key_code:?}"); + return; + } + }; + + let Some(mut input) = self.lock_input() else { + tracing::error!("Locking input mutex timed out after 100ms"); + return; + }; + + let key_state_bits = match keyboard_event.state.into() { + KeyState::Pressed => KEY_PRESSED_BITS, + KeyState::Released => KEY_RELEASED_BITS, + }; + + input.keys.clear_and_set(key as usize, key_state_bits, 0); + } + WindowEvent::CursorMoved { device_id: _, position } => { + { + let Some(mut input) = self.lock_input() else { + tracing::error!("Locking input mutex timed out after 100ms"); + return; + }; + + input.absolute_mouse_pos = position.into(); + } + + let Some((window, window_settings)) = + self.windows.get(WindowId::from_inner(window_id)) + else { + cold_path(); + return; + }; + + if window_settings.cursor_grab_mode != CursorGrabMode::Locked { + return; + } + + let Some(window) = window.upgrade() else { + cold_path(); + return; + }; + + if !window.has_focus() { + return; + } + + let window_size = window.inner_size(); + + if let Err(err) = + window.set_cursor_position(Position::Physical(PhysicalPosition { + x: window_size.width as i32 / 2, + y: window_size.height as i32 / 2, + })) + { + cold_path(); + tracing::error!( + window_id=?window_id, + "Failed to lock cursor position: {:#}", + crate::Error::new(err) + ); + }; + } + WindowEvent::MouseWheel { device_id: _, delta, phase: _ } => { + let (hor_lines, vert_lines) = match delta { + winit::event::MouseScrollDelta::LineDelta(hor_lines, vert_lines) => { + (hor_lines, vert_lines) + } + winit::event::MouseScrollDelta::PixelDelta(pos_delta) => { + let hor_lines = match pos_delta.x.partial_cmp(&0.0) { + Some(std::cmp::Ordering::Greater) => 1.0, + Some(std::cmp::Ordering::Less) => -1.0, + _ => 0.0, + }; + + let vert_lines = match pos_delta.y.partial_cmp(&0.0) { + Some(std::cmp::Ordering::Greater) => 1.0, + Some(std::cmp::Ordering::Less) => -1.0, + _ => 0.0, + }; + + (hor_lines, vert_lines) + } + }; + + let Some(mut input) = self.lock_input() else { + tracing::error!("Locking input mutex timed out after 100ms"); + return; + }; + + input.mouse_scroll_delta.hor_lines += hor_lines; + input.mouse_scroll_delta.vert_lines += vert_lines; + } + WindowEvent::MouseInput { device_id: _, state, button } => { + let Some(mut input) = self.lock_input() else { + tracing::error!("Locking input mutex timed out after 100ms"); + return; + }; + + let button = MouseButton::from(button); + let button_state = MouseButtonState::from(state); + + let button_input = input + .mouse_buttons + .entry(button) + .or_insert_with(|| MouseButtonInput::default()); + + if button_input.flags.is_all() || !button_input.lagged.is_empty() { + button_input.lagged.push_back(button_state); + return; + } + + button_input.flags.insert(match button_state { + MouseButtonState::Pressed => MouseButtonFlags::PRESSED, + MouseButtonState::Released => MouseButtonFlags::RELEASED, + }); + } + WindowEvent::ScaleFactorChanged { scale_factor, inner_size_writer: _ } => { + self.send_message(MessageFromApp::WindowScaleFactorChanged( + WindowId::from_inner(window_id), + scale_factor, + )); + } + _ => {} + } + } + + #[tracing::instrument(skip_all)] + fn device_event( + &mut self, + _event_loop: &ActiveEventLoop, + _device_id: DeviceId, + device_event: DeviceEvent, + ) + { + match device_event { + DeviceEvent::MouseMotion { delta } => { + let Some(mut input) = self.lock_input() else { + tracing::error!("Locking input mutex timed out after 100ms"); + return; + }; + + input.relative_mouse_pos_delta += Vec2::from(delta); + } + _ => {} + } + } +} + +#[derive(Debug, Default)] +struct WindowSettings +{ + cursor_grab_mode: CursorGrabMode, +} + +fn iter_array_queue<Item>( + queue: &ArrayQueue<Item>, +) -> impl Iterator<Item = Item> + use<'_, Item> +{ + (0..queue.len()).into_iter().filter_map(|_| queue.pop()) +} diff --git a/engine/src/windowing/dpi.rs b/engine/src/windowing/dpi.rs new file mode 100644 index 0000000..e3b1be1 --- /dev/null +++ b/engine/src/windowing/dpi.rs @@ -0,0 +1,217 @@ +use crate::reflection::Reflection; + +macro_rules! gen_struct_from_to_impls { + ($struct: ident, fields=($($field: ident),*)) => { + impl<Pixel> From<winit::dpi::$struct<Pixel>> for $struct<Pixel> + { + fn from(source: winit::dpi::$struct<Pixel>) -> Self + { + Self { + $($field: source.$field),* + } + } + } + + impl<Pixel> From<$struct<Pixel>> for winit::dpi::$struct<Pixel> + { + fn from(source: $struct<Pixel>) -> Self + { + Self { + $($field: source.$field),* + } + } + } + }; +} + +macro_rules! gen_enum_from_to_impls { + ($enum: ident, variants=($($variant: ident),*)) => { + impl From<winit::dpi::$enum> for $enum + { + fn from(source: winit::dpi::$enum) -> Self + { + match source { + $(winit::dpi::$enum::$variant(pos) => Self::$variant(pos.into())),* + } + } + } + + impl From<$enum> for winit::dpi::$enum + { + fn from(source: $enum) -> Self + { + match source { + $($enum::$variant(pos) => Self::$variant(pos.into())),* + } + } + } + }; +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Reflection)] +#[reflection(impl_with_generics(<u32>, <i32>, <f32>, <f64>))] +pub struct PhysicalPosition<Pixel> +{ + pub x: Pixel, + pub y: Pixel, +} + +impl<Pixel> PhysicalPosition<Pixel> +{ + pub fn to_logical<LogicalPixel>( + &self, + scale_factor: f64, + ) -> LogicalPosition<LogicalPixel> + where + Pixel: Into<f64> + Clone, + LogicalPixel: From<f64>, + { + let x = self.x.clone().into(); + let y = self.y.clone().into(); + + LogicalPosition { + x: (x / scale_factor).into(), + y: (y / scale_factor).into(), + } + } + + pub fn try_convert_from<SourcePixel>( + source: PhysicalPosition<SourcePixel>, + ) -> Result<Self, Pixel::Error> + where + Pixel: TryFrom<SourcePixel>, + { + Ok(Self { + x: source.x.try_into()?, + y: source.y.try_into()?, + }) + } +} + +gen_struct_from_to_impls!(PhysicalPosition, fields = (x, y)); + +#[derive(Debug, Default, Clone, Copy, PartialEq, Reflection)] +#[reflection(impl_with_generics(<f64>))] +pub struct LogicalPosition<Pixel> +{ + pub x: Pixel, + pub y: Pixel, +} + +impl<Pixel> LogicalPosition<Pixel> +{ + pub fn try_convert_from<SourcePixel>( + source: LogicalPosition<SourcePixel>, + ) -> Result<Self, Pixel::Error> + where + Pixel: TryFrom<SourcePixel>, + { + Ok(Self { + x: source.x.try_into()?, + y: source.y.try_into()?, + }) + } +} + +gen_struct_from_to_impls!(LogicalPosition, fields = (x, y)); + +#[derive(Debug, Clone, Copy, PartialEq, Reflection)] +pub enum Position +{ + Physical(PhysicalPosition<i32>), + Logical(LogicalPosition<f64>), +} + +impl Default for Position +{ + fn default() -> Self + { + Self::Logical(LogicalPosition::default()) + } +} + +gen_enum_from_to_impls!(Position, variants = (Physical, Logical)); + +#[derive(Debug, Default, Clone, Copy, PartialEq, Reflection)] +#[reflection(impl_with_generics(<u32>))] +pub struct PhysicalSize<Pixel> +{ + pub width: Pixel, + pub height: Pixel, +} + +impl<Pixel> PhysicalSize<Pixel> +{ + pub fn to_logical<LogicalPixel>(&self, scale_factor: f64) -> LogicalSize<LogicalPixel> + where + Pixel: Into<f64> + Clone, + LogicalPixel: From<f64>, + { + let width = self.width.clone().into(); + let height = self.height.clone().into(); + + LogicalSize { + width: (width / scale_factor).into(), + height: (height / scale_factor).into(), + } + } +} + +impl<Pixel> PhysicalSize<Pixel> +{ + pub fn try_convert_from<SourcePixel>( + source: PhysicalSize<SourcePixel>, + ) -> Result<Self, Pixel::Error> + where + Pixel: TryFrom<SourcePixel>, + { + Ok(Self { + width: source.width.try_into()?, + height: source.height.try_into()?, + }) + } +} + +gen_struct_from_to_impls!(PhysicalSize, fields = (width, height)); + +#[derive(Debug, Default, Clone, Copy, PartialEq, Reflection)] +#[reflection(impl_with_generics(<f64>))] +pub struct LogicalSize<Pixel> +{ + pub width: Pixel, + pub height: Pixel, +} + +impl<Pixel> LogicalSize<Pixel> +{ + pub fn try_convert_from<SourcePixel>( + source: LogicalSize<SourcePixel>, + ) -> Result<Self, Pixel::Error> + where + Pixel: TryFrom<SourcePixel>, + { + Ok(Self { + width: source.width.try_into()?, + height: source.height.try_into()?, + }) + } +} + +gen_struct_from_to_impls!(LogicalSize, fields = (width, height)); + +#[derive(Debug, Clone, Copy, PartialEq, Reflection)] +pub enum Size +{ + Physical(PhysicalSize<u32>), + Logical(LogicalSize<f64>), +} + +impl Default for Size +{ + fn default() -> Self + { + Self::Logical(LogicalSize::default()) + } +} + +gen_enum_from_to_impls!(Size, variants = (Physical, Logical)); diff --git a/engine/src/windowing/keyboard.rs b/engine/src/windowing/keyboard.rs new file mode 100644 index 0000000..dffa95e --- /dev/null +++ b/engine/src/windowing/keyboard.rs @@ -0,0 +1,826 @@ +use util_macros::VariantArr; + +use crate::ecs::Sole; +use crate::util::BitArray; + +#[derive(Debug, Default, Sole)] +pub struct Keyboard +{ + keys: BitArray<{ (Key::KEYS.len() * BITS_PER_KEY).div_ceil(8) }, BITS_PER_KEY>, + text_keys: String, +} + +impl Keyboard +{ + /// Returns whether the given key was just pressed this frame. This function will + /// return `false` if the key was also pressed the previous frame. + pub fn just_pressed(&self, key: Key) -> bool + { + self.get_key_state(key) == KeyState::Pressed + && self.get_prev_key_state(key) == KeyState::Released + } + + /// Returns whether the given key was just released this frame. This function will + /// return `false` if the key was also released the previous frame. + pub fn just_released(&self, key: Key) -> bool + { + self.get_key_state(key) == KeyState::Released + && self.get_prev_key_state(key) == KeyState::Pressed + } + + /// Returns whether the given key is currently pressed. + pub fn pressed(&self, key: Key) -> bool + { + self.get_key_state(key) == KeyState::Pressed + } + + /// Returns whether the given key is currently released. + pub fn released(&self, key: Key) -> bool + { + self.get_key_state(key) == KeyState::Released + } + + /// Returns a iterator of key & key state pairs where each of the keys have a + /// different state than they had the previous frame. + pub fn new_key_states(&self) -> impl Iterator<Item = (Key, KeyState)> + use<'_> + { + self.keys.iter_occupied().filter_map(|(key_index, bits)| { + if bits == KEY_CURR_PRESSED_BITS | KEY_PREV_PRESSED_BITS { + return None; + } + + let key = Key::KEYS[key_index]; + + let curr_state = match bits & KEY_CURR_PRESSED_BITS { + KEY_CURR_PRESSED_BITS => KeyState::Pressed, + 0 => KeyState::Released, + _ => unreachable!(), + }; + + Some((key, curr_state)) + }) + } + + #[must_use] + pub fn get_key_state(&self, key: Key) -> KeyState + { + let bits = self.keys.get(key as usize); + + let state = match bits & KEY_CURR_PRESSED_BITS { + KEY_CURR_PRESSED_BITS => KeyState::Pressed, + 0 => KeyState::Released, + _ => unreachable!(), + }; + + state + } + + #[must_use] + pub fn get_prev_key_state(&self, key: Key) -> KeyState + { + let bits = self.keys.get(key as usize); + + let state = match bits & KEY_PREV_PRESSED_BITS { + KEY_PREV_PRESSED_BITS => KeyState::Pressed, + 0 => KeyState::Released, + _ => unreachable!(), + }; + + state + } + + pub fn text_keys(&self) -> &str + { + &self.text_keys + } + + pub fn set_key_state(&mut self, key: Key, key_state: KeyState) + { + self.keys.clear_and_set( + key as usize, + match key_state { + KeyState::Pressed => KEY_CURR_PRESSED_BITS, + KeyState::Released => 0, + }, + KEY_CURR_PRESSED_BITS, + ); + } + + pub fn make_key_states_previous(&mut self) + { + for byte in self.keys.bytes_mut() { + *byte = (*byte >> 1) & 0b01010101 | *byte & 0b10101010; + } + } + + pub fn set_text_keys(&mut self, text_keys: impl IntoIterator<Item = char>) + { + self.text_keys.clear(); + self.text_keys.extend(text_keys); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, VariantArr)] +#[variant_arr(name = KEYS)] +#[non_exhaustive] +pub enum Key +{ + /// <kbd>`</kbd> on a US keyboard. This is also called a backtick or grave. + /// This is the <kbd>半角</kbd>/<kbd>全角</kbd>/<kbd>漢字</kbd> + /// (hankaku/zenkaku/kanji) key on Japanese keyboards + Backquote, + /// Used for both the US <kbd>\\</kbd> (on the 101-key layout) and also for the key + /// located between the <kbd>"</kbd> and <kbd>Enter</kbd> keys on row C of the 102-, + /// 104- and 106-key layouts. + /// Labeled <kbd>#</kbd> on a UK (102) keyboard. + Backslash, + /// <kbd>[</kbd> on a US keyboard. + BracketLeft, + /// <kbd>]</kbd> on a US keyboard. + BracketRight, + /// <kbd>,</kbd> on a US keyboard. + Comma, + /// <kbd>0</kbd> on a US keyboard. + Digit0, + /// <kbd>1</kbd> on a US keyboard. + Digit1, + /// <kbd>2</kbd> on a US keyboard. + Digit2, + /// <kbd>3</kbd> on a US keyboard. + Digit3, + /// <kbd>4</kbd> on a US keyboard. + Digit4, + /// <kbd>5</kbd> on a US keyboard. + Digit5, + /// <kbd>6</kbd> on a US keyboard. + Digit6, + /// <kbd>7</kbd> on a US keyboard. + Digit7, + /// <kbd>8</kbd> on a US keyboard. + Digit8, + /// <kbd>9</kbd> on a US keyboard. + Digit9, + /// <kbd>=</kbd> on a US keyboard. + Equal, + /// Located between the left <kbd>Shift</kbd> and <kbd>Z</kbd> keys. + /// Labeled <kbd>\\</kbd> on a UK keyboard. + IntlBackslash, + /// Located between the <kbd>/</kbd> and right <kbd>Shift</kbd> keys. + /// Labeled <kbd>\\</kbd> (ro) on a Japanese keyboard. + IntlRo, + /// Located between the <kbd>=</kbd> and <kbd>Backspace</kbd> keys. + /// Labeled <kbd>¥</kbd> (yen) on a Japanese keyboard. <kbd>\\</kbd> on a + /// Russian keyboard. + IntlYen, + /// <kbd>a</kbd> on a US keyboard. + /// Labeled <kbd>q</kbd> on an AZERTY (e.g., French) keyboard. + A, + /// <kbd>b</kbd> on a US keyboard. + B, + /// <kbd>c</kbd> on a US keyboard. + C, + /// <kbd>d</kbd> on a US keyboard. + D, + /// <kbd>e</kbd> on a US keyboard. + E, + /// <kbd>f</kbd> on a US keyboard. + F, + /// <kbd>g</kbd> on a US keyboard. + G, + /// <kbd>h</kbd> on a US keyboard. + H, + /// <kbd>i</kbd> on a US keyboard. + I, + /// <kbd>j</kbd> on a US keyboard. + J, + /// <kbd>k</kbd> on a US keyboard. + K, + /// <kbd>l</kbd> on a US keyboard. + L, + /// <kbd>m</kbd> on a US keyboard. + M, + /// <kbd>n</kbd> on a US keyboard. + N, + /// <kbd>o</kbd> on a US keyboard. + O, + /// <kbd>p</kbd> on a US keyboard. + P, + /// <kbd>q</kbd> on a US keyboard. + /// Labeled <kbd>a</kbd> on an AZERTY (e.g., French) keyboard. + Q, + /// <kbd>r</kbd> on a US keyboard. + R, + /// <kbd>s</kbd> on a US keyboard. + S, + /// <kbd>t</kbd> on a US keyboard. + T, + /// <kbd>u</kbd> on a US keyboard. + U, + /// <kbd>v</kbd> on a US keyboard. + V, + /// <kbd>w</kbd> on a US keyboard. + /// Labeled <kbd>z</kbd> on an AZERTY (e.g., French) keyboard. + W, + /// <kbd>x</kbd> on a US keyboard. + X, + /// <kbd>y</kbd> on a US keyboard. + /// Labeled <kbd>z</kbd> on a QWERTZ (e.g., German) keyboard. + Y, + /// <kbd>z</kbd> on a US keyboard. + /// Labeled <kbd>w</kbd> on an AZERTY (e.g., French) keyboard, and <kbd>y</kbd> on a + /// QWERTZ (e.g., German) keyboard. + Z, + /// <kbd>-</kbd> on a US keyboard. + Minus, + /// <kbd>.</kbd> on a US keyboard. + Period, + /// <kbd>'</kbd> on a US keyboard. + Quote, + /// <kbd>;</kbd> on a US keyboard. + Semicolon, + /// <kbd>/</kbd> on a US keyboard. + Slash, + /// <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>. + AltLeft, + /// <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>. + /// This is labeled <kbd>AltGr</kbd> on many keyboard layouts. + AltRight, + /// <kbd>Backspace</kbd> or <kbd>⌫</kbd>. + /// Labeled <kbd>Delete</kbd> on Apple keyboards. + Backspace, + /// <kbd>CapsLock</kbd> or <kbd>⇪</kbd> + CapsLock, + /// The application context menu key, which is typically found between the right + /// <kbd>Super</kbd> key and the right <kbd>Control</kbd> key. + ContextMenu, + /// <kbd>Control</kbd> or <kbd>⌃</kbd> + ControlLeft, + /// <kbd>Control</kbd> or <kbd>⌃</kbd> + ControlRight, + /// <kbd>Enter</kbd> or <kbd>↵</kbd>. Labeled <kbd>Return</kbd> on Apple keyboards. + Enter, + /// The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key. + SuperLeft, + /// The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key. + SuperRight, + /// <kbd>Shift</kbd> or <kbd>⇧</kbd> + ShiftLeft, + /// <kbd>Shift</kbd> or <kbd>⇧</kbd> + ShiftRight, + /// <kbd> </kbd> (space) + Space, + /// <kbd>Tab</kbd> or <kbd>⇥</kbd> + Tab, + /// Japanese: <kbd>変</kbd> (henkan) + Convert, + /// Japanese: <kbd>カタカナ</kbd>/<kbd>ひらがな</kbd>/<kbd>ローマ字</kbd> + /// (katakana/hiragana/romaji) + KanaMode, + /// Korean: HangulMode <kbd>한/영</kbd> (han/yeong) + /// + /// Japanese (Mac keyboard): <kbd>か</kbd> (kana) + Lang1, + /// Korean: Hanja <kbd>한</kbd> (hanja) + /// + /// Japanese (Mac keyboard): <kbd>英</kbd> (eisu) + Lang2, + /// Japanese (word-processing keyboard): Katakana + Lang3, + /// Japanese (word-processing keyboard): Hiragana + Lang4, + /// Japanese (word-processing keyboard): Zenkaku/Hankaku + Lang5, + /// Japanese: <kbd>無変換</kbd> (muhenkan) + NonConvert, + /// <kbd>⌦</kbd>. The forward delete key. + /// Note that on Apple keyboards, the key labelled <kbd>Delete</kbd> on the main part + /// of the keyboard is encoded as [`Backspace`]. + /// + /// [`Backspace`]: Self::Backspace + Delete, + /// <kbd>Page Down</kbd>, <kbd>End</kbd>, or <kbd>↘</kbd> + End, + /// <kbd>Help</kbd>. Not present on standard PC keyboards. + Help, + /// <kbd>Home</kbd> or <kbd>↖</kbd> + Home, + /// <kbd>Insert</kbd> or <kbd>Ins</kbd>. Not present on Apple keyboards. + Insert, + /// <kbd>Page Down</kbd>, <kbd>PgDn</kbd>, or <kbd>⇟</kbd> + PageDown, + /// <kbd>Page Up</kbd>, <kbd>PgUp</kbd>, or <kbd>⇞</kbd> + PageUp, + /// <kbd>↓</kbd> + ArrowDown, + /// <kbd>←</kbd> + ArrowLeft, + /// <kbd>→</kbd> + ArrowRight, + /// <kbd>↑</kbd> + ArrowUp, + /// On the Mac, this is used for the numpad <kbd>Clear</kbd> key. + NumLock, + /// <kbd>0 Ins</kbd> on a keyboard. <kbd>0</kbd> on a phone or remote control + Numpad0, + /// <kbd>1 End</kbd> on a keyboard. <kbd>1</kbd> or <kbd>1 QZ</kbd> on a phone or + /// remote control + Numpad1, + /// <kbd>2 ↓</kbd> on a keyboard. <kbd>2 ABC</kbd> on a phone or remote control + Numpad2, + /// <kbd>3 PgDn</kbd> on a keyboard. <kbd>3 DEF</kbd> on a phone or remote control + Numpad3, + /// <kbd>4 ←</kbd> on a keyboard. <kbd>4 GHI</kbd> on a phone or remote control + Numpad4, + /// <kbd>5</kbd> on a keyboard. <kbd>5 JKL</kbd> on a phone or remote control + Numpad5, + /// <kbd>6 →</kbd> on a keyboard. <kbd>6 MNO</kbd> on a phone or remote control + Numpad6, + /// <kbd>7 Home</kbd> on a keyboard. <kbd>7 PQRS</kbd> or <kbd>7 PRS</kbd> on a phone + /// or remote control + Numpad7, + /// <kbd>8 ↑</kbd> on a keyboard. <kbd>8 TUV</kbd> on a phone or remote control + Numpad8, + /// <kbd>9 PgUp</kbd> on a keyboard. <kbd>9 WXYZ</kbd> or <kbd>9 WXY</kbd> on a phone + /// or remote control + Numpad9, + /// <kbd>+</kbd> + NumpadAdd, + /// Found on the Microsoft Natural Keyboard. + NumpadBackspace, + /// <kbd>C</kbd> or <kbd>A</kbd> (All Clear). Also for use with numpads that have a + /// <kbd>Clear</kbd> key that is separate from the <kbd>NumLock</kbd> key. On the + /// Mac, the numpad <kbd>Clear</kbd> key is encoded as [`NumLock`]. + /// + /// [`NumLock`]: Self::NumLock + NumpadClear, + /// <kbd>C</kbd> (Clear Entry) + NumpadClearEntry, + /// <kbd>,</kbd> (thousands separator). For locales where the thousands separator + /// is a "." (e.g., Brazil), this key may generate a <kbd>.</kbd>. + NumpadComma, + /// <kbd>. Del</kbd>. For locales where the decimal separator is "," (e.g., + /// Brazil), this key may generate a <kbd>,</kbd>. + NumpadDecimal, + /// <kbd>/</kbd> + NumpadDivide, + NumpadEnter, + /// <kbd>=</kbd> + NumpadEqual, + /// <kbd>#</kbd> on a phone or remote control device. This key is typically found + /// below the <kbd>9</kbd> key and to the right of the <kbd>0</kbd> key. + NumpadHash, + /// <kbd>M</kbd> Add current entry to the value stored in memory. + NumpadMemoryAdd, + /// <kbd>M</kbd> Clear the value stored in memory. + NumpadMemoryClear, + /// <kbd>M</kbd> Replace the current entry with the value stored in memory. + NumpadMemoryRecall, + /// <kbd>M</kbd> Replace the value stored in memory with the current entry. + NumpadMemoryStore, + /// <kbd>M</kbd> Subtract current entry from the value stored in memory. + NumpadMemorySubtract, + /// <kbd>*</kbd> on a keyboard. For use with numpads that provide mathematical + /// operations (<kbd>+</kbd>, <kbd>-</kbd> <kbd>*</kbd> and <kbd>/</kbd>). + /// + /// Use `NumpadStar` for the <kbd>*</kbd> key on phones and remote controls. + NumpadMultiply, + /// <kbd>(</kbd> Found on the Microsoft Natural Keyboard. + NumpadParenLeft, + /// <kbd>)</kbd> Found on the Microsoft Natural Keyboard. + NumpadParenRight, + /// <kbd>*</kbd> on a phone or remote control device. + /// + /// This key is typically found below the <kbd>7</kbd> key and to the left of + /// the <kbd>0</kbd> key. + /// + /// Use <kbd>"NumpadMultiply"</kbd> for the <kbd>*</kbd> key on + /// numeric keypads. + NumpadStar, + /// <kbd>-</kbd> + NumpadSubtract, + /// <kbd>Esc</kbd> or <kbd>⎋</kbd> + Escape, + /// <kbd>Fn</kbd> This is typically a hardware key that does not generate a separate + /// code. + Fn, + /// <kbd>FLock</kbd> or <kbd>FnLock</kbd>. Function Lock key. Found on the Microsoft + /// Natural Keyboard. + FnLock, + /// <kbd>PrtScr SysRq</kbd> or <kbd>Print Screen</kbd> + PrintScreen, + /// <kbd>Scroll Lock</kbd> + ScrollLock, + /// <kbd>Pause Break</kbd> + Pause, + /// Some laptops place this key to the left of the <kbd>↑</kbd> key. + /// + /// This also the "back" button (triangle) on Android. + BrowserBack, + BrowserFavorites, + /// Some laptops place this key to the right of the <kbd>↑</kbd> key. + BrowserForward, + /// The "home" button on Android. + BrowserHome, + BrowserRefresh, + BrowserSearch, + BrowserStop, + /// <kbd>Eject</kbd> or <kbd>⏏</kbd>. This key is placed in the function section on + /// some Apple keyboards. + Eject, + /// Sometimes labelled <kbd>My Computer</kbd> on the keyboard + LaunchApp1, + /// Sometimes labelled <kbd>Calculator</kbd> on the keyboard + LaunchApp2, + LaunchMail, + MediaPlayPause, + MediaSelect, + MediaStop, + MediaTrackNext, + MediaTrackPrevious, + /// This key is placed in the function section on some Apple keyboards, replacing the + /// <kbd>Eject</kbd> key. + Power, + Sleep, + AudioVolumeDown, + AudioVolumeMute, + AudioVolumeUp, + WakeUp, + // Legacy modifier key. Also called "Super" in certain places. + Meta, + // Legacy modifier key. + Hyper, + Turbo, + Abort, + Resume, + Suspend, + /// Found on Sun’s USB keyboard. + Again, + /// Found on Sun’s USB keyboard. + Copy, + /// Found on Sun’s USB keyboard. + Cut, + /// Found on Sun’s USB keyboard. + Find, + /// Found on Sun’s USB keyboard. + Open, + /// Found on Sun’s USB keyboard. + Paste, + /// Found on Sun’s USB keyboard. + Props, + /// Found on Sun’s USB keyboard. + Select, + /// Found on Sun’s USB keyboard. + Undo, + /// Use for dedicated <kbd>ひらがな</kbd> key found on some Japanese word processing + /// keyboards. + Hiragana, + /// Use for dedicated <kbd>カタカナ</kbd> key found on some Japanese word processing + /// keyboards. + Katakana, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F1, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F2, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F3, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F4, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F5, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F6, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F7, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F8, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F9, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F10, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F11, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F12, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F13, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F14, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F15, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F16, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F17, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F18, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F19, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F20, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F21, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F22, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F23, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F24, + /// General-purpose function key. + F25, + /// General-purpose function key. + F26, + /// General-purpose function key. + F27, + /// General-purpose function key. + F28, + /// General-purpose function key. + F29, + /// General-purpose function key. + F30, + /// General-purpose function key. + F31, + /// General-purpose function key. + F32, + /// General-purpose function key. + F33, + /// General-purpose function key. + F34, + /// General-purpose function key. + F35, +} + +impl TryFrom<winit::keyboard::KeyCode> for Key +{ + type Error = UnknownKeyCodeError; + + fn try_from(key_code: winit::keyboard::KeyCode) -> Result<Self, Self::Error> + { + match key_code { + winit::keyboard::KeyCode::Backquote => Ok(Self::Backquote), + winit::keyboard::KeyCode::Backslash => Ok(Self::Backslash), + winit::keyboard::KeyCode::BracketLeft => Ok(Self::BracketLeft), + winit::keyboard::KeyCode::BracketRight => Ok(Self::BracketRight), + winit::keyboard::KeyCode::Comma => Ok(Self::Comma), + winit::keyboard::KeyCode::Digit0 => Ok(Self::Digit0), + winit::keyboard::KeyCode::Digit1 => Ok(Self::Digit1), + winit::keyboard::KeyCode::Digit2 => Ok(Self::Digit2), + winit::keyboard::KeyCode::Digit3 => Ok(Self::Digit3), + winit::keyboard::KeyCode::Digit4 => Ok(Self::Digit4), + winit::keyboard::KeyCode::Digit5 => Ok(Self::Digit5), + winit::keyboard::KeyCode::Digit6 => Ok(Self::Digit6), + winit::keyboard::KeyCode::Digit7 => Ok(Self::Digit7), + winit::keyboard::KeyCode::Digit8 => Ok(Self::Digit8), + winit::keyboard::KeyCode::Digit9 => Ok(Self::Digit9), + winit::keyboard::KeyCode::Equal => Ok(Self::Equal), + winit::keyboard::KeyCode::IntlBackslash => Ok(Self::IntlBackslash), + winit::keyboard::KeyCode::IntlRo => Ok(Self::IntlRo), + winit::keyboard::KeyCode::IntlYen => Ok(Self::IntlYen), + winit::keyboard::KeyCode::KeyA => Ok(Self::A), + winit::keyboard::KeyCode::KeyB => Ok(Self::B), + winit::keyboard::KeyCode::KeyC => Ok(Self::C), + winit::keyboard::KeyCode::KeyD => Ok(Self::D), + winit::keyboard::KeyCode::KeyE => Ok(Self::E), + winit::keyboard::KeyCode::KeyF => Ok(Self::F), + winit::keyboard::KeyCode::KeyG => Ok(Self::G), + winit::keyboard::KeyCode::KeyH => Ok(Self::H), + winit::keyboard::KeyCode::KeyI => Ok(Self::I), + winit::keyboard::KeyCode::KeyJ => Ok(Self::J), + winit::keyboard::KeyCode::KeyK => Ok(Self::K), + winit::keyboard::KeyCode::KeyL => Ok(Self::L), + winit::keyboard::KeyCode::KeyM => Ok(Self::M), + winit::keyboard::KeyCode::KeyN => Ok(Self::N), + winit::keyboard::KeyCode::KeyO => Ok(Self::O), + winit::keyboard::KeyCode::KeyP => Ok(Self::P), + winit::keyboard::KeyCode::KeyQ => Ok(Self::Q), + winit::keyboard::KeyCode::KeyR => Ok(Self::R), + winit::keyboard::KeyCode::KeyS => Ok(Self::S), + winit::keyboard::KeyCode::KeyT => Ok(Self::T), + winit::keyboard::KeyCode::KeyU => Ok(Self::U), + winit::keyboard::KeyCode::KeyV => Ok(Self::V), + winit::keyboard::KeyCode::KeyW => Ok(Self::W), + winit::keyboard::KeyCode::KeyX => Ok(Self::X), + winit::keyboard::KeyCode::KeyY => Ok(Self::Y), + winit::keyboard::KeyCode::KeyZ => Ok(Self::Z), + winit::keyboard::KeyCode::Minus => Ok(Self::Minus), + winit::keyboard::KeyCode::Period => Ok(Self::Period), + winit::keyboard::KeyCode::Quote => Ok(Self::Quote), + winit::keyboard::KeyCode::Semicolon => Ok(Self::Semicolon), + winit::keyboard::KeyCode::Slash => Ok(Self::Slash), + winit::keyboard::KeyCode::AltLeft => Ok(Self::AltLeft), + winit::keyboard::KeyCode::AltRight => Ok(Self::AltRight), + winit::keyboard::KeyCode::Backspace => Ok(Self::Backspace), + winit::keyboard::KeyCode::CapsLock => Ok(Self::CapsLock), + winit::keyboard::KeyCode::ContextMenu => Ok(Self::ContextMenu), + winit::keyboard::KeyCode::ControlLeft => Ok(Self::ControlLeft), + winit::keyboard::KeyCode::ControlRight => Ok(Self::ControlRight), + winit::keyboard::KeyCode::Enter => Ok(Self::Enter), + winit::keyboard::KeyCode::SuperLeft => Ok(Self::SuperLeft), + winit::keyboard::KeyCode::SuperRight => Ok(Self::SuperRight), + winit::keyboard::KeyCode::ShiftLeft => Ok(Self::ShiftLeft), + winit::keyboard::KeyCode::ShiftRight => Ok(Self::ShiftRight), + winit::keyboard::KeyCode::Space => Ok(Self::Space), + winit::keyboard::KeyCode::Tab => Ok(Self::Tab), + winit::keyboard::KeyCode::Convert => Ok(Self::Convert), + winit::keyboard::KeyCode::KanaMode => Ok(Self::KanaMode), + winit::keyboard::KeyCode::Lang1 => Ok(Self::Lang1), + winit::keyboard::KeyCode::Lang2 => Ok(Self::Lang2), + winit::keyboard::KeyCode::Lang3 => Ok(Self::Lang3), + winit::keyboard::KeyCode::Lang4 => Ok(Self::Lang4), + winit::keyboard::KeyCode::Lang5 => Ok(Self::Lang5), + winit::keyboard::KeyCode::NonConvert => Ok(Self::NonConvert), + winit::keyboard::KeyCode::Delete => Ok(Self::Delete), + winit::keyboard::KeyCode::End => Ok(Self::End), + winit::keyboard::KeyCode::Help => Ok(Self::Help), + winit::keyboard::KeyCode::Home => Ok(Self::Home), + winit::keyboard::KeyCode::Insert => Ok(Self::Insert), + winit::keyboard::KeyCode::PageDown => Ok(Self::PageDown), + winit::keyboard::KeyCode::PageUp => Ok(Self::PageUp), + winit::keyboard::KeyCode::ArrowDown => Ok(Self::ArrowDown), + winit::keyboard::KeyCode::ArrowLeft => Ok(Self::ArrowLeft), + winit::keyboard::KeyCode::ArrowRight => Ok(Self::ArrowRight), + winit::keyboard::KeyCode::ArrowUp => Ok(Self::ArrowUp), + winit::keyboard::KeyCode::NumLock => Ok(Self::NumLock), + winit::keyboard::KeyCode::Numpad0 => Ok(Self::Numpad0), + winit::keyboard::KeyCode::Numpad1 => Ok(Self::Numpad1), + winit::keyboard::KeyCode::Numpad2 => Ok(Self::Numpad2), + winit::keyboard::KeyCode::Numpad3 => Ok(Self::Numpad3), + winit::keyboard::KeyCode::Numpad4 => Ok(Self::Numpad4), + winit::keyboard::KeyCode::Numpad5 => Ok(Self::Numpad5), + winit::keyboard::KeyCode::Numpad6 => Ok(Self::Numpad6), + winit::keyboard::KeyCode::Numpad7 => Ok(Self::Numpad7), + winit::keyboard::KeyCode::Numpad8 => Ok(Self::Numpad8), + winit::keyboard::KeyCode::Numpad9 => Ok(Self::Numpad9), + winit::keyboard::KeyCode::NumpadAdd => Ok(Self::NumpadAdd), + winit::keyboard::KeyCode::NumpadBackspace => Ok(Self::NumpadBackspace), + winit::keyboard::KeyCode::NumpadClear => Ok(Self::NumpadClear), + winit::keyboard::KeyCode::NumpadClearEntry => Ok(Self::NumpadClearEntry), + winit::keyboard::KeyCode::NumpadComma => Ok(Self::NumpadComma), + winit::keyboard::KeyCode::NumpadDecimal => Ok(Self::NumpadDecimal), + winit::keyboard::KeyCode::NumpadDivide => Ok(Self::NumpadDivide), + winit::keyboard::KeyCode::NumpadEnter => Ok(Self::NumpadEnter), + winit::keyboard::KeyCode::NumpadEqual => Ok(Self::NumpadEqual), + winit::keyboard::KeyCode::NumpadHash => Ok(Self::NumpadHash), + winit::keyboard::KeyCode::NumpadMemoryAdd => Ok(Self::NumpadMemoryAdd), + winit::keyboard::KeyCode::NumpadMemoryClear => Ok(Self::NumpadMemoryClear), + winit::keyboard::KeyCode::NumpadMemoryRecall => Ok(Self::NumpadMemoryRecall), + winit::keyboard::KeyCode::NumpadMemoryStore => Ok(Self::NumpadMemoryStore), + winit::keyboard::KeyCode::NumpadMemorySubtract => { + Ok(Self::NumpadMemorySubtract) + } + winit::keyboard::KeyCode::NumpadMultiply => Ok(Self::NumpadMultiply), + winit::keyboard::KeyCode::NumpadParenLeft => Ok(Self::NumpadParenLeft), + winit::keyboard::KeyCode::NumpadParenRight => Ok(Self::NumpadParenRight), + winit::keyboard::KeyCode::NumpadStar => Ok(Self::NumpadStar), + winit::keyboard::KeyCode::NumpadSubtract => Ok(Self::NumpadSubtract), + winit::keyboard::KeyCode::Escape => Ok(Self::Escape), + winit::keyboard::KeyCode::Fn => Ok(Self::Fn), + winit::keyboard::KeyCode::FnLock => Ok(Self::FnLock), + winit::keyboard::KeyCode::PrintScreen => Ok(Self::PrintScreen), + winit::keyboard::KeyCode::ScrollLock => Ok(Self::ScrollLock), + winit::keyboard::KeyCode::Pause => Ok(Self::Pause), + winit::keyboard::KeyCode::BrowserBack => Ok(Self::BrowserBack), + winit::keyboard::KeyCode::BrowserFavorites => Ok(Self::BrowserFavorites), + winit::keyboard::KeyCode::BrowserForward => Ok(Self::BrowserForward), + winit::keyboard::KeyCode::BrowserHome => Ok(Self::BrowserHome), + winit::keyboard::KeyCode::BrowserRefresh => Ok(Self::BrowserRefresh), + winit::keyboard::KeyCode::BrowserSearch => Ok(Self::BrowserSearch), + winit::keyboard::KeyCode::BrowserStop => Ok(Self::BrowserStop), + winit::keyboard::KeyCode::Eject => Ok(Self::Eject), + winit::keyboard::KeyCode::LaunchApp1 => Ok(Self::LaunchApp1), + winit::keyboard::KeyCode::LaunchApp2 => Ok(Self::LaunchApp2), + winit::keyboard::KeyCode::LaunchMail => Ok(Self::LaunchMail), + winit::keyboard::KeyCode::MediaPlayPause => Ok(Self::MediaPlayPause), + winit::keyboard::KeyCode::MediaSelect => Ok(Self::MediaSelect), + winit::keyboard::KeyCode::MediaStop => Ok(Self::MediaStop), + winit::keyboard::KeyCode::MediaTrackNext => Ok(Self::MediaTrackNext), + winit::keyboard::KeyCode::MediaTrackPrevious => Ok(Self::MediaTrackPrevious), + winit::keyboard::KeyCode::Power => Ok(Self::Power), + winit::keyboard::KeyCode::Sleep => Ok(Self::Sleep), + winit::keyboard::KeyCode::AudioVolumeDown => Ok(Self::AudioVolumeDown), + winit::keyboard::KeyCode::AudioVolumeMute => Ok(Self::AudioVolumeMute), + winit::keyboard::KeyCode::AudioVolumeUp => Ok(Self::AudioVolumeUp), + winit::keyboard::KeyCode::WakeUp => Ok(Self::WakeUp), + winit::keyboard::KeyCode::Meta => Ok(Self::Meta), + winit::keyboard::KeyCode::Hyper => Ok(Self::Hyper), + winit::keyboard::KeyCode::Turbo => Ok(Self::Turbo), + winit::keyboard::KeyCode::Abort => Ok(Self::Abort), + winit::keyboard::KeyCode::Resume => Ok(Self::Resume), + winit::keyboard::KeyCode::Suspend => Ok(Self::Suspend), + winit::keyboard::KeyCode::Again => Ok(Self::Again), + winit::keyboard::KeyCode::Copy => Ok(Self::Copy), + winit::keyboard::KeyCode::Cut => Ok(Self::Cut), + winit::keyboard::KeyCode::Find => Ok(Self::Find), + winit::keyboard::KeyCode::Open => Ok(Self::Open), + winit::keyboard::KeyCode::Paste => Ok(Self::Paste), + winit::keyboard::KeyCode::Props => Ok(Self::Props), + winit::keyboard::KeyCode::Select => Ok(Self::Select), + winit::keyboard::KeyCode::Undo => Ok(Self::Undo), + winit::keyboard::KeyCode::Hiragana => Ok(Self::Hiragana), + winit::keyboard::KeyCode::Katakana => Ok(Self::Katakana), + winit::keyboard::KeyCode::F1 => Ok(Self::F1), + winit::keyboard::KeyCode::F2 => Ok(Self::F2), + winit::keyboard::KeyCode::F3 => Ok(Self::F3), + winit::keyboard::KeyCode::F4 => Ok(Self::F4), + winit::keyboard::KeyCode::F5 => Ok(Self::F5), + winit::keyboard::KeyCode::F6 => Ok(Self::F6), + winit::keyboard::KeyCode::F7 => Ok(Self::F7), + winit::keyboard::KeyCode::F8 => Ok(Self::F8), + winit::keyboard::KeyCode::F9 => Ok(Self::F9), + winit::keyboard::KeyCode::F10 => Ok(Self::F10), + winit::keyboard::KeyCode::F11 => Ok(Self::F11), + winit::keyboard::KeyCode::F12 => Ok(Self::F12), + winit::keyboard::KeyCode::F13 => Ok(Self::F13), + winit::keyboard::KeyCode::F14 => Ok(Self::F14), + winit::keyboard::KeyCode::F15 => Ok(Self::F15), + winit::keyboard::KeyCode::F16 => Ok(Self::F16), + winit::keyboard::KeyCode::F17 => Ok(Self::F17), + winit::keyboard::KeyCode::F18 => Ok(Self::F18), + winit::keyboard::KeyCode::F19 => Ok(Self::F19), + winit::keyboard::KeyCode::F20 => Ok(Self::F20), + winit::keyboard::KeyCode::F21 => Ok(Self::F21), + winit::keyboard::KeyCode::F22 => Ok(Self::F22), + winit::keyboard::KeyCode::F23 => Ok(Self::F23), + winit::keyboard::KeyCode::F24 => Ok(Self::F24), + winit::keyboard::KeyCode::F25 => Ok(Self::F25), + winit::keyboard::KeyCode::F26 => Ok(Self::F26), + winit::keyboard::KeyCode::F27 => Ok(Self::F27), + winit::keyboard::KeyCode::F28 => Ok(Self::F28), + winit::keyboard::KeyCode::F29 => Ok(Self::F29), + winit::keyboard::KeyCode::F30 => Ok(Self::F30), + winit::keyboard::KeyCode::F31 => Ok(Self::F31), + winit::keyboard::KeyCode::F32 => Ok(Self::F32), + winit::keyboard::KeyCode::F33 => Ok(Self::F33), + winit::keyboard::KeyCode::F34 => Ok(Self::F34), + winit::keyboard::KeyCode::F35 => Ok(Self::F35), + _ => Err(UnknownKeyCodeError), + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("Unknown key code")] +pub struct UnknownKeyCodeError; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum KeyState +{ + Pressed, + Released, +} + +impl KeyState +{ + #[must_use] + #[inline] + pub fn is_pressed(&self) -> bool + { + matches!(self, Self::Pressed) + } + + #[must_use] + #[inline] + pub fn is_released(&self) -> bool + { + matches!(self, Self::Released) + } +} + +impl From<winit::event::ElementState> for KeyState +{ + fn from(element_state: winit::event::ElementState) -> Self + { + match element_state { + winit::event::ElementState::Pressed => Self::Pressed, + winit::event::ElementState::Released => Self::Released, + } + } +} + +const BITS_PER_KEY: usize = 2; + +const KEY_CURR_PRESSED_BITS: u8 = 0b10; +const KEY_PREV_PRESSED_BITS: u8 = 0b01; diff --git a/engine/src/windowing/monitor.rs b/engine/src/windowing/monitor.rs new file mode 100644 index 0000000..894448a --- /dev/null +++ b/engine/src/windowing/monitor.rs @@ -0,0 +1,50 @@ +use crate::windowing::dpi::{PhysicalPosition, PhysicalSize}; + +/// Handle to a monitor that may or may not exist any longer. +#[derive(Debug, Clone)] +pub struct Handle +{ + inner: winit::monitor::MonitorHandle, +} + +impl Handle +{ + /// Returns a human-readable name of the monitor. + #[inline] + pub fn name(&self) -> Option<String> + { + self.inner.name() + } + + /// Returns the monitor's resolution. + #[inline] + pub fn size(&self) -> PhysicalSize<u32> + { + self.inner.size().into() + } + + /// Returns the top-left corner position of the monitor relative to the larger full + /// screen area. + #[inline] + pub fn position(&self) -> PhysicalPosition<i32> + { + self.inner.position().into() + } + + /// Returns the scale factor of the underlying monitor. + #[inline] + pub fn scale_factor(&self) -> f64 + { + self.inner.scale_factor() + } +} + +impl Handle +{ + pub(super) fn from_winit_monitor_handle( + monitor: winit::monitor::MonitorHandle, + ) -> Self + { + Self { inner: monitor } + } +} diff --git a/engine/src/windowing/mouse.rs b/engine/src/windowing/mouse.rs new file mode 100644 index 0000000..3a43e79 --- /dev/null +++ b/engine/src/windowing/mouse.rs @@ -0,0 +1,197 @@ +use std::collections::HashMap; + +use crate::ecs::Sole; +use crate::reflection::Reflection; +use crate::vector::Vec2; +use crate::windowing::dpi::PhysicalPosition; + +#[derive(Debug, Default, Clone, Sole, Reflection)] +#[non_exhaustive] +pub struct Mouse +{ + /// Change in coordinates this tick. Unit is unspecified and platforms may use + /// different units. Updated automatically by the [`windowing extension`]. + /// + /// [`windowing extension`]: crate::windowing + pub curr_tick_position_delta: Vec2<f64>, + + /// Coordinates in pixels relative to the top-left corner of the window. May have + /// been affected by cursor acceleration + pub position: PhysicalPosition<f64>, + + pub curr_tick_scroll_delta: ScrollDelta, +} + +#[derive(Debug, Clone, Default, Reflection)] +pub struct ScrollDelta +{ + pub vert_lines: f32, + pub hor_lines: f32, +} + +impl ScrollDelta +{ + pub fn is_zero(&self) -> bool + { + self.vert_lines == 0.0 && self.hor_lines == 0.0 + } +} + +/// Mouse buttons. +#[derive(Debug, Default, Sole)] +pub struct Buttons +{ + map: HashMap<Button, ButtonData>, +} + +impl Buttons +{ + pub fn get(&self, button: Button) -> ButtonState + { + let Some(button_data) = self.map.get(&button) else { + return ButtonState::Released; + }; + + button_data.current_state + } + + pub fn get_previous(&self, button: Button) -> ButtonState + { + let Some(button_data) = self.map.get(&button) else { + return ButtonState::Released; + }; + + button_data.previous_state + } + + /// Returns a iterator that yields buttons and their current states. Only buttons with + /// states is included. + pub fn all_current(&self) -> impl Iterator<Item = (Button, ButtonState)> + use<'_> + { + self.map + .iter() + .map(|(button, button_data)| (button.clone(), button_data.current_state)) + } + + pub fn set(&mut self, button: Button, button_state: ButtonState) + { + let button_data = self.map.entry(button).or_default(); + + button_data.current_state = button_state; + } + + pub fn set_previous(&mut self, button: Button, button_state: ButtonState) + { + let button_data = self.map.entry(button).or_default(); + + button_data.previous_state = button_state; + } + + pub fn set_previous_to_current(&mut self, button: Button) + { + let button_data = self.map.entry(button).or_default(); + + button_data.previous_state = button_data.current_state; + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Button +{ + Left, + Right, + Middle, + Back, + Forward, + Other(u16), +} + +impl intmap::IntKey for Button +{ + type Int = u32; + + const PRIME: Self::Int = <u32 as intmap::IntKey>::PRIME; + + fn into_int(self) -> Self::Int + { + match &self { + Self::Left => 0, + Self::Right => 1, + Self::Middle => 2, + Self::Back => 3, + Self::Forward => 4, + Self::Other(other) => 5 + *other as u32, + } + } +} + +impl From<winit::event::MouseButton> for Button +{ + fn from(mouse_button: winit::event::MouseButton) -> Self + { + match mouse_button { + winit::event::MouseButton::Left => Self::Left, + winit::event::MouseButton::Right => Self::Right, + winit::event::MouseButton::Middle => Self::Middle, + winit::event::MouseButton::Back => Self::Back, + winit::event::MouseButton::Forward => Self::Forward, + winit::event::MouseButton::Other(other_mouse_button) => { + Self::Other(other_mouse_button) + } + } + } +} + +/// Mouse button state. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ButtonState +{ + Pressed, + Released, +} + +impl ButtonState +{ + #[must_use] + #[inline] + pub fn is_pressed(&self) -> bool + { + matches!(self, Self::Pressed) + } + + #[must_use] + #[inline] + pub fn is_released(&self) -> bool + { + matches!(self, Self::Released) + } +} + +impl From<winit::event::ElementState> for ButtonState +{ + fn from(element_state: winit::event::ElementState) -> Self + { + match element_state { + winit::event::ElementState::Pressed => Self::Pressed, + winit::event::ElementState::Released => Self::Released, + } + } +} + +#[derive(Debug)] +struct ButtonData +{ + current_state: ButtonState, + previous_state: ButtonState, +} + +impl Default for ButtonData +{ + fn default() -> Self + { + Self { + current_state: ButtonState::Released, + previous_state: ButtonState::Released, + } + } +} diff --git a/engine/src/windowing/window.rs b/engine/src/windowing/window.rs new file mode 100644 index 0000000..6d7a464 --- /dev/null +++ b/engine/src/windowing/window.rs @@ -0,0 +1,291 @@ +use std::borrow::Cow; + +use crate::ecs::Component; +use crate::image::Image; +use crate::reflection::Reflection; +use crate::windowing::dpi::{PhysicalSize, Position, Size}; + +pub mod platform; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id +{ + inner: winit::window::WindowId, +} + +impl intmap::IntKey for Id +{ + type Int = u64; + + const PRIME: Self::Int = <u64 as intmap::IntKey>::PRIME; + + fn into_int(self) -> Self::Int + { + self.inner.into() + } +} + +impl Id +{ + pub(crate) fn from_inner(inner: winit::window::WindowId) -> Self + { + Self { inner } + } +} + +#[derive(Debug, Component, Clone, Reflection)] +#[non_exhaustive] +pub struct CreationAttributes +{ + pub title: Cow<'static, str>, + pub transparent: bool, + pub maximized: bool, + pub fullscreen: Option<Fullscreen>, + pub visible: bool, + pub resizable: bool, + pub position: Option<Position>, + pub inner_size: Option<Size>, + pub icon: Option<Icon>, + + pub x_visual_id: Option<XVisualID>, + + // These do not have equivalents in winit::window::WindowAttributes + pub cursor_visible: bool, + pub cursor_grab_mode: CursorGrabMode, +} + +macro_rules! gen_creation_attrs_with_fn { + ($field: ident, $field_type: ty) => { + paste::paste! { + impl CreationAttributes + { + pub fn [<with_ $field>](mut self, new: impl Into<$field_type>) -> Self + { + self.$field = new.into(); + self + } + } + } + }; +} + +gen_creation_attrs_with_fn!(title, Cow<'static, str>); +gen_creation_attrs_with_fn!(transparent, bool); +gen_creation_attrs_with_fn!(maximized, bool); +gen_creation_attrs_with_fn!(fullscreen, Option<Fullscreen>); +gen_creation_attrs_with_fn!(visible, bool); +gen_creation_attrs_with_fn!(resizable, bool); +gen_creation_attrs_with_fn!(position, Option<Position>); +gen_creation_attrs_with_fn!(inner_size, Option<Size>); +gen_creation_attrs_with_fn!(icon, Option<Icon>); + +gen_creation_attrs_with_fn!(x_visual_id, Option<XVisualID>); + +gen_creation_attrs_with_fn!(cursor_visible, bool); +gen_creation_attrs_with_fn!(cursor_grab_mode, CursorGrabMode); + +impl CreationAttributes +{ + #[tracing::instrument(skip_all)] + pub(crate) fn into_window_attrs(self) -> winit::window::WindowAttributes + { + let mut window_attrs = winit::window::WindowAttributes::default() + .with_title(self.title.into_owned()) + .with_transparent(self.transparent) + .with_maximized(self.maximized) + .with_fullscreen(match self.fullscreen { + Some(Fullscreen::Borderless) => { + Some(winit::window::Fullscreen::Borderless(None)) + } + None => None, + }) + .with_visible(self.visible) + .with_resizable(self.resizable) + .with_window_icon(match self.icon { + Some(Icon::Image(image)) => { + let image_dimens = image.dimensions(); + + let result = winit::window::Icon::from_rgba( + image.into_rgba8().into_bytes(), + image_dimens.width, + image_dimens.height, + ); + + if let Err(err) = &result { + tracing::error!("Invalid icon image: {err}"); + } + + result.ok() + } + #[cfg(windows)] + Some(Icon::WindowsResource(resource_name)) => { + use winit::platform::windows::IconExtWindows; + + let result = winit::window::Icon::from_resource_name( + resource_name.as_ref(), + None, + ); + + if let Err(err) = &result { + tracing::error!( + "Invalid icon image in resource {resource_name}: {err}" + ); + } + + result.ok() + } + None => None, + }); + + window_attrs.position = self.position.map(Into::into); + window_attrs.inner_size = self.inner_size.map(Into::into); + + #[cfg(target_os = "linux")] + if let Some(visual_id) = self.x_visual_id { + use winit::platform::x11::WindowAttributesExtX11; + + return window_attrs.with_x11_visual(visual_id); + } + + window_attrs + } + + pub(crate) fn apply_extra_attrs_to_window(&self, window: &winit::window::Window) + { + window.set_cursor_visible(self.cursor_visible); + } +} + +impl Default for CreationAttributes +{ + fn default() -> Self + { + Self { + title: "Unnamed window".into(), + transparent: false, + maximized: false, + fullscreen: None, + visible: true, + resizable: true, + position: None, + inner_size: None, + icon: None, + x_visual_id: None, + cursor_visible: true, + cursor_grab_mode: CursorGrabMode::None, + } + } +} + +#[derive(Debug, Clone, Reflection)] +#[non_exhaustive] +pub enum Fullscreen +{ + Borderless, +} + +/// Window icon +#[derive(Debug, Clone, Reflection)] +#[non_exhaustive] +pub enum Icon +{ + Image(Image), + + #[cfg(windows)] + WindowsResource(Cow<'static, str>), +} + +#[derive(Debug, Default, Component, Clone, Copy, Reflection)] +pub struct CreationReady; + +#[derive(Debug, Component, Reflection)] +#[non_exhaustive] +pub struct Window +{ + pub title: String, + pub cursor_visible: bool, + pub cursor_grab_mode: CursorGrabMode, + pub inner_size: PhysicalSize<u32>, + wid: Id, + scale_factor: f64, +} + +impl Window +{ + pub fn wid(&self) -> Id + { + self.wid + } + + pub fn scale_factor(&self) -> f64 + { + self.scale_factor + } + + pub(crate) fn new( + winit_window: &winit::window::Window, + creation_attrs: &CreationAttributes, + ) -> Self + { + Self { + title: creation_attrs.title.clone().into_owned(), + cursor_visible: creation_attrs.cursor_visible, + cursor_grab_mode: creation_attrs.cursor_grab_mode, + wid: Id::from_inner(winit_window.id()), + inner_size: winit_window.inner_size().into(), + scale_factor: winit_window.scale_factor(), + } + } + + #[must_use] + pub(crate) fn apply(&self, winit_window: &winit::window::Window) -> ApplyResults + { + winit_window.set_title(&self.title); + winit_window.set_cursor_visible(self.cursor_visible); + + let curr_inner_size = winit_window.inner_size().clone().into(); + + let inner_size_request_result = match winit_window.request_inner_size( + winit::dpi::Size::Physical(self.inner_size.clone().into()), + ) { + // The comparison of curr_inner_size is in case the user's windowing system + // lies about using the requested inner size + None if curr_inner_size == self.inner_size => Ok(()), + None => Err(curr_inner_size), + Some(inner_size) => Err(inner_size.into()), + }; + + ApplyResults { + inner_size_request: inner_size_request_result, + } + } + + pub(crate) fn set_scale_factor(&mut self, scale_factor: f64) + { + self.scale_factor = scale_factor; + } +} + +#[derive(Debug, Component, Reflection)] +pub struct Closed; + +#[derive( + Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Reflection, +)] +pub enum CursorGrabMode +{ + #[default] + None, + + /// The cursor is locked to a specific position in the window. + Locked, +} + +/// A unique identifier for an X11 visual. +pub type XVisualID = u32; + +#[derive(Debug)] +pub(crate) struct ApplyResults +{ + pub inner_size_request: Result<(), PhysicalSize<u32>>, +} diff --git a/engine/src/windowing/window/platform.rs b/engine/src/windowing/window/platform.rs new file mode 100644 index 0000000..1f38f3b --- /dev/null +++ b/engine/src/windowing/window/platform.rs @@ -0,0 +1,12 @@ +#[cfg(target_os = "linux")] +pub mod x11 +{ + use std::ffi::c_void; + + pub type XlibErrorHook = Box<dyn Fn(*mut c_void, *mut c_void) -> bool + Send + Sync>; + + pub fn register_xlib_error_hook(hook: XlibErrorHook) + { + winit::platform::x11::register_xlib_error_hook(hook); + } +} diff --git a/engine/src/work_queue.rs b/engine/src/work_queue.rs new file mode 100644 index 0000000..494d2b5 --- /dev/null +++ b/engine/src/work_queue.rs @@ -0,0 +1,77 @@ +use std::borrow::Cow; +use std::marker::PhantomData; +use std::panic::catch_unwind; +use std::sync::mpsc::{channel as mpsc_channel, Sender as MpscSender}; +use std::sync::{Arc, OnceLock}; +use std::thread::{Builder as ThreadBuilder, JoinHandle as ThreadJoinHandle}; + +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_panic: Arc<OnceLock<Box<str>>>, + _thread: ThreadJoinHandle<()>, + _pd: PhantomData<UserData>, +} + +impl<UserData: Send + Sync + 'static> WorkQueue<UserData> +{ + pub fn new(name: &str) -> Self + { + let (work_sender, work_receiver) = mpsc_channel::<Work<UserData>>(); + + let thread_panic = Arc::new(OnceLock::new()); + + let thread_panic_b = thread_panic.clone(); + + Self { + work_sender, + thread_panic: thread_panic, + _thread: ThreadBuilder::new() + .name(name.to_string()) + .spawn(move || { + if let Err(panic_err) = catch_unwind(|| { + while let Ok(work) = work_receiver.recv() { + (work.func)(work.user_data); + } + }) { + let panic_message: Cow<'static, str> = + if let Some(panic_message) = + panic_err.downcast_ref::<&'static str>() + { + (*panic_message).into() + } else if let Some(panic_message) = + panic_err.downcast_ref::<String>() + { + panic_message.clone().into() + } else { + "(unknown panic payload type)".into() + }; + + let _ = thread_panic_b + .set(panic_message.into_owned().into_boxed_str()); + } + }) + .expect("Failed to create work queue thread"), + _pd: PhantomData, + } + } + + pub fn get_thread_panic(&self) -> Option<&str> + { + self.thread_panic.get().map(|thread_panic| &**thread_panic) + } + + 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"); + } + } +} |
