diff options
Diffstat (limited to 'engine/src')
58 files changed, 9705 insertions, 4298 deletions
diff --git a/engine/src/asset.rs b/engine/src/asset.rs new file mode 100644 index 0000000..e9a3831 --- /dev/null +++ b/engine/src/asset.rs @@ -0,0 +1,933 @@ +use std::any::{Any, type_name}; +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::HashMap; +use std::convert::Infallible; +use std::ffi::{OsStr, OsString}; +use std::fmt::{Debug, Display}; +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::sync::mpsc::{ + Receiver as MpscReceiver, + Sender as MpscSender, + channel as mpsc_channel, +}; + +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{PRE_UPDATE as PRE_UPDATE_PHASE, Phase}; +use ecs::sole::Single; +use ecs::{Sole, declare_entity}; + +use crate::work_queue::{Work, WorkQueue}; + +declare_entity!( + pub HANDLE_ASSETS_PHASE, + ( + Phase, + Pair::builder() + .relation::<ChildOf>() + .target_id(*PRE_UPDATE_PHASE) + .build() + ) +); + +/// 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 + 'static, + { + self.importers.push(WrappedImporterFn::new(func)); + + let importer_index = self.importers.len() - 1; + + self.importer_lookup + .extend(file_extensions.into_iter().map(|file_ext| { + let file_ext: Cow<str> = file_ext.into(); + + (file_ext.into_owned().into(), importer_index) + })); + } + + #[tracing::instrument(skip_all, fields(asset_type=type_name::<Asset>()))] + pub fn get<Asset: 'static + Send + Sync>( + &self, + handle: &Handle<Asset>, + ) -> Option<&Asset> + { + let asset_lookup = self.asset_lookup.borrow(); + + let LookupEntry::Occupied(asset_index, _) = + asset_lookup.get(&handle.id.label_hash)? + else { + return None; + }; + + let stored_asset = self.assets.get(*asset_index).expect("Not possible"); + + let Some(asset) = stored_asset.strong.downcast_ref::<Asset>() else { + tracing::error!("Wrong asset type"); + return None; + }; + + Some(asset) + } + + #[tracing::instrument(skip_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())); + + if label.name.is_some() { + let parent_asset_label = Label { + path: label.path.as_ref().into(), + name: None, + }; + + let parent_asset_label_hash = LabelHash::new(&parent_asset_label); + + if matches!( + self.asset_lookup.get_mut().get(&parent_asset_label_hash), + Some(LookupEntry::Pending) + ) { + self.asset_lookup.get_mut().remove(&parent_asset_label_hash); + } else if self + .asset_lookup + .get_mut() + .get(&parent_asset_label_hash) + .is_none() + { + self.assets + .push(StoredAsset::new::<Option<Infallible>>(None)); + + self.asset_lookup.get_mut().insert( + parent_asset_label_hash, + LookupEntry::Occupied( + self.assets.len() - 1, + parent_asset_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 load asset {}: {err}", + asset_path.display() + ); + } + }, + user_data: ImportWorkUserData { + import_work_msg_sender: self.import_work_msg_sender.clone(), + asset_path: label.path.to_path_buf(), + asset_settings: asset_settings.map(|asset_settings| { + Box::new(asset_settings) as Box<dyn Any + Send + Sync> + }), + importer: importer.clone(), + }, + }); + + asset_lookup.insert(label_hash, LookupEntry::Pending); + + if label.name.is_some() { + asset_lookup.insert( + LabelHash::new(&Label { + path: label.path.as_ref().into(), + name: None, + }), + LookupEntry::Pending, + ); + } + } + + fn get_importer(&self, file_ext: &OsStr) -> Option<&WrappedImporterFn> + { + let index = *self.importer_lookup.get(file_ext)?; + + Some(self.importers.get(index).expect("Not possible")) + } +} + +impl Default for Assets +{ + fn default() -> Self + { + Self::with_capacity(0) + } +} + +pub struct Submitter<'path> +{ + import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: &'path Path, +} + +impl Submitter<'_> +{ + pub fn submit_load_other<'label, Asset: Send + Sync + 'static>( + &self, + label: impl Into<Label<'label>>, + ) -> Handle<Asset> + { + let label = label.into(); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Load { + do_load: |assets, label, _asset_settings| { + let _ = assets.load::<Asset>(label); + }, + label: label.to_owned(), + asset_settings: None, + }); + + Handle::new(LabelHash::new(&label)) + } + + pub fn submit_load_other_with_settings<'label, Asset, AssetSettings>( + &self, + label: impl Into<Label<'label>>, + asset_settings: AssetSettings, + ) -> Handle<Asset> + where + Asset: Send + Sync + 'static, + AssetSettings: Send + Sync + Debug + 'static, + { + let label = label.into(); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Load { + do_load: |assets, label, asset_settings| { + let asset_settings = *asset_settings + .expect("Not possible") + .downcast::<AssetSettings>() + .expect("Not possible"); + + let _ = assets + .load_with_settings::<Asset, AssetSettings>(label, asset_settings); + }, + label: label.to_owned(), + asset_settings: Some(Box::new(asset_settings)), + }); + + Handle::new(LabelHash::new(&label)) + } + + pub fn submit_store<Asset: Send + Sync + 'static>( + &self, + asset: Asset, + ) -> Handle<Asset> + { + let label = LabelOwned { + path: self.asset_path.into(), + name: None, + }; + + let label_hash = LabelHash::new(&label.to_label()); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Store { + do_store: |assets, label, boxed_asset| { + let Ok(asset) = boxed_asset.downcast::<Asset>() else { + unreachable!(); + }; + + assets.store_with_label::<Asset>(&label, *asset); + }, + label, + asset: Box::new(asset), + }); + + Handle::new(label_hash) + } + + pub fn submit_store_named<Asset: Send + Sync + 'static>( + &self, + name: impl AsRef<str>, + asset: Asset, + ) -> Handle<Asset> + { + let label = LabelOwned { + path: self.asset_path.into(), + name: Some(name.as_ref().into()), + }; + + let label_hash = LabelHash::new(&label.to_label()); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Store { + do_store: |assets, label, boxed_asset| { + let Ok(asset) = boxed_asset.downcast::<Asset>() else { + unreachable!(); + }; + + assets.store_with_label::<Asset>(&label, *asset); + }, + label, + asset: Box::new(asset), + }); + + Handle::new(label_hash) + } +} + +/// Asset handle. +#[derive(Debug)] +pub struct Handle<Asset: 'static> +{ + id: Id, + _pd: PhantomData<Asset>, +} + +impl<Asset: 'static> Handle<Asset> +{ + pub fn 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>), +} + +#[derive(Debug, Clone)] +struct WrappedImporterFn +{ + wrapper_func: fn( + MpscSender<ImportWorkMessage>, + &Path, + Option<&(dyn Any + Send + Sync)>, + ) -> Result<(), ImporterError>, +} + +impl WrappedImporterFn +{ + fn new<InnerFunc, AssetSettings, Err>(inner_func_param: InnerFunc) -> Self + where + InnerFunc: + Fn(&mut Submitter<'_>, &Path, Option<&AssetSettings>) -> Result<(), Err>, + AssetSettings: 'static, + Err: std::error::Error + 'static, + { + assert_eq!(size_of::<InnerFunc>(), 0); + + let wrapper_func = + |import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: &Path, + asset_settings: Option<&(dyn Any + Send + Sync)>| { + let inner_func = unsafe { std::mem::zeroed::<InnerFunc>() }; + + let asset_settings = asset_settings + .map(|asset_settings| { + asset_settings + .downcast_ref::<AssetSettings>() + .ok_or_else(|| { + ImporterError::IncorrectAssetSettingsType( + asset_path.to_path_buf(), + ) + }) + }) + .transpose()?; + + inner_func( + &mut Submitter { import_work_msg_sender, asset_path }, + asset_path, + asset_settings, + ) + .map_err(|err| ImporterError::Other(Box::new(err)))?; + + Ok(()) + }; + + std::mem::forget(inner_func_param); + + Self { wrapper_func } + } + + fn call( + &self, + import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: &Path, + asset_settings: Option<&(dyn Any + Send + Sync)>, + ) -> Result<(), ImporterError> + { + (self.wrapper_func)(import_work_msg_sender, asset_path, asset_settings) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct LabelHash(u64); + +impl LabelHash +{ + fn new(label: &Label<'_>) -> Self + { + let mut hasher = DefaultHasher::new(); + + label.hash(&mut hasher); + + Self(hasher.finish()) + } +} + +#[derive(Debug, Default)] +pub(crate) struct Extension +{ + pub assets: Assets, +} + +impl ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: ecs::extension::Collector<'_>) + { + let _ = collector.add_sole(self.assets); + + collector.add_declared_entity(&HANDLE_ASSETS_PHASE); + + collector.add_system(*HANDLE_ASSETS_PHASE, add_received_assets); + } +} + +fn add_received_assets(mut assets: Single<Assets>) +{ + 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(&mut assets, label, asset); + } + ImportWorkMessage::Load { do_load, label, asset_settings } => { + do_load( + &assets, + Label { + path: label.path.as_path().into(), + name: label.name.as_deref().map(|name| name.into()), + }, + asset_settings, + ); + } + } + } +} + +#[derive(Debug)] +struct ImportWorkUserData +{ + import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: PathBuf, + asset_settings: Option<Box<dyn Any + Send + Sync>>, + importer: WrappedImporterFn, +} + +#[derive(Debug)] +enum ImportWorkMessage +{ + Store + { + do_store: fn(&mut Assets, LabelOwned, Box<dyn Any + Send + Sync>), + label: LabelOwned, + asset: Box<dyn Any + Send + Sync>, + }, + + Load + { + do_load: fn(&Assets, Label<'_>, Option<Box<dyn Any + Send + Sync>>), + label: LabelOwned, + asset_settings: Option<Box<dyn Any + Send + Sync>>, + }, +} + +#[derive(Debug, Clone)] +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..fa3d612 100644 --- a/engine/src/camera.rs +++ b/engine/src/camera.rs @@ -5,7 +5,7 @@ use crate::vector::Vec3; pub mod fly; -#[derive(Debug, Component)] +#[derive(Debug, Clone, Component)] pub struct Camera { pub target: Vec3<f32>, @@ -18,7 +18,7 @@ impl Default for Camera fn default() -> Self { Self { - target: Vec3::default(), + target: Vec3::FRONT, global_up: Vec3::UP, projection: Projection::Perspective(Perspective::default()), } diff --git a/engine/src/camera/fly.rs b/engine/src/camera/fly.rs index 087f727..2042c0c 100644 --- a/engine/src/camera/fly.rs +++ b/engine/src/camera/fly.rs @@ -1,14 +1,16 @@ use ecs::component::local::Local; use ecs::phase::UPDATE as UPDATE_PHASE; use ecs::sole::Single; -use ecs::system::{Into, System}; +use ecs::system::Into; +use ecs::system::initializable::Initializable; use ecs::{Component, Query}; +use crate::builder; use crate::camera::{Active as ActiveCamera, Camera}; use crate::delta_time::DeltaTime; -use crate::input::{Cursor, CursorFlags, Key, KeyState, Keys}; -use crate::transform::Position; -use crate::util::builder; +use crate::input::keyboard::{Key, Keyboard}; +use crate::input::mouse::Mouse; +use crate::transform::WorldPosition; use crate::vector::{Vec2, Vec3}; builder! { @@ -59,12 +61,7 @@ impl ecs::extension::Extension for Extension { fn collect(self, mut collector: ecs::extension::Collector<'_>) { - collector.add_system( - *UPDATE_PHASE, - update - .into_system() - .initialize((CursorState::default(), self.0)), - ); + collector.add_system(*UPDATE_PHASE, update.into_system().initialize((self.0,))); } } @@ -75,36 +72,28 @@ pub struct Options } fn update( - camera_query: Query<(&mut Camera, &mut Position, &mut Fly, &ActiveCamera)>, - keys: Single<Keys>, - cursor: Single<Cursor>, - cursor_flags: Single<CursorFlags>, + camera_query: Query<(&mut Camera, &mut WorldPosition, &mut Fly, &ActiveCamera)>, + keyboard: Single<Keyboard>, + mouse: Single<Mouse>, delta_time: Single<DeltaTime>, - mut cursor_state: Local<CursorState>, options: Local<Options>, ) { - for (mut camera, mut camera_pos, mut fly_camera, _) in &camera_query { - if cursor.has_moved && cursor_flags.is_first_move.flag { - tracing::debug!("First cursor move"); - - cursor_state.last_pos = cursor.position; - } - + for (mut camera, mut camera_world_pos, mut fly_camera, _) in &camera_query { 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.current_yaw += x_offset; + fly_camera.current_pitch += y_offset; - fly_camera.current_pitch = fly_camera.current_pitch.clamp(-89.0, 89.0); + 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)] @@ -121,35 +110,30 @@ fn update( camera.global_up = cam_right.cross(&direction).normalize(); - if keys.get_key_state(Key::W) == KeyState::Pressed { - camera_pos.position += + if keyboard.pressed(Key::W) { + camera_world_pos.position += direction * fly_camera.speed * delta_time.as_secs_f32(); } - if keys.get_key_state(Key::S) == KeyState::Pressed { - camera_pos.position -= + if keyboard.pressed(Key::S) { + camera_world_pos.position -= direction * fly_camera.speed * delta_time.as_secs_f32(); } - if 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_world_pos.position += + cam_left * fly_camera.speed * delta_time.as_secs_f32(); } - if keys.get_key_state(Key::D) == KeyState::Pressed { + if keyboard.pressed(Key::D) { let cam_right = direction.cross(&Vec3::UP).normalize(); - camera_pos.position += + camera_world_pos.position += cam_right * fly_camera.speed * delta_time.as_secs_f32(); } - camera.target = camera_pos.position + direction; + camera.target = camera_world_pos.position + direction; } } - -#[derive(Debug, Default, Component)] -struct CursorState -{ - last_pos: Vec2<f64>, -} diff --git a/engine/src/data_types/color.rs b/engine/src/data_types/color.rs index cef3b92..c5316e6 100644 --- a/engine/src/data_types/color.rs +++ b/engine/src/data_types/color.rs @@ -1,7 +1,6 @@ use std::ops::{Add, Div, Mul, Neg, Sub}; #[derive(Debug, Clone, Default)] -#[repr(C)] pub struct Color<Value> { pub red: Value, diff --git a/engine/src/data_types/dimens.rs b/engine/src/data_types/dimens.rs index 5002436..a197560 100644 --- a/engine/src/data_types/dimens.rs +++ b/engine/src/data_types/dimens.rs @@ -1,11 +1,55 @@ +use std::num::NonZeroU32; +use std::ops::Div; + /// 2D dimensions. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] 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)] pub struct Dimens3<Value> @@ -14,3 +58,27 @@ pub struct Dimens3<Value> pub height: Value, pub depth: Value, } + +impl<Value: Clone> From<Value> for Dimens3<Value> +{ + fn from(value: Value) -> Self + { + Self { + width: value.clone(), + height: value.clone(), + depth: value, + } + } +} + +impl<Value: Clone> From<(Value, Value, Value)> for Dimens3<Value> +{ + fn from(value: (Value, Value, Value)) -> Self + { + Self { + width: value.0, + height: value.1, + depth: value.2, + } + } +} diff --git a/engine/src/data_types/matrix.rs b/engine/src/data_types/matrix.rs index 3a29ae2..486ab24 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::Mul; + +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], + pub items: [[Value; ROWS]; COLUMNS], } impl<Value, const ROWS: usize, const COLUMNS: usize> Matrix<Value, ROWS, COLUMNS> @@ -20,17 +22,24 @@ impl<Value, const ROWS: usize, const COLUMNS: usize> Matrix<Value, ROWS, COLUMNS } } + pub fn from_columns<Column>(columns: [Column; COLUMNS]) -> Self + where + Column: Into<[Value; ROWS]>, + { + Self { + items: columns.map(|column| column.into()), + } + } + /// Sets the value at the specified cell. pub fn set_cell(&mut self, row: usize, column: usize, value: Value) { self.items[column][row] = value; } - /// Returns the internal 2D array as a pointer. - #[must_use] - pub fn as_ptr(&self) -> *const Value + pub fn items(&self) -> &[[Value; ROWS]; COLUMNS] { - self.items[0].as_ptr() + &self.items } } @@ -119,4 +128,155 @@ impl Matrix<f32, 4, 4> self.set_cell(3, 3, 1.0); } + + pub fn inverse(&self) -> Self + { + let coef_00 = + self.items[2][2] * self.items[3][3] - self.items[3][2] * self.items[2][3]; + let coef_02 = + self.items[1][2] * self.items[3][3] - self.items[3][2] * self.items[1][3]; + let coef_03 = + self.items[1][2] * self.items[2][3] - self.items[2][2] * self.items[1][3]; + + let coef_04 = + self.items[2][1] * self.items[3][3] - self.items[3][1] * self.items[2][3]; + let coef_06 = + self.items[1][1] * self.items[3][3] - self.items[3][1] * self.items[1][3]; + let coef_07 = + self.items[1][1] * self.items[2][3] - self.items[2][1] * self.items[1][3]; + + let coef_08 = + self.items[2][1] * self.items[3][2] - self.items[3][1] * self.items[2][2]; + let coef_10 = + self.items[1][1] * self.items[3][2] - self.items[3][1] * self.items[1][2]; + let coef_11 = + self.items[1][1] * self.items[2][2] - self.items[2][1] * self.items[1][2]; + + let coef_12 = + self.items[2][0] * self.items[3][3] - self.items[3][0] * self.items[2][3]; + let coef_14 = + self.items[1][0] * self.items[3][3] - self.items[3][0] * self.items[1][3]; + let coef_15 = + self.items[1][0] * self.items[2][3] - self.items[2][0] * self.items[1][3]; + + let coef_16 = + self.items[2][0] * self.items[3][2] - self.items[3][0] * self.items[2][2]; + let coef_18 = + self.items[1][0] * self.items[3][2] - self.items[3][0] * self.items[1][2]; + let coef_19 = + self.items[1][0] * self.items[2][2] - self.items[2][0] * self.items[1][2]; + + let coef_20 = + self.items[2][0] * self.items[3][1] - self.items[3][0] * self.items[2][1]; + let coef_22 = + self.items[1][0] * self.items[3][1] - self.items[3][0] * self.items[1][1]; + let coef_23 = + self.items[1][0] * self.items[2][1] - self.items[2][0] * self.items[1][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.items[1][0], + y: self.items[0][0], + z: self.items[0][0], + w: self.items[0][0], + }; + let vec_1 = Vec4 { + x: self.items[1][1], + y: self.items[0][1], + z: self.items[0][1], + w: self.items[0][1], + }; + let vec_2 = Vec4 { + x: self.items[1][2], + y: self.items[0][2], + z: self.items[0][2], + w: self.items[0][2], + }; + let vec_3 = Vec4 { + x: self.items[1][3], + y: self.items[0][3], + z: self.items[0][3], + w: self.items[0][3], + }; + + 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.items[0][0], + y: inverse.items[1][0], + z: inverse.items[2][0], + w: inverse.items[3][0], + }; + + let dot_0 = Vec4::<f32>::from(self.items[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(|column| (Vec4::from(column) * scalar).into()), + } + } } diff --git a/engine/src/data_types/vector.rs b/engine/src/data_types/vector.rs index 802a4a7..933ca51 100644 --- a/engine/src/data_types/vector.rs +++ b/engine/src/data_types/vector.rs @@ -2,18 +2,49 @@ use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign}; use crate::color::Color; -#[derive(Debug, Default, Clone, Copy, PartialEq)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct Vec2<Value> { 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 for Vec2<Value> +where + Value: Add<Value, Output = Value>, +{ + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output + { + Self::Output { x: self.x + rhs.x, y: self.y + rhs.y } + } +} + +impl<Value> AddAssign for Vec2<Value> +where + Value: Add<Value, Output = Value> + Clone, +{ + fn add_assign(&mut self, rhs: Self) + { + self.x = self.x.clone() + rhs.x; + self.y = self.y.clone() + rhs.y; + } +} + impl<Value> Add<Value> for Vec2<Value> where Value: Add<Output = Value> + Clone, @@ -74,8 +105,23 @@ where } } +impl<Value> From<[Value; 2]> for Vec2<Value> +{ + fn from([x, y]: [Value; 2]) -> Self + { + Self { x, y } + } +} + +impl<Value> From<Vec2<Value>> for [Value; 2] +{ + fn from(vec: Vec2<Value>) -> Self + { + [vec.x, vec.y] + } +} + #[derive(Debug, Default, Clone, Copy, PartialEq)] -#[repr(C)] pub struct Vec3<Value> { pub x: Value, @@ -144,9 +190,9 @@ impl Vec3<f32> impl<Value> Vec3<Value> { - pub fn as_ptr(&self) -> *const Value + pub fn into_array(self) -> [Value; 3] { - &self.x + self.into() } } @@ -318,17 +364,11 @@ where } } -impl<Value> From<Value> for Vec3<Value> -where - Value: Clone, +impl From<f32> for Vec3<f32> { - fn from(value: Value) -> Self + fn from(value: f32) -> Self { - Self { - x: value.clone(), - y: value.clone(), - z: value, - } + Self { x: value, y: value, z: value } } } @@ -343,3 +383,127 @@ impl<Value> From<Color<Value>> for Vec3<Value> } } } + +impl<Value> From<[Value; 3]> for Vec3<Value> +{ + fn from([x, y, z]: [Value; 3]) -> Self + { + Self { x, y, z } + } +} + +impl<Value> From<Vec3<Value>> for [Value; 3] +{ + fn from(vec: Vec3<Value>) -> Self + { + [vec.x, vec.y, vec.z] + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub struct Vec4<Value> +{ + pub x: Value, + pub y: Value, + pub z: Value, + pub w: Value, +} + +impl<Value> Mul for Vec4<Value> +where + Value: Mul<Value, Output = Value>, +{ + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output + { + Self::Output { + x: self.x * rhs.x, + y: self.y * rhs.y, + z: self.z * rhs.z, + w: self.w * rhs.w, + } + } +} + +impl<Value> Add for Vec4<Value> +where + Value: Add<Value, Output = Value>, +{ + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output + { + Self::Output { + x: self.x + rhs.x, + y: self.y + rhs.y, + z: self.z + rhs.z, + w: self.w + rhs.w, + } + } +} + +impl<Value> Sub for Vec4<Value> +where + Value: Sub<Value, Output = Value>, +{ + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output + { + Self::Output { + x: self.x - rhs.x, + y: self.y - rhs.y, + z: self.z - rhs.z, + w: self.w - rhs.w, + } + } +} + +impl<Value> Mul<Value> for Vec4<Value> +where + Value: Mul<Value, Output = Value> + Clone, +{ + type Output = Self; + + fn mul(mut self, rhs: Value) -> Self::Output + { + self.x = self.x * rhs.clone(); + self.y = self.y * rhs.clone(); + self.z = self.z * rhs.clone(); + self.w = self.w * rhs.clone(); + + self + } +} + +impl<Value: Clone> From<Value> for Vec4<Value> +{ + fn from(value: Value) -> Self + { + Self { + x: value.clone(), + y: value.clone(), + z: value.clone(), + w: value, + } + } +} + +impl<Value> From<[Value; 4]> for Vec4<Value> +{ + fn from(values: [Value; 4]) -> Self + { + 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] + } +} diff --git a/engine/src/draw_flags.rs b/engine/src/draw_flags.rs index df5eed1..8328669 100644 --- a/engine/src/draw_flags.rs +++ b/engine/src/draw_flags.rs @@ -1,6 +1,6 @@ use ecs::Component; -use crate::util::builder; +use crate::builder; builder! { /// Flags for how a object should be drawn. @@ -22,7 +22,7 @@ impl DrawFlags } } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct PolygonModeConfig { pub face: PolygonModeFace, diff --git a/engine/src/file_format/wavefront/mtl.rs b/engine/src/file_format/wavefront/mtl.rs index d90dbcf..f3c7a64 100644 --- a/engine/src/file_format/wavefront/mtl.rs +++ b/engine/src/file_format/wavefront/mtl.rs @@ -2,7 +2,7 @@ //! //! File format documentation: <https://paulbourke.net/dataformats/mtl> -use std::path::Path; +use std::path::{Path, PathBuf}; use crate::color::Color; use crate::file_format::wavefront::common::{ @@ -11,8 +11,6 @@ use crate::file_format::wavefront::common::{ ParsingError, Statement, }; -use crate::material::{Builder as MaterialBuilder, Material}; -use crate::texture::{Error as TextureError, Texture}; /// Parses the content of a Wavefront `.mtl`. /// @@ -50,18 +48,41 @@ pub fn parse(obj_content: &str) -> Result<Vec<NamedMaterial>, Error> } #[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::WHITE_F32, + diffuse: Color::WHITE_F32, + specular: Color::WHITE_F32, + 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)] @@ -70,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}" @@ -100,59 +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 { - 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)?; - 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)?; - 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)?; - 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 { @@ -165,51 +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))?; - - 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, + }); + } - 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, + }); + } - 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 { - 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) @@ -235,24 +279,6 @@ fn get_color_from_statement( 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))?) -} - keyword! { #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum Keyword { @@ -271,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 6ca11c2..446903c 100644 --- a/engine/src/file_format/wavefront/obj.rs +++ b/engine/src/file_format/wavefront/obj.rs @@ -7,16 +7,20 @@ use std::fs::read_to_string; use std::path::PathBuf; use crate::file_format::wavefront::common::{ - keyword, - parse_statement_line, ParsingError, Statement, Triplet, + keyword, + parse_statement_line, +}; +use crate::mesh::vertex_buffer::{ + NamedVertexAttr, + VertexAttrInfo, + VertexBuffer as MeshVertexBuffer, }; -use crate::mesh::Mesh; +use crate::mesh::{Mesh, POSITION_VERTEX_ATTRIB_NAME, VertexAttrType}; use crate::util::try_option; use crate::vector::{Vec2, Vec3}; -use crate::vertex::{Builder as VertexBuilder, Vertex}; /// Parses the content of a Wavefront `.obj`. /// @@ -83,7 +87,24 @@ impl Obj /// - A face index does not fit in a [`u32`] pub fn to_mesh(&self) -> Result<Mesh, Error> { - let mut vertices = Vec::<Vertex>::with_capacity(self.faces.len() * 3); + let mut vertex_buf = MeshVertexBuffer::with_capacity( + &[ + VertexAttrInfo { + name: POSITION_VERTEX_ATTRIB_NAME.into(), + ty: VertexAttrType::Float32Array { length: 3 }, + }, + VertexAttrInfo { + name: "texture_coords".into(), + ty: VertexAttrType::Float32Array { length: 2 }, + }, + VertexAttrInfo { + name: "normal".into(), + 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 = @@ -97,10 +118,75 @@ impl Obj continue; } - vertices.push(face_vertex.to_vertex(self)?); + 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 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 { + name: POSITION_VERTEX_ATTRIB_NAME, + value: pos.into_array(), + }, + NamedVertexAttr { + name: "texture_coords", + value: texture_pos.into_array(), + }, + NamedVertexAttr { + name: "normal", + value: normal.into_array(), + }, + )); + + let vertex_index = vertex_buf.len() - 1; - let vertex_index = u32::try_from(vertices.len() - 1) - .map_err(|_| Error::FaceIndexTooBig(vertices.len() - 1))?; + let vertex_index = u32::try_from(vertex_index) + .map_err(|_| Error::FaceIndexTooBig(vertex_index))?; indices.push(vertex_index); @@ -108,7 +194,10 @@ impl Obj } } - Ok(Mesh::new(vertices, Some(indices))) + Ok(Mesh::builder() + .vertices(vertex_buf) + .indices(indices) + .build()) } /// Reads and parses the material libraries of this `Obj`. @@ -157,51 +246,6 @@ pub struct FaceVertex pub normal: Option<u32>, } -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> - { - let mut vertex_builder = VertexBuilder::default(); - - let vertex_pos = *obj.vertex_positions.get(self.position as usize - 1).ok_or( - Error::FaceVertexPositionNotFound { vertex_pos_index: self.position }, - )?; - - vertex_builder = vertex_builder.pos(vertex_pos); - - 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, - })?; - - vertex_builder = vertex_builder.texture_coords(*texture_pos); - } - - 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, - })?; - - vertex_builder = vertex_builder.normal(vertex_normal); - } - - Ok(vertex_builder.build()) - } -} - impl From<Triplet> for FaceVertex { fn from(triplet: Triplet) -> Self @@ -220,9 +264,7 @@ pub enum Error #[error(transparent)] ParsingError(#[from] ParsingError), - #[error( - "Face vertex position with index {vertex_pos_index} (1-based) was not found" - )] + #[error("Face vertex position with index {vertex_pos_index} (1-based) was not found")] FaceVertexPositionNotFound { vertex_pos_index: u32 @@ -525,12 +567,16 @@ fn get_mtl_libs_from_statements( 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>>()); + 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)) }) diff --git a/engine/src/image.rs b/engine/src/image.rs new file mode 100644 index 0000000..b6d8013 --- /dev/null +++ b/engine/src/image.rs @@ -0,0 +1,272 @@ +use std::any::type_name; +use std::fs::File; +use std::io::BufReader; +use std::path::Path; + +use image_rs::GenericImageView as _; +use zerocopy::{FromBytes, Immutable}; + +use crate::color::Color; +use crate::data_types::dimens::Dimens; + +#[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::ReadFailed)?); + + let image_reader = image_rs::ImageReader::with_format( + buffered_reader, + image_rs::ImageFormat::from_path(path) + .map_err(|_| Error::UnsupportedFormat)?, + ); + + Ok(Self { + inner: image_reader + .decode() + .map_err(|err| Error::DecodeFailed(DecodeError(err)))?, + }) + } + + pub fn try_from_bytes( + bytes: &[u8], + dimens: Dimens<u32>, + color_type: ColorType, + ) -> Result<Self, FromBytesError> + { + use image_rs::{Luma, LumaA, Rgb, Rgba}; + + let inner = match color_type { + ColorType::L8 => image_buf_from_bytes::<Luma<u8>>(dimens, bytes)?.into(), + ColorType::La8 => image_buf_from_bytes::<LumaA<u8>>(dimens, bytes)?.into(), + ColorType::Rgb8 => image_buf_from_bytes::<Rgb<u8>>(dimens, bytes)?.into(), + ColorType::Rgba8 => image_buf_from_bytes::<Rgba<u8>>(dimens, bytes)?.into(), + ColorType::L16 => image_buf_from_bytes::<Luma<u16>>(dimens, bytes)?.into(), + ColorType::La16 => image_buf_from_bytes::<LumaA<u16>>(dimens, bytes)?.into(), + ColorType::Rgb16 => image_buf_from_bytes::<Rgb<u16>>(dimens, bytes)?.into(), + ColorType::Rgba16 => image_buf_from_bytes::<Rgba<u16>>(dimens, bytes)?.into(), + ColorType::Rgb32F => image_buf_from_bytes::<Rgb<f32>>(dimens, bytes)?.into(), + ColorType::Rgba32F => { + image_buf_from_bytes::<Rgba<f32>>(dimens, bytes)?.into() + } + }; + + Ok(Self { inner }) + } + + pub fn from_color(dimens: impl Into<Dimens<u32>>, color: impl Into<Color<u8>>) + -> Self + { + let dimens: Dimens<u32> = dimens.into(); + + let color: Color<u8> = color.into(); + + Self { + inner: image_rs::RgbImage::from_pixel( + dimens.width, + dimens.height, + image_rs::Rgb([color.red, color.green, color.blue]), + ) + .into(), + } + } + + pub fn from_color_and_alpha( + dimens: impl Into<Dimens<u32>>, + color: impl Into<Color<u8>>, + alpha: u8, + ) -> Self + { + let dimens: Dimens<u32> = dimens.into(); + + let color: Color<u8> = color.into(); + + Self { + inner: image_rs::RgbaImage::from_pixel( + dimens.width, + dimens.height, + image_rs::Rgba([color.red, color.green, color.blue, alpha]), + ) + .into(), + } + } + + 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, + } + } + + pub fn to_rgba8(&self) -> Self + { + Self { inner: self.inner.to_rgba8().into() } + } + + pub fn as_bytes(&self) -> &[u8] + { + self.inner.as_bytes() + } +} + +/// An enumeration over supported color types and bit depths +#[derive(Copy, PartialEq, Eq, Debug, Clone, Hash)] +#[non_exhaustive] +pub enum ColorType +{ + /// Pixel is 8-bit luminance + L8, + + /// Pixel is 8-bit luminance with an alpha channel + La8, + + /// Pixel contains 8-bit R, G and B channels + Rgb8, + + /// Pixel is 8-bit RGB with an alpha channel + Rgba8, + + /// Pixel is 16-bit luminance + L16, + + /// Pixel is 16-bit luminance with an alpha channel + La16, + + /// Pixel is 16-bit RGB + Rgb16, + + /// Pixel is 16-bit RGBA + Rgba16, + + /// Pixel is 32-bit float RGB + Rgb32F, + + /// Pixel is 32-bit float RGBA + Rgba32F, +} + +impl From<image_rs::ColorType> for ColorType +{ + fn from(color_type: image_rs::ColorType) -> Self + { + match color_type { + image_rs::ColorType::L8 => Self::L8, + image_rs::ColorType::La8 => Self::La8, + image_rs::ColorType::Rgb8 => Self::Rgb8, + image_rs::ColorType::Rgba8 => Self::Rgba8, + image_rs::ColorType::L16 => Self::L16, + image_rs::ColorType::La16 => Self::La16, + image_rs::ColorType::Rgb16 => Self::Rgb16, + image_rs::ColorType::Rgba16 => Self::Rgba16, + image_rs::ColorType::Rgb32F => Self::Rgb32F, + image_rs::ColorType::Rgba32F => Self::Rgba32F, + _ => { + panic!("Unrecognized image_rs::ColorType variant"); + } + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error("Failed to read file")] + ReadFailed(#[source] std::io::Error), + + #[error("Failed to decode image")] + DecodeFailed(DecodeError), + + #[error("Unsupported image format")] + UnsupportedFormat, +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct DecodeError(image_rs::ImageError); + +#[derive(Debug, thiserror::Error)] +pub enum FromBytesError +{ + #[error( + "The number of bytes provided is not enough to fit a image with the given size" + )] + NotEnoughBytesForDimensions, + + #[error("Failed to cast bytes to subpixels")] + CastToSubPixelsFailed(#[from] CastToSubPixelsError), +} + +#[derive(Debug, thiserror::Error)] +pub enum CastToSubPixelsError +{ + #[error( + "Source address {:?} ({}) isn't a multiple of the alignment of the type {} ({})", + src, + src_type_name, + dst_type_name, + dst_align + )] + Alignment + { + src_type_name: &'static str, + dst_type_name: &'static str, + src: *const u8, + dst_align: usize, + }, + + #[error( + "Source size {} ({}) is incorrect size for type {}", + src_size, + src_type_name, + dst_type_name + )] + Size + { + src_type_name: &'static str, + dst_type_name: &'static str, + src_size: usize, + }, +} + +fn image_buf_from_bytes<Pixel>( + dimens: Dimens<u32>, + bytes: &[u8], +) -> Result<image_rs::ImageBuffer<Pixel, Vec<Pixel::Subpixel>>, FromBytesError> +where + Pixel: image_rs::Pixel<Subpixel: FromBytes + Immutable>, +{ + let buf = <[Pixel::Subpixel]>::ref_from_bytes(bytes).map_err(|err| match err { + zerocopy::CastError::Alignment(_) => CastToSubPixelsError::Alignment { + src_type_name: type_name::<&[u8]>(), + dst_type_name: type_name::<&[Pixel]>(), + src: bytes.as_ptr(), + dst_align: align_of_val::<[Pixel]>(&[]), + }, + zerocopy::ConvertError::Size(_) => CastToSubPixelsError::Size { + src_type_name: type_name::<&[u8]>(), + dst_type_name: type_name::<&[Pixel]>(), + src_size: bytes.len(), + }, + })?; + + image_rs::ImageBuffer::from_raw(dimens.width, dimens.height, buf.to_vec()) + .ok_or(FromBytesError::NotEnoughBytesForDimensions) +} diff --git a/engine/src/input.rs b/engine/src/input.rs index 95de048..60dd1e7 100644 --- a/engine/src/input.rs +++ b/engine/src/input.rs @@ -1,149 +1,24 @@ -use std::collections::HashMap; - +use ecs::declare_entity; use ecs::extension::Collector as ExtensionCollector; -use ecs::phase::{Phase, PRE_UPDATE as PRE_UPDATE_PHASE, START as START_PHASE}; -use ecs::relationship::{ChildOf, Relationship}; -use ecs::sole::Single; -use ecs::{static_entity, Sole}; - -use crate::vector::Vec2; -use crate::window::{Window, UPDATE_PHASE as WINDOW_UPDATE_PHASE}; +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::Phase; -mod reexports -{ - pub use crate::window::{Key, KeyState}; -} +use crate::windowing::PHASE as WINDOWING_PHASE; -pub use reexports::*; +pub mod keyboard; +pub mod mouse; -static_entity!( - SET_PREV_KEY_STATE_PHASE, +declare_entity!( + pub PHASE, ( Phase, - <Relationship<ChildOf, Phase>>::new(*WINDOW_UPDATE_PHASE) + Pair::builder() + .relation::<ChildOf>() + .target_id(*WINDOWING_PHASE) + .build() ) ); -#[derive(Debug, Sole)] -pub struct Keys -{ - map: HashMap<Key, KeyData>, - pending: Vec<(Key, KeyState)>, -} - -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(), - pending: Vec::with_capacity(Key::KEYS.len()), - } - } - - #[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) - { - 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; - } -} - /// Input extension. #[derive(Debug, Default)] pub struct Extension {} @@ -152,101 +27,8 @@ impl ecs::extension::Extension for Extension { fn collect(self, mut collector: ExtensionCollector<'_>) { - collector.add_system(*START_PHASE, initialize); - collector.add_system(*PRE_UPDATE_PHASE, maybe_clear_cursor_is_first_move); - collector.add_system(*SET_PREV_KEY_STATE_PHASE, set_pending_key_states); + collector.add_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.pending.push((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| { - 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 { - 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 { - 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_pending_key_states(mut keys: Single<Keys>) -{ - let Keys { map, pending } = &mut *keys; - - for key_data in map.values_mut() { - key_data.prev_tick_state = key_data.state; - } - - for (key, key_state) in pending { - let Some(key_data) = map.get_mut(key) else { - unreachable!(); - }; - - key_data.state = *key_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 a9a5a97..0941de0 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -1,42 +1,52 @@ #![deny(clippy::all, clippy::pedantic)] #![allow(clippy::needless_pass_by_value)] -use ecs::component::{Component, Sequence as ComponentSequence}; +use ecs::component::Sequence as ComponentSequence; use ecs::extension::Extension; use ecs::phase::PRE_UPDATE as PRE_UPDATE_PHASE; use ecs::sole::Sole; +use ecs::system::initializable::Initializable; +use ecs::system::observer::Observer; use ecs::system::{Into, System}; use ecs::uid::Uid; use ecs::{SoleAlreadyExistsError, World}; -use crate::delta_time::{update as update_delta_time, DeltaTime, LastUpdate}; +use crate::asset::{Assets, Extension as AssetExtension}; +use crate::delta_time::{DeltaTime, LastUpdate, update as update_delta_time}; +use crate::shader::Extension as ShaderExtension; -mod opengl; mod util; +mod work_queue; +pub mod asset; pub mod camera; pub mod collision; pub mod data_types; pub mod delta_time; pub mod draw_flags; pub mod file_format; +pub mod image; pub mod input; pub mod lighting; pub mod material; pub mod math; pub mod mesh; +pub mod model; pub mod projection; +pub mod reflection; pub mod renderer; +pub mod shader; pub mod texture; pub mod transform; -pub mod vertex; -pub mod window; +pub mod windowing; pub extern crate ecs; pub(crate) use crate::data_types::matrix; pub use crate::data_types::{color, vector}; +const INITIAL_ASSET_CAPACITY: usize = 128; + #[derive(Debug)] pub struct Engine { @@ -49,6 +59,9 @@ impl Engine #[must_use] pub fn new() -> Self { + #[cfg(windows)] + nu_ansi_term::enable_ansi_support().unwrap(); + let mut world = World::new(); world.add_sole(DeltaTime::default()).ok(); @@ -60,6 +73,17 @@ impl Engine .initialize((LastUpdate::default(),)), ); + let mut assets = Assets::with_capacity(INITIAL_ASSET_CAPACITY); + + crate::model::asset::add_importers(&mut assets); + crate::material::asset::add_importers(&mut assets); + crate::shader::add_asset_importers(&mut assets); + + crate::texture::initialize(&mut assets); + + world.add_extension(AssetExtension { assets }); + world.add_extension(ShaderExtension); + Self { world } } @@ -79,14 +103,12 @@ impl Engine self.world.register_system(phase_euid, system); } - pub fn register_observer_system<'this, SystemImpl, Event>( + pub fn register_observer<'this, SystemImpl>( &'this mut self, - system: impl System<'this, SystemImpl>, - event: Event, - ) where - Event: Component, + observer: impl Observer<'this, SystemImpl>, + ) { - self.world.register_observer_system(system, event); + self.world.register_observer(observer); } /// Adds a globally shared singleton value. @@ -104,7 +126,7 @@ impl Engine } /// Runs the event loop. - pub fn start(&self) + pub fn start(&mut self) { self.world.start_loop(); } diff --git a/engine/src/lighting.rs b/engine/src/lighting.rs index 48adb0e..9ab2ca8 100644 --- a/engine/src/lighting.rs +++ b/engine/src/lighting.rs @@ -1,8 +1,8 @@ use ecs::{Component, Sole}; +use crate::builder; use crate::color::Color; use crate::data_types::vector::Vec3; -use crate::util::builder; builder! { #[builder(name = PointLightBuilder, derives = (Debug, Clone))] @@ -10,7 +10,8 @@ builder! { #[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,7 +32,7 @@ impl Default for PointLight fn default() -> Self { Self { - position: Vec3::default(), + local_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 }, attenuation_params: AttenuationParams::default(), @@ -58,7 +59,6 @@ pub struct AttenuationParams impl Default for AttenuationParams { - #[must_use] fn default() -> Self { Self { diff --git a/engine/src/material.rs b/engine/src/material.rs index e368519..ed1c139 100644 --- a/engine/src/material.rs +++ b/engine/src/material.rs @@ -1,29 +1,38 @@ use ecs::Component; +use crate::asset::Handle as AssetHandle; +use crate::builder; use crate::color::Color; -use crate::data_types::dimens::Dimens; -use crate::texture::{Id as TextureId, Texture}; -use crate::util::builder; +use crate::texture::Texture; -#[derive(Debug, Clone, Component)] +pub mod asset; + +#[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 + pub const fn builder() -> Builder + { + Builder::new() + } +} + +impl Default for Material +{ + fn default() -> Self { - Builder::default() + Self::builder().build() } } @@ -31,29 +40,27 @@ impl Material #[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, } impl Builder { #[must_use] - pub fn new() -> Self + pub const fn new() -> Self { Self { - ambient: None, - diffuse: None, - specular: None, + ambient: Color::WHITE_F32, + diffuse: Color::WHITE_F32, + specular: Color::WHITE_F32, ambient_map: None, diffuse_map: None, specular_map: None, - textures: Vec::new(), shininess: 32.0, } } @@ -61,7 +68,7 @@ impl Builder #[must_use] pub fn ambient(mut self, ambient: Color<f32>) -> Self { - self.ambient = Some(ambient); + self.ambient = ambient; self } @@ -69,7 +76,7 @@ impl Builder #[must_use] pub fn diffuse(mut self, diffuse: Color<f32>) -> Self { - self.diffuse = Some(diffuse); + self.diffuse = diffuse; self } @@ -77,13 +84,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); @@ -91,7 +98,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); @@ -99,7 +106,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); @@ -107,22 +114,6 @@ impl Builder } #[must_use] - pub fn textures(mut self, textures: impl IntoIterator<Item = Texture>) -> Self - { - self.textures = textures.into_iter().collect(); - - self - } - - #[must_use] - pub fn texture(mut self, texture: Texture) -> Self - { - self.textures.push(texture); - - self - } - - #[must_use] pub fn shininess(mut self, shininess: f32) -> Self { self.shininess = shininess; @@ -135,43 +126,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, } } @@ -187,8 +150,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)] #[non_exhaustive] pub struct Flags { @@ -201,13 +164,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..b210154 --- /dev/null +++ b/engine/src/material/asset.rs @@ -0,0 +1,89 @@ +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 named_materials = crate::file_format::wavefront::mtl::parse( + &read_to_string(path) + .map_err(|err| Error::ReadFailed(err, path.to_path_buf()))?, + )?; + + let mut mat_asset_map = Map { + assets: HashMap::with_capacity(named_materials.len()), + }; + + 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(ambient_map.path.as_path()), + ); + } + + if let Some(diffuse_map) = material.diffuse_map { + material_builder = material_builder.diffuse_map( + asset_submitter.submit_load_other(diffuse_map.path.as_path()), + ); + } + + if let Some(specular_map) = material.specular_map { + material_builder = material_builder.specular_map( + asset_submitter.submit_load_other(specular_map.path.as_path()), + ); + } + + let material_name = material.name; + let material = material_builder.build(); + + let material_asset = + asset_submitter.submit_store_named(material_name.clone(), material); + + mat_asset_map + .assets + .insert(material_name.into(), material_asset); + } + + asset_submitter.submit_store(mat_asset_map); + + Ok(()) +} + +#[derive(Debug, thiserror::Error)] +enum Error +{ + #[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/mesh.rs b/engine/src/mesh.rs index 917e7f7..a223f60 100644 --- a/engine/src/mesh.rs +++ b/engine/src/mesh.rs @@ -1,35 +1,29 @@ -use ecs::Component; +use std::alloc::Layout; use crate::vector::Vec3; -use crate::vertex::Vertex; pub mod cube; +pub mod vertex_buffer; -#[derive(Debug, Clone, Component)] +pub const POSITION_VERTEX_ATTRIB_NAME: &str = "pos"; + +#[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 - { - Self { vertices, indices } - } - - #[must_use] - pub fn vertices(&self) -> &[Vertex] + pub fn builder() -> Builder { - &self.vertices + Builder::default() } - #[must_use] - pub fn vertices_mut(&mut self) -> &mut [Vertex] + pub fn vertex_buf(&self) -> &vertex_buffer::VertexBuffer { - &mut self.vertices + &self.vertex_buf } #[must_use] @@ -46,21 +40,25 @@ impl Mesh /// 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<'_> + pub fn find_furthest_vertex_positions(&self) -> DirectionPositions { - let mut point_iter = self.vertices().iter().map(|vertex| &vertex.pos).into_iter(); + let mut pos_iter = self + .vertex_buf() + .iter::<[f32; 3]>(POSITION_VERTEX_ATTRIB_NAME) + .map(|vertex_pos| Vec3::from(*vertex_pos)) + .into_iter(); - let first_point = point_iter.next().unwrap(); + let first_pos = pos_iter.next().unwrap(); - point_iter + pos_iter .fold( FurthestPosAcc { - up: FurthestPos::new(&first_point, &Vec3::UP), - down: FurthestPos::new(&first_point, &Vec3::DOWN), - left: FurthestPos::new(&first_point, &Vec3::LEFT), - right: FurthestPos::new(&first_point, &Vec3::RIGHT), - back: FurthestPos::new(&first_point, &Vec3::BACK), - front: FurthestPos::new(&first_point, &Vec3::FRONT), + 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); @@ -77,18 +75,75 @@ impl Mesh } } +/// 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)] -pub struct DirectionPositions<'mesh> +pub struct DirectionPositions { - pub up: &'mesh Vec3<f32>, - pub down: &'mesh Vec3<f32>, - pub left: &'mesh Vec3<f32>, - pub right: &'mesh Vec3<f32>, - pub back: &'mesh Vec3<f32>, - pub front: &'mesh Vec3<f32>, + 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<'mesh> +impl<'mesh> From<FurthestPosAcc<'mesh>> for DirectionPositions { fn from(acc: FurthestPosAcc<'mesh>) -> Self { @@ -117,14 +172,14 @@ struct FurthestPosAcc<'mesh> #[derive(Debug)] struct FurthestPos<'mesh> { - pos: &'mesh Vec3<f32>, + pos: Vec3<f32>, dot_prod: f32, direction: &'mesh Vec3<f32>, } impl<'mesh> FurthestPos<'mesh> { - fn new(pos: &'mesh Vec3<f32>, direction: &'mesh Vec3<f32>) -> Self + fn new(pos: Vec3<f32>, direction: &'mesh Vec3<f32>) -> Self { Self { pos, @@ -133,9 +188,9 @@ impl<'mesh> FurthestPos<'mesh> } } - fn update_if_further(&mut self, point: &'mesh Vec3<f32>) + fn update_if_further(&mut self, point: Vec3<f32>) { - let point_dot_prod = self.direction.dot(point); + let point_dot_prod = self.direction.dot(&point); if point_dot_prod > self.dot_prod { self.pos = point; diff --git a/engine/src/mesh/cube.rs b/engine/src/mesh/cube.rs index c29ce0b..dba6473 100644 --- a/engine/src/mesh/cube.rs +++ b/engine/src/mesh/cube.rs @@ -1,8 +1,15 @@ +use std::collections::HashMap; + +use crate::builder; +use crate::data_types::dimens::Dimens3; use crate::math::calc_triangle_surface_normal; -use crate::mesh::Mesh; -use crate::util::builder; +use crate::mesh::vertex_buffer::{ + NamedVertexAttr, + VertexAttrInfo, + VertexBuffer as MeshVertexBuffer, +}; +use crate::mesh::{Mesh, POSITION_VERTEX_ATTRIB_NAME, VertexAttrType}; use crate::vector::{Vec2, Vec3}; -use crate::vertex::{Builder as VertexBuilder, Vertex}; builder! { /// Cube mesh creation specification. @@ -27,27 +34,16 @@ impl CreationSpec } } -/// Describes a single side of a cube (obviously). -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum Side +impl CreationSpecBuilder { - /// +Z - Front, - - /// -Z - Back, - - /// -X - Left, - - /// +X - Right, - - /// +Y - Top, + pub fn dimens(mut self, dimens: Dimens3<f32>) -> Self + { + self.width = dimens.width; + self.height = dimens.height; + self.depth = dimens.depth; - /// -Y - Bottom, + self + } } /// Describes what location on a side of a cube a face is. @@ -65,11 +61,10 @@ pub enum FaceLocation /// /// By default, the texture coordinates are arranged so that the full texture is visible /// on every side. This can be changed inside of the `face_cb` function. -pub fn create( - creation_spec: CreationSpec, - face_cb: impl FnMut(FaceVertices, Side, FaceLocation) -> FaceVertices, -) -> Mesh +pub fn create(creation_spec: CreationSpec) -> Mesh { + // TODO: Reimplement this mess + let mut data = Data::default(); create_side(&SidePositions::new_top(&creation_spec), &mut data); @@ -79,7 +74,7 @@ pub fn create( create_side(&SidePositions::new_back(&creation_spec), &mut data); create_side(&SidePositions::new_front(&creation_spec), &mut data); - data.into_mesh(face_cb) + data.into_mesh() } #[derive(Debug, Default)] @@ -98,88 +93,55 @@ struct VertexData impl Data { - fn into_mesh( - self, - mut face_cb: impl FnMut(FaceVertices, Side, FaceLocation) -> FaceVertices, - ) -> Mesh + fn into_mesh(self) -> Mesh { - let mut vertices = Vec::<Vertex>::with_capacity(self.faces.len() * 3); + let mut vertex_buf = MeshVertexBuffer::with_capacity( + &[ + VertexAttrInfo { + name: POSITION_VERTEX_ATTRIB_NAME.into(), + ty: VertexAttrType::Float32Array { length: 3 }, + }, + VertexAttrInfo { + name: "texture_coords".into(), + ty: VertexAttrType::Float32Array { length: 2 }, + }, + VertexAttrInfo { + name: "normal".into(), + 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>::new(); + let mut face_location = FaceLocation::RightUp; let Self { faces, vertex_data } = self; for face in faces { - let side = face.side; - - let face_vertices = face_cb( - FaceVertices::new(face, &vertex_data) - .with_full_per_side_tex_coords(face_location), - side, - face_location, - ); - - for vertex in face_vertices.vertices { - if let Some((prev_vertex_index, _)) = vertices - .iter() - .enumerate() - .find(|(_, prev_vertex)| *prev_vertex == &vertex) - { - indices - .push(u32::try_from(prev_vertex_index).expect( - "Vertex index does not fit into 32-bit unsigned int", - )); - + let face_texture_coords = match face_location { + FaceLocation::RightUp => [ + Vec2 { x: 1.0, y: 1.0 }, + Vec2 { x: 0.0, y: 1.0 }, + Vec2 { x: 1.0, y: 0.0 }, + ], + FaceLocation::LeftDown => [ + Vec2 { x: 0.0, y: 1.0 }, + Vec2 { x: 0.0, y: 0.0 }, + Vec2 { x: 1.0, y: 0.0 }, + ], + }; + + for (face_vertex, vertex_uv) in face.vertices.iter().zip(face_texture_coords) + { + if let Some(vertex_index) = added_face_vertices.get(face_vertex) { + indices.push(*vertex_index); continue; } - vertices.push(vertex); - - let vertex_index = u32::try_from(vertices.len() - 1) - .expect("Vertex index does not fit into 32-bit unsigned int"); - - indices.push(vertex_index); - } - - match face_location { - FaceLocation::RightUp => face_location = FaceLocation::LeftDown, - FaceLocation::LeftDown => face_location = FaceLocation::RightUp, - } - } - - Mesh::new(vertices, Some(indices)) - } -} - -/// The vertices of a single face of a cube. -#[derive(Debug, Default, Clone)] -pub struct FaceVertices -{ - /// The three vertices of a face in counter-clockwise order. - /// - /// Order when [`FaceLocation::RightUp`]: - /// ```text - /// ₂ ₁ - /// 🮝 - /// ³ - /// ``` - /// - /// Order when [`FaceLocation::LeftDown`]: - /// ```text - /// ₁ - /// 🮟 - /// ² ³ - /// ``` - pub vertices: [Vertex; 3], -} - -impl FaceVertices -{ - fn new(face: Face, vertex_data: &VertexData) -> Self - { - Self { - vertices: face.vertices.map(|face_vertex| { let vertex_pos = vertex_data .vertex_positions .get(face_vertex.pos_index as usize) @@ -192,30 +154,39 @@ impl FaceVertices .expect("Vertex normal index is out of bounds") .clone(); - VertexBuilder::default() - .pos(vertex_pos) - .normal(vertex_normal) - .build() - }), - } - } + vertex_buf.push(( + NamedVertexAttr { + name: POSITION_VERTEX_ATTRIB_NAME, + value: vertex_pos.into_array(), + }, + NamedVertexAttr { + name: "texture_coords", + value: vertex_uv.into_array(), + }, + NamedVertexAttr { + name: "normal", + value: vertex_normal.into_array(), + }, + )); + + let vertex_index = u32::try_from(vertex_buf.len() - 1) + .expect("Vertex index does not fit into 32-bit unsigned int"); - fn with_full_per_side_tex_coords(mut self, face_location: FaceLocation) -> Self - { - match face_location { - FaceLocation::RightUp => { - self.vertices[0].texture_coords = Vec2 { x: 1.0, y: 1.0 }; - self.vertices[1].texture_coords = Vec2 { x: 0.0, y: 1.0 }; - self.vertices[2].texture_coords = Vec2 { x: 1.0, y: 0.0 }; + indices.push(vertex_index); + + added_face_vertices.insert(face_vertex.clone(), vertex_index); } - FaceLocation::LeftDown => { - self.vertices[0].texture_coords = Vec2 { x: 0.0, y: 1.0 }; - self.vertices[1].texture_coords = Vec2 { x: 0.0, y: 0.0 }; - self.vertices[2].texture_coords = Vec2 { x: 1.0, y: 0.0 }; + + match face_location { + FaceLocation::RightUp => face_location = FaceLocation::LeftDown, + FaceLocation::LeftDown => face_location = FaceLocation::RightUp, } - }; + } - self + Mesh::builder() + .vertices(vertex_buf) + .indices(indices) + .build() } } @@ -223,7 +194,6 @@ impl FaceVertices struct Face { vertices: [FaceVertex; 3], - side: Side, } #[derive(Debug, PartialEq, Eq, Hash, Clone)] @@ -241,7 +211,6 @@ struct SidePositions down_left: Vec3<f32>, down_right: Vec3<f32>, normal_calc_order: NormalCalcOrder, - side: Side, } impl SidePositions @@ -266,7 +235,6 @@ impl SidePositions down_left: Vec3 { x: up_left.x, ..down_right.clone() }, down_right, normal_calc_order: NormalCalcOrder::Clockwise, - side: Side::Top, } } @@ -290,7 +258,6 @@ impl SidePositions down_left: Vec3 { x: up_left.x, ..down_right.clone() }, down_right, normal_calc_order: NormalCalcOrder::CounterClockwise, - side: Side::Bottom, } } @@ -314,7 +281,6 @@ impl SidePositions down_left: Vec3 { z: up_left.z, ..down_right.clone() }, down_right, normal_calc_order: NormalCalcOrder::CounterClockwise, - side: Side::Left, } } @@ -338,7 +304,6 @@ impl SidePositions down_left: Vec3 { z: up_left.z, ..down_right.clone() }, down_right, normal_calc_order: NormalCalcOrder::Clockwise, - side: Side::Right, } } @@ -362,7 +327,6 @@ impl SidePositions down_left: Vec3 { x: up_left.x, ..down_right.clone() }, down_right, normal_calc_order: NormalCalcOrder::Clockwise, - side: Side::Back, } } @@ -386,7 +350,6 @@ impl SidePositions down_left: Vec3 { x: up_left.x, ..down_right.clone() }, down_right, normal_calc_order: NormalCalcOrder::CounterClockwise, - side: Side::Front, } } } @@ -457,7 +420,6 @@ fn create_side(side_positions: &SidePositions, data: &mut Data) normal_index: top_normal_index as u32, }, ], - side: side_positions.side, }); // 🮟 @@ -476,6 +438,5 @@ fn create_side(side_positions: &SidePositions, data: &mut Data) normal_index: top_normal_index as u32, }, ], - side: side_positions.side, }); } diff --git a/engine/src/mesh/vertex_buffer.rs b/engine/src/mesh/vertex_buffer.rs new file mode 100644 index 0000000..3e3c467 --- /dev/null +++ b/engine/src/mesh/vertex_buffer.rs @@ -0,0 +1,247 @@ +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<'name, Value: VertexAttrValue> +{ + pub name: &'name str, + pub value: Value, +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct VertexAttrProperties +{ + pub name: Cow<'static, str>, + pub ty: VertexAttrType, + pub layout: Layout, + pub byte_offset: usize, +} + +#[derive(Debug)] +pub struct VertexAttrInfo +{ + pub name: Cow<'static, str>, + pub ty: VertexAttrType, +} + +#[derive(Debug, Clone, Default)] +pub struct VertexBuffer +{ + buf: Vec<u8>, + vertex_size: usize, + vertex_attr_props: Vec<VertexAttrProperties>, +} + +impl VertexBuffer +{ + pub fn with_capacity(vertex_attrs: &[VertexAttrInfo], capacity: usize) -> Self + { + let mut vertex_attr_props = vertex_attrs + .iter() + .map(|VertexAttrInfo { name, ty }| VertexAttrProperties { + name: name.clone(), + ty: ty.clone(), + layout: ty.layout(), + byte_offset: 0, + }) + .collect::<Vec<_>>(); + + let mut vertex_layout = Layout::new::<()>(); + + for VertexAttrProperties { + name: _, + ty: _, + 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<'name, VertexAttrs: NamedVertexAttrs<'name>>( + &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_attr_name, 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.name == vertex_attr_name) + .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 iter<VertexAttr: VertexAttrValue>( + &self, + vertex_attr_name: &str, + ) -> Iter<'_, VertexAttr> + { + let vertex_attr_props = self + .vertex_attr_props + .iter() + .find(|vertex_attr_props| vertex_attr_props.name == vertex_attr_name) + .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<'name> +{ + fn vertex_attr_cnt(&self) -> usize; + + fn vertex_attrs(&self) -> impl Iterator<Item = (&'name str, &[u8], VertexAttrType)>; +} + +macro_rules! impl_named_vertex_attrs { + ($cnt: tt) => { + seq!(I in 0..$cnt { + impl<'name, #(VertexAttr~I: VertexAttrValue,)*> + NamedVertexAttrs<'name> for (#(NamedVertexAttr<'name, VertexAttr~I>,)*) + { + fn vertex_attr_cnt(&self) -> usize + { + $cnt + } + + fn vertex_attrs(&self) + -> impl Iterator<Item = (&'name str, &[u8], VertexAttrType)> + { + [#( + (self.I.name, 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..ebf623f --- /dev/null +++ b/engine/src/model.rs @@ -0,0 +1,193 @@ +use std::borrow::Cow; +use std::collections::HashMap; + +use ecs::Component; + +use crate::asset::{Assets, Handle as AssetHandle}; +use crate::material::Material; +use crate::material::asset::Map as MaterialAssetMap; +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: Materials, + pub material_names: Vec<Cow<'static, str>>, +} + +impl Spec +{ + pub fn builder() -> SpecBuilder + { + SpecBuilder::default() + } + + pub fn find_first_material<'assets>( + &'assets self, + assets: &'assets Assets, + ) -> MaterialSearchResult<'assets> + { + let Some(material_name) = self.material_names.first() else { + return MaterialSearchResult::NoMaterials; + }; + + let material_asset = match &self.materials { + Materials::Maps(material_asset_map_assets) => material_asset_map_assets + .iter() + .find_map(|mat_asset_map_asset| { + let mat_asset_map = assets.get(mat_asset_map_asset)?; + + mat_asset_map.assets.get(material_name) + }), + Materials::Direct(material_assets) => material_assets.get(material_name), + }; + + let Some(material_asset) = material_asset else { + return MaterialSearchResult::NotFound; + }; + + if assets.get(material_asset).is_none() { + tracing::trace!("Missing material asset"); + return MaterialSearchResult::NotFound; + } + + MaterialSearchResult::Found(material_asset) + } +} + +#[derive(Debug, Default, Clone)] +pub struct SpecBuilder +{ + mesh_asset: Option<AssetHandle<Mesh>>, + materials: Materials, + material_names: Vec<Cow<'static, str>>, +} + +impl SpecBuilder +{ + pub fn mesh(mut self, asset: AssetHandle<Mesh>) -> Self + { + self.mesh_asset = Some(asset); + + self + } + + pub fn materials(mut self, materials: Materials) -> Self + { + self.materials = materials; + + self + } + + pub fn material_name(mut self, material_name: impl Into<Cow<'static, str>>) -> Self + { + self.material_names.push(material_name.into()); + + self + } + + pub fn material_names<MaterialName>( + mut self, + material_names: impl IntoIterator<Item = MaterialName>, + ) -> Self + where + MaterialName: Into<Cow<'static, str>>, + { + self.material_names + .extend(material_names.into_iter().map(|mat_name| mat_name.into())); + + self + } + + #[tracing::instrument(skip_all)] + pub fn build(self) -> Spec + { + if !self.materials.is_empty() && self.material_names.is_empty() { + tracing::warn!("Model spec will have materials but no material names"); + } + + if self.materials.is_empty() && !self.material_names.is_empty() { + tracing::warn!("Model spec will have material names but no materials"); + } + + Spec { + mesh_asset: self.mesh_asset, + materials: self.materials, + material_names: self.material_names, + } + } +} + +#[derive(Debug, Clone)] +pub enum Materials +{ + Direct(HashMap<Cow<'static, str>, AssetHandle<Material>>), + Maps(Vec<AssetHandle<MaterialAssetMap>>), +} + +impl Materials +{ + pub fn direct<MaterialName>( + material_assets: impl IntoIterator<Item = (MaterialName, AssetHandle<Material>)>, + ) -> Self + where + MaterialName: Into<Cow<'static, str>>, + { + Self::Direct( + material_assets + .into_iter() + .map(|(material_name, mat_asset)| (material_name.into(), mat_asset)) + .collect(), + ) + } + + pub fn is_empty(&self) -> bool + { + match self { + Self::Direct(material_assets) => material_assets.is_empty(), + Self::Maps(material_asset_map_assets) => material_asset_map_assets.is_empty(), + } + } + + pub fn len(&self) -> usize + { + match self { + Self::Direct(material_assets) => material_assets.len(), + Self::Maps(material_asset_map_assets) => material_asset_map_assets.len(), + } + } +} + +impl Default for Materials +{ + fn default() -> Self + { + Self::Maps(Vec::new()) + } +} + +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..070200d --- /dev/null +++ b/engine/src/model/asset.rs @@ -0,0 +1,82 @@ +use std::collections::HashSet; +use std::fs::read_to_string; +use std::path::{Path, PathBuf}; + +use crate::asset::{Assets, Submitter as AssetSubmitter}; +use crate::material::asset::Map as MaterialAssetMap; +use crate::model::{Materials, Spec}; + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct Settings {} + +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 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()?; + + let mesh_asset = asset_submitter.submit_store_named("mesh", mesh); + + let mut material_asset_map_assets = + Vec::with_capacity(obj.mtl_libs.iter().flatten().count()); + + for mtl_lib_path in obj.mtl_libs.iter().flatten() { + let mtl_lib_asset = + asset_submitter.submit_load_other::<MaterialAssetMap>(mtl_lib_path.as_path()); + + material_asset_map_assets.push(mtl_lib_asset); + } + + let material_names = obj + .faces + .into_iter() + .map(|face| face.material_name) + .flatten() + .fold( + (HashSet::<String>::new(), Vec::<String>::new()), + |(mut pushed_mat_names, mut unique_mat_names), material_name| { + if pushed_mat_names.contains(&material_name) { + return (pushed_mat_names, unique_mat_names); + } + + unique_mat_names.push(material_name.clone()); + pushed_mat_names.insert(material_name); + + (pushed_mat_names, unique_mat_names) + }, + ) + .1; + + asset_submitter.submit_store( + Spec::builder() + .mesh(mesh_asset) + .materials(Materials::Maps(material_asset_map_assets)) + .material_names(material_names) + .build(), + ); + + Ok(()) +} + +#[derive(Debug, thiserror::Error)] +enum Error +{ + #[error("Failed to read file {}", .1.display())] + ReadFailed(#[source] std::io::Error, PathBuf), + + #[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 68a75fb..0000000 --- a/engine/src/opengl/buffer.rs +++ /dev/null @@ -1,79 +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 - } -} - -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/glsl.rs b/engine/src/opengl/glsl.rs deleted file mode 100644 index 6fd5638..0000000 --- a/engine/src/opengl/glsl.rs +++ /dev/null @@ -1,616 +0,0 @@ -use std::borrow::Cow; -use std::fmt::{Display, Formatter}; -use std::path::{Path, PathBuf}; -use std::string::FromUtf8Error; - -const PREINCLUDE_DIRECTIVE: &str = "#preinclude"; - -pub fn preprocess<'content>( - shader_content: impl Into<Cow<'content, str>>, - read_file: &impl Fn(&Path) -> Result<Vec<u8>, std::io::Error>, -) -> Result<Cow<'content, str>, PreprocessingError> -{ - do_preprocess(shader_content, SpanPath::Original, read_file) -} - -fn do_preprocess<'content>( - shader_content: impl Into<Cow<'content, str>>, - shader_path: SpanPath<'_>, - read_file: &impl Fn(&Path) -> Result<Vec<u8>, std::io::Error>, -) -> Result<Cow<'content, str>, PreprocessingError> -{ - let shader_content = shader_content.into(); - - let mut preincludes = shader_content - .match_indices(PREINCLUDE_DIRECTIVE) - .peekable(); - - if preincludes.peek().is_none() { - // Shader content contains no preincludes - return Ok(shader_content.into()); - }; - - let mut preprocessed = shader_content.to_string(); - - 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 = handle_preinclude( - &preprocessed, - &shader_path, - preinclude_start, - span_line_offset, - )?; - - let path = replacement_job.path.clone(); - - let mut included = - String::from_utf8(read_file(&replacement_job.path).map_err(|err| { - PreprocessingError::ReadIncludedShaderFailed { - source: err, - path: replacement_job.path.clone(), - } - })?) - .map_err(|err| { - PreprocessingError::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 = do_preprocess( - &included, - SpanPath::Path(replacement_job.path.as_path().into()), - read_file, - )?; - - 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.into()) -} - -fn handle_preinclude( - shader_content: &str, - shader_path: &SpanPath<'_>, - preinclude_start_index: usize, - span_line_offset: usize, -) -> Result<ReplacementJob, PreprocessingError> -{ - let expect_token = |token: char, index: usize| { - let token_found = shader_content.chars().nth(index).ok_or_else(|| { - PreprocessingError::ExpectedToken { - expected: token, - span: Span::new( - shader_content, - shader_path.to_owned(), - index, - span_line_offset, - preinclude_start_index, - ), - } - })?; - - if token_found != token { - return Err(PreprocessingError::InvalidToken { - expected: token, - found: token_found, - span: Span::new( - shader_content, - shader_path.to_owned(), - 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(PreprocessingError::ExpectedToken { - expected: '"', - span: Span::new( - shader_content, - shader_path.to_owned(), - 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| { - PreprocessingError::PreincludePathInvalidUtf8 { - source: err, - span: Span::new( - shader_content, - shader_path.to_owned(), - 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, -} - -#[derive(Debug, thiserror::Error)] -pub enum PreprocessingError -{ - #[error( - "Invalid token at line {}, column {} of {}. Expected '{}', found '{}'", - span.line, - span.column, - span.path, - expected, - found - )] - InvalidToken - { - expected: char, - found: char, - span: Span, - }, - - #[error( - "Expected token '{}' at line {}, column {} of {}. Found eof", - expected, - span.line, - span.column, - span.path - )] - ExpectedToken - { - expected: char, span: Span - }, - - #[error( - "Preinclude path at line {}, column {} of {} is invalid UTF-8", - span.line, - span.column, - span.path - )] - 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)] -#[non_exhaustive] -pub struct Span -{ - pub line: usize, - pub column: usize, - pub path: SpanPath<'static>, -} - -impl Span -{ - fn new( - file_content: &str, - path: SpanPath<'static>, - 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, - path, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SpanPath<'a> -{ - Original, - Path(Cow<'a, Path>), -} - -impl<'a> SpanPath<'a> -{ - fn to_owned(&self) -> SpanPath<'static> - { - match self { - Self::Original => SpanPath::Original, - Self::Path(path) => SpanPath::Path(Cow::Owned(path.to_path_buf().into())), - } - } -} - -impl<'a> Display for SpanPath<'a> -{ - fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result - { - match self { - Self::Original => write!(formatter, "original file"), - Self::Path(path) => write!(formatter, "file {}", path.display()), - } - } -} - -impl<'a, PathLike> PartialEq<PathLike> for SpanPath<'a> -where - PathLike: AsRef<Path>, -{ - fn eq(&self, other: &PathLike) -> bool - { - match self { - Self::Original => false, - Self::Path(path) => path == other.as_ref(), - } - } -} - -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; - - use super::{preprocess, PreprocessingError}; - use crate::opengl::glsl::SpanPath; - - #[test] - fn preprocess_no_directives_is_same() - { - assert_eq!( - preprocess("#version 330 core\n", &|_| { unreachable!() }).unwrap(), - "#version 330 core\n" - ); - } - - #[test] - fn preprocess_with_directives_works() - { - assert_eq!( - preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"foo.glsl\"\n", - "\n", - "void main() {}", - ), - &|_| { Ok(b"out vec4 FragColor;".to_vec()) } - ) - .unwrap(), - concat!( - "#version 330 core\n", - "\n", - "out vec4 FragColor;\n", - "\n", - "void main() {}", - ) - ); - - assert_eq!( - preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ), - &|_| { Ok(b"out vec4 FragColor;".to_vec()) } - ) - .unwrap(), - concat!( - "#version 330 core\n", - "\n", - "out vec4 FragColor;\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ) - ); - - assert_eq!( - 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() {}", - ), - &|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()) - } - }, - ) - .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_directive_does_not_work() - { - let res = preprocess( - concat!( - "#version 330 core\n", - "\n", - // Missing " - "#preinclude foo.glsl\"\n", - "\n", - "void main() {}", - ), - &|_| Ok(b"out vec4 FragColor;".to_vec()), - ); - - let Err(PreprocessingError::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.path, SpanPath::Original); - } - - #[test] - fn preprocess_error_has_correct_span() - { - let res = preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "#preinclude \"foo.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ), - &|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 if path == OsStr::new("foo.glsl") { - Ok(concat!( - "uniform sampler2D input_texture;\n", - "\n", - // Missing space before first " - "#preinclude\"shared_types.glsl\"\n", - ) - .as_bytes() - .to_vec()) - } else { - panic!(concat!( - "Expected read function to be called with ", - "either path bar.glsl or foo.glsl" - )); - } - }, - ); - - let Err(PreprocessingError::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, 3); - assert_eq!(span.column, 12); - assert_eq!(span.path, SpanPath::Path(Path::new("foo.glsl").into())); - } - - #[test] - fn preprocess_included_shader_with_include_works() - { - assert_eq!( - preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ), - &|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()) - } - } - ) - .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 = preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ), - &|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()) - } - }, - ); - - let Err(PreprocessingError::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.path, Path::new("bar.glsl")); - } -} diff --git a/engine/src/opengl/mod.rs b/engine/src/opengl/mod.rs deleted file mode 100644 index 53e0120..0000000 --- a/engine/src/opengl/mod.rs +++ /dev/null @@ -1,128 +0,0 @@ -use bitflags::bitflags; -use gl::types::GLint; - -use crate::data_types::dimens::Dimens; -use crate::vector::Vec2; - -pub mod buffer; -pub mod glsl; -pub mod shader; -pub mod texture; -pub mod vertex_array; - -mod util; - -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); - } -} - -pub fn get_context_flags() -> ContextFlags -{ - let mut context_flags: GLint = 0; - - unsafe { - gl::GetIntegerv(gl::CONTEXT_FLAGS as u32, &mut context_flags); - } - - ContextFlags::from_bits_truncate(context_flags 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, - } - } -} - -bitflags! { -#[derive(Debug, Clone, Copy)] -pub struct ContextFlags: u32 { - const FORWARD_COMPATIBLE = gl::CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT; - const DEBUG = gl::CONTEXT_FLAG_DEBUG_BIT; - const ROBUST_ACCESS = gl::CONTEXT_FLAG_ROBUST_ACCESS_BIT; -} -} diff --git a/engine/src/opengl/shader.rs b/engine/src/opengl/shader.rs deleted file mode 100644 index 36dc1a4..0000000 --- a/engine/src/opengl/shader.rs +++ /dev/null @@ -1,247 +0,0 @@ -use std::ffi::CStr; -use std::ptr::null_mut; - -use crate::matrix::Matrix; -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(&mut 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(&mut 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); - } - } -} - -/// Shader kind. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Kind -{ - Vertex, - Fragment, -} - -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(&mut self, shader: &Shader) - { - unsafe { - gl::AttachShader(self.program, shader.shader); - } - } - - pub fn link(&mut 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 52c8554..0000000 --- a/engine/src/opengl/texture.rs +++ /dev/null @@ -1,240 +0,0 @@ -use crate::data_types::dimens::Dimens; -use crate::texture::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_num(num: usize) -> Option<Self> { - match num { - #( - 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 e1e1a15..0000000 --- a/engine/src/opengl/vertex_array.rs +++ /dev/null @@ -1,172 +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) } - } -} - -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/projection.rs b/engine/src/projection.rs index faa741f..46df6d4 100644 --- a/engine/src/projection.rs +++ b/engine/src/projection.rs @@ -1,9 +1,9 @@ -use crate::data_types::dimens::Dimens3; +use crate::builder; +use crate::data_types::dimens::Dimens; use crate::matrix::Matrix; -use crate::util::builder; -use crate::vector::Vec3; +use crate::vector::Vec2; -#[derive(Debug)] +#[derive(Debug, Clone)] #[non_exhaustive] pub enum Projection { @@ -12,7 +12,7 @@ pub enum Projection } /// Perspective projection parameters. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Perspective { pub fov_radians: f32, @@ -25,7 +25,7 @@ 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> + -> Matrix<f32, 4, 4> { let mut out = Matrix::new(); @@ -55,13 +55,23 @@ impl Default for Perspective } } +#[derive(Debug, Clone)] +pub enum OrthographicSize +{ + FixedSize(Dimens<f32>), + WindowSize, +} + builder! { #[builder(name = OrthographicBuilder, derives=(Debug, Clone))] -#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[derive(Debug, Clone)] #[non_exhaustive] pub struct Orthographic { - pub size: Dimens3<f32>, + pub near: f32, + pub far: f32, + pub viewport_origin: Vec2<f32>, + pub size: OrthographicSize, } } @@ -75,18 +85,28 @@ impl Orthographic /// Creates a orthographic projection matrix using right-handed coordinates. pub fn to_matrix_rh( &self, - center_pos: &Vec3<f32>, + window_size: Dimens<f32>, clip_volume: ClipVolume, ) -> Matrix<f32, 4, 4> { let mut result = Matrix::<f32, 4, 4>::new(); - let left = center_pos.x - (self.size.width / 2.0); - let right = center_pos.x + (self.size.width / 2.0); - let bottom = center_pos.y - (self.size.height / 2.0); - let top = center_pos.y + (self.size.height / 2.0); - let near = center_pos.z - (self.size.depth / 2.0); - let far = center_pos.z + (self.size.depth / 2.0); + let size = match self.size { + OrthographicSize::FixedSize(fixed_size) => fixed_size, + OrthographicSize::WindowSize => window_size, + }; + + 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; match clip_volume { ClipVolume::NegOneToOne => { @@ -108,13 +128,7 @@ impl Default for Orthographic { fn default() -> Self { - Self { - size: Dimens3 { - width: 10.0, - height: 7.0, - depth: 10.0, - }, - } + Self::builder().build() } } @@ -122,9 +136,12 @@ impl Default for OrthographicBuilder { fn default() -> Self { - let orthographic = Orthographic::default(); - - OrthographicBuilder { size: orthographic.size } + Self { + near: 0.0, + far: 1000.0, + viewport_origin: Vec2 { x: 0.5, y: 0.5 }, + size: OrthographicSize::WindowSize, + } } } diff --git a/engine/src/reflection.rs b/engine/src/reflection.rs new file mode 100644 index 0000000..429946b --- /dev/null +++ b/engine/src/reflection.rs @@ -0,0 +1,205 @@ +use std::alloc::Layout; +use std::any::{TypeId, type_name}; +use std::fmt::Debug; + +pub use engine_macros::Reflection; + +/// Trait implemented by types that support runtime reflection on them. +/// +/// # Safety +/// Implementors of this trait must provide accurate reflection information in the +/// `TYPE_REFLECTION` associated constant and the `type_reflection` and +/// `get_type_reflection` methods. +pub unsafe trait Reflection: 'static +{ + const TYPE_REFLECTION: &Type; + + fn type_reflection() -> &'static Type + where + Self: Sized, + { + Self::TYPE_REFLECTION + } + + fn get_type_reflection(&self) -> &'static Type + { + Self::TYPE_REFLECTION + } +} + +/// Trait implemented by enums that support runtime reflection on them. +/// +/// # Safety +/// Implementors of this trait must provide accurate reflection information in the +/// `get_variant_reflection` method. +pub unsafe trait EnumReflectionExt: Reflection +{ + fn get_variant_reflection(&self) -> &'static EnumVariant; +} + +#[derive(Debug)] +#[non_exhaustive] +pub enum Type +{ + Struct(Struct), + Enum(Enum), + Array(Array), + Slice(Slice), + Literal(Literal), +} + +impl Type +{ + pub const fn as_struct(&self) -> Option<&Struct> + { + match self { + Self::Struct(struct_reflection) => Some(struct_reflection), + _ => None, + } + } + + pub const fn as_enum(&self) -> Option<&Enum> + { + match self { + Self::Enum(enum_reflection) => Some(enum_reflection), + _ => None, + } + } +} + +#[derive(Debug, Clone)] +pub struct Struct +{ + pub fields: &'static [StructField], +} + +#[derive(Debug, Clone)] +pub struct StructField +{ + pub name: &'static str, + pub index: usize, + pub layout: Layout, + pub byte_offset: usize, + pub type_id: TypeId, + pub type_name: &'static str, + pub get_type: FnWithDebug<Option<&'static Type>>, +} + +impl StructField +{ + pub fn type_reflection(&self) -> Option<&'static Type> + { + self.get_type.get() + } +} + +#[derive(Debug, Clone)] +pub struct Enum +{ + /// Enum variants in the same order as in the enum definition. + pub variants: &'static [EnumVariant], +} + +#[derive(Debug, Clone)] +pub struct EnumVariant +{ + pub name: &'static str, +} + +#[derive(Debug, Clone)] +pub struct Array +{ + pub item_reflection: &'static Type, + pub length: usize, +} + +#[derive(Debug, Clone)] +pub struct Slice +{ + pub item_reflection: &'static Type, +} + +#[derive(Debug)] +pub struct Literal +{ + pub layout: Layout, + pub type_id: TypeId, + pub type_name: fn() -> &'static str, +} + +macro_rules! impl_with_for_literals { + ($($literal: ty),*) => { + $( + unsafe impl Reflection for $literal + { + const TYPE_REFLECTION: &Type = &Type::Literal(Literal { + layout: Layout::new::<$literal>(), + type_id: TypeId::of::<$literal>(), + type_name: || type_name::<$literal>() + }); + } + )* + }; +} + +impl_with_for_literals!( + u8, + i8, + u16, + i16, + u32, + i32, + u64, + i64, + u128, + i128, + f32, + f64, + usize, + isize, + &'static str +); + +unsafe impl<T: Reflection, const LEN: usize> Reflection for [T; LEN] +{ + const TYPE_REFLECTION: &Type = &Type::Array(Array { + item_reflection: T::TYPE_REFLECTION, + length: LEN, + }); +} + +unsafe impl<T: Reflection> Reflection for &'static [T] +{ + const TYPE_REFLECTION: &Type = + &Type::Slice(Slice { item_reflection: T::TYPE_REFLECTION }); +} + +#[derive(Clone)] +pub struct FnWithDebug<Value> +{ + func: fn() -> Value, +} + +impl<Value> FnWithDebug<Value> +{ + pub const fn new(func: fn() -> Value) -> Self + { + Self { func } + } + + pub fn get(&self) -> Value + { + (self.func)() + } +} + +impl<Value: Debug> Debug for FnWithDebug<Value> +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + formatter + .debug_tuple("FnWithDebug") + .field(&self.get()) + .finish() + } +} diff --git a/engine/src/renderer.rs b/engine/src/renderer.rs index 2544919..15a2571 100644 --- a/engine/src/renderer.rs +++ b/engine/src/renderer.rs @@ -1 +1,546 @@ +use std::collections::VecDeque; +use std::sync::atomic::{AtomicU64, Ordering}; + +use bitflags::bitflags; +use ecs::actions::Actions; +use ecs::component::local::Local; +use ecs::event::component::{Changed, EventMatchExt, Removed}; +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{POST_UPDATE as POST_UPDATE_PHASE, Phase}; +use ecs::query::term::With; +use ecs::sole::Single; +use ecs::system::Into; +use ecs::system::initializable::Initializable; +use ecs::system::observer::Observe; +use ecs::{Component, Query, Sole, declare_entity}; +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::mesh::Mesh; +use crate::renderer::blending::Config as BlendingConfig; +use crate::renderer::object::{Id as ObjectId, Store as ObjectStore}; +use crate::shader::Program as ShaderProgram; +use crate::shader::cursor::{ + BindingLocation as ShaderBindingLocation, + BindingValue as ShaderBindingValue, + Cursor as ShaderCursor, +}; +use crate::texture::Texture; +use crate::vector::Vec2; +use crate::windowing::window::Window; + +pub mod blending; +pub mod main_render_pass; +pub mod object; pub mod opengl; + +static NEXT_SURFACE_ID: AtomicU64 = AtomicU64::new(0); + +declare_entity!( + pub PRE_RENDER_PHASE, + ( + Phase, + Pair::builder() + .relation::<ChildOf>() + .target_id(*POST_UPDATE_PHASE) + .build() + ) +); + +declare_entity!( + pub RENDER_PHASE, + ( + Phase, + Pair::builder() + .relation::<ChildOf>() + .target_id(*PRE_RENDER_PHASE) + .build() + ) +); + +declare_entity!( + pub POST_RENDER_PHASE, + (Phase, Pair::builder().relation::<ChildOf>().target_id(*RENDER_PHASE).build()) +); + +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 ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: ecs::extension::Collector<'_>) + { + collector.add_declared_entity(&PRE_RENDER_PHASE); + collector.add_declared_entity(&RENDER_PHASE); + collector.add_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_passes); + + collector.add_system( + *RENDER_PHASE, + enqueue_commands_from_render_passes + .into_system() + .initialize((ActiveDrawProperties::default(),)), + ); + + collector.add_observer(handle_window_changed); + collector.add_observer(handle_window_removed); + + opengl::Extension::default().collect(collector); + } +} + +impl Default for Extension +{ + fn default() -> Self + { + Self::builder().build() + } +} + +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(8), + debug: false, + depth_test: true, + } + } +} + +#[derive(Debug, Component)] +pub struct SurfaceSpec +{ + pub id: SurfaceId, +} + +#[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 surface_id: SurfaceId, + pub commands: Vec<Command>, + pub draw_properties: DrawProperties, +} + +#[derive(Debug, Reflection)] +#[non_exhaustive] +pub enum Command +{ + RemoveSurface(SurfaceId), + MakeCurrent(SurfaceId), + SetSurfaceSize(SurfaceId, Dimens<u32>), + ClearBuffers(BufferClearMask), + SwapBuffers(SurfaceId), + CreateShaderProgram(ObjectId, ShaderProgram), + ActivateShader(ObjectId), + SetShaderBinding(ShaderBindingLocation, ShaderBindingValue), + CreateTexture(AssetHandle<Texture>), + CreateMesh + { + obj_id: ObjectId, + + /// Optional mesh data. Must be included if `obj_id` is [`ObjectId::Sequential`]. + /// If `obj_id` is [`ObjectId::Asset`], this mesh data will be used instead of + /// the mesh data stored in the asset. + mesh: Option<Mesh>, + + usage: MeshUsage, + }, + UpdateMesh + { + obj_id: ObjectId, + mesh: Mesh, + usage: MeshUsage, + }, + RemoveMesh(ObjectId), + DrawMesh(ObjectId, DrawMeshOptions), + UpdateDrawProperties(DrawProperties, DrawPropertiesUpdateFlags), +} + +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)] + 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, 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 scissor_test_enabled: bool, + pub scissor_box: ScissorBox, + pub face_culling_enabled: bool, +} + +impl Default for DrawProperties +{ + fn default() -> Self + { + Self { + polygon_mode_config: PolygonModeConfig::default(), + blending_enabled: false, + blending_config: BlendingConfig::default(), + depth_test_enabled: true, + scissor_test_enabled: false, + scissor_box: ScissorBox::default(), + face_culling_enabled: false, + } + } +} + +bitflags! { + #[derive(Debug, Clone, Copy)] + pub struct DrawPropertiesUpdateFlags: usize + { + const POLYGON_MODE_CONFIG = 1 << 0; + const BLENDING_CONFIG = 1 << 1; + const BLENDING_ENABLED = 1 << 2; + const DEPTH_TEST_ENABLED = 1 << 3; + const SCISSOR_TEST_ENABLED = 1 << 4; + const SCISSOR_BOX = 1 << 5; + const FACE_CULLING_ENABLED = 1 << 6; + } +} + +/// Renderer command FIFO queue. +/// +/// This component is present in renderer context entities. +#[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)] +pub fn enqueue_commands_from_render_passes( + window_surface_spec_query: Query<(&SurfaceSpec,), (With<Window>,)>, + mut command_queue: Single<CommandQueue>, + mut render_passes: Single<RenderPasses>, + mut active_draw_props: Local<ActiveDrawProperties>, +) +{ + let mut last_render_pass_draw_props = active_draw_props.draw_properties.clone(); + + let mut last_surface_id: Option<SurfaceId> = None; + + for render_pass in render_passes.passes.drain(..) { + if last_surface_id + .is_none_or(|last_surface_id| last_surface_id != render_pass.surface_id) + { + command_queue.push(Command::MakeCurrent(render_pass.surface_id)); + + last_surface_id = Some(render_pass.surface_id); + } + + 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_spec,) in &window_surface_spec_query { + command_queue.push(Command::SwapBuffers(window_surface_spec.id)); + } +} + +#[tracing::instrument(skip_all)] +fn handle_window_changed( + observe: Observe<Pair<Changed, Window>>, + mut command_queue: Single<CommandQueue>, +) +{ + for evt_match in &observe { + let window_ent = evt_match.get_entity(); + + let Some(window_surface_spec) = window_ent.get::<SurfaceSpec>() else { + continue; + }; + + let window = evt_match.get_ent_target_comp(); + + tracing::debug!( + window_id=?window.wid(), + window_title=&*window.title, + "Handling potential resize of window" + ); + + command_queue.queue.push_front(Command::SetSurfaceSize( + window_surface_spec.id, + evt_match.get_ent_target_comp().inner_size().clone(), + )); + + command_queue + .queue + .push_front(Command::MakeCurrent(window_surface_spec.id)); + } +} + +#[tracing::instrument(skip_all)] +fn handle_window_removed( + observe: Observe<Pair<Removed, Window>>, + mut command_queue: Single<CommandQueue>, + mut actions: Actions, +) +{ + for evt_match in &observe { + let window_ent_id = evt_match.entity_id(); + + let window_ent = evt_match.get_entity(); + + tracing::debug!( + entity_id = %window_ent_id, + title = %evt_match.get_ent_target_comp().title, + "Handling removal of window" + ); + + let Some(window_surface_spec) = window_ent.get::<SurfaceSpec>() else { + continue; + }; + + actions.remove_comps::<(SurfaceSpec,)>(window_ent_id); + + command_queue + .queue + .push_front(Command::RemoveSurface(window_surface_spec.id)); + } +} + +// TODO: Maybe move this struct to somewhere more appropriate +#[derive(Default, Clone, Component)] +pub struct PendingShaderBindings +{ + pub bindings: Vec<(ShaderBindingLocation, ShaderBindingValue)>, + pub surface_specific_bindings: + Vec<(SurfaceId, ShaderBindingLocation, ShaderBindingValue)>, +} + +impl<'a> Extend<(ShaderCursor<'a>, ShaderBindingValue)> for PendingShaderBindings +{ + fn extend<Iter: IntoIterator<Item = (ShaderCursor<'a>, ShaderBindingValue)>>( + &mut self, + iter: Iter, + ) + { + self.bindings.extend(iter.into_iter().map( + |(shader_cursor, shader_binding_val)| { + (shader_cursor.into_binding_location(), shader_binding_val) + }, + )) + } +} + +#[derive(Debug, Default, Clone, Component)] +struct ActiveDrawProperties +{ + pub draw_properties: DrawProperties, +} diff --git a/engine/src/renderer/blending.rs b/engine/src/renderer/blending.rs new file mode 100644 index 0000000..9ae2f82 --- /dev/null +++ b/engine/src/renderer/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/renderer/main_render_pass.rs b/engine/src/renderer/main_render_pass.rs new file mode 100644 index 0000000..7492379 --- /dev/null +++ b/engine/src/renderer/main_render_pass.rs @@ -0,0 +1,241 @@ +use ecs::Query; +use ecs::query::term::{With, Without}; +use ecs::sole::Single; + +use crate::asset::Assets; +use crate::draw_flags::{DrawFlags, NoDraw, PolygonModeConfig}; +use crate::model::{MaterialSearchResult, Model}; +use crate::renderer::object::{Id as RendererObjectId, Store as RendererObjectStore}; +use crate::renderer::{ + BufferClearMask as RendererBufferClearMask, + Command as RendererCommand, + DrawMeshOptions as RendererDrawMeshOptions, + DrawProperties as RendererDrawProperties, + DrawPropertiesUpdateFlags as RendererDrawPropertiesUpdateFlags, + MeshUsage as RendererMeshUsage, + PendingShaderBindings, + RenderPass, + RenderPasses as RendererRenderPasses, + SurfaceSpec, +}; +use crate::shader::default::ASSET_LABEL as DEFAULT_SHADER_ASSET_LABEL; +use crate::shader::{ + Context as ShaderContext, + ModuleSource as ShaderModuleSource, + Shader, +}; +use crate::texture::{Texture, WHITE_1X1_ASSET_LABEL as TEXTURE_WHITE_1X1_ASSET_LABEL}; +use crate::windowing::window::Window; + +type RenderableEntity<'a> = ( + &'a Model, + Option<&'a DrawFlags>, + Option<&'a Shader>, + Option<&'a mut PendingShaderBindings>, +); + +#[tracing::instrument(skip_all)] +pub fn add_main_render_passes( + renderable_query: Query<RenderableEntity<'_>, (Without<NoDraw>,)>, + window_surface_spec_query: Query<(&SurfaceSpec,), (With<Window>,)>, + assets: Single<Assets>, + shader_context: Single<ShaderContext>, + mut render_passes: Single<RendererRenderPasses>, + mut object_store: Single<RendererObjectStore>, +) +{ + let Some(default_shader_asset) = assets + .get_handle_to_loaded::<ShaderModuleSource>(DEFAULT_SHADER_ASSET_LABEL.clone()) + else { + tracing::error!("Default shader asset is not loaded"); + return; + }; + + for (surface_spec,) in &window_surface_spec_query { + let render_pass = render_passes.passes.push_front_mut(RenderPass { + surface_id: surface_spec.id, + commands: Vec::with_capacity(30), + draw_properties: RendererDrawProperties::default(), + }); + + let default_texture_asset = assets + .get_handle_to_loaded::<Texture>(TEXTURE_WHITE_1X1_ASSET_LABEL.clone()) + .expect("Not possible"); + + if !object_store.contains_maybe_pending_with_id(&RendererObjectId::Asset( + default_texture_asset.id(), + )) { + object_store + .insert_pending(RendererObjectId::Asset(default_texture_asset.id())); + + render_pass + .commands + .push(RendererCommand::CreateTexture(default_texture_asset)); + } + + render_pass.commands.push(RendererCommand::ClearBuffers( + RendererBufferClearMask::COLOR | RendererBufferClearMask::DEPTH, + )); + + for (model, draw_flags, shader, mut pending_shader_bindings) in &renderable_query + { + let shader_asset = match &shader { + Some(shader) => &shader.asset_handle, + None => &default_shader_asset, + }; + + let Some(pending_shader_bindings) = pending_shader_bindings.as_mut() else { + continue; + }; + + if pending_shader_bindings.bindings.is_empty() + && pending_shader_bindings.surface_specific_bindings.is_empty() + { + continue; + } + + 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; + } + + debug_assert!(model_spec.material_names.len() <= 1); + + let model_material_asset = match model_spec.find_first_material(&assets) { + MaterialSearchResult::Found(model_material_asset) => { + model_material_asset.clone() + // Some(model_material_asset.clone()) + } + MaterialSearchResult::NotFound | MaterialSearchResult::NoMaterials => { + // MaterialSearchResult::NotFound => { + continue; + } // MaterialSearchResult::NoMaterials => None, + }; + + if !object_store.contains_maybe_pending_with_id(&RendererObjectId::Asset( + shader_asset.id(), + )) { + let Some(shader_program) = shader_context.get_program(&shader_asset.id()) + else { + tracing::error!( + "Shader context doesn't have a program for shader asset {:?}", + assets.get_label(&shader_asset) + ); + continue; + }; + + object_store.insert_pending(RendererObjectId::Asset(shader_asset.id())); + + render_pass + .commands + .push(RendererCommand::CreateShaderProgram( + RendererObjectId::Asset(shader_asset.id()), + shader_program.clone(), + )); + } + + render_pass.commands.push(RendererCommand::ActivateShader( + RendererObjectId::Asset(shader_asset.id()), + )); + + let Some(model_material) = assets.get(&model_material_asset) else { + // TODO: Handle this case since it may occur + unreachable!(); + }; + + for texture_asset in [ + &model_material.ambient_map, + &model_material.diffuse_map, + &model_material.specular_map, + ] + .into_iter() + .flatten() + { + if !object_store.contains_maybe_pending_with_id(&RendererObjectId::Asset( + texture_asset.id(), + )) { + object_store + .insert_pending(RendererObjectId::Asset(texture_asset.id())); + + render_pass + .commands + .push(RendererCommand::CreateTexture(texture_asset.clone())); + } + } + + for (shader_binding_loc, shader_binding_val) in + &pending_shader_bindings.bindings + { + render_pass.commands.push(RendererCommand::SetShaderBinding( + shader_binding_loc.clone(), + shader_binding_val.clone(), + )); + } + + for (shader_binding_surface_id, shader_binding_loc, shader_binding_val) in + &pending_shader_bindings.surface_specific_bindings + { + if *shader_binding_surface_id != surface_spec.id { + continue; + } + + render_pass.commands.push(RendererCommand::SetShaderBinding( + shader_binding_loc.clone(), + shader_binding_val.clone(), + )); + } + + if let Some(draw_flags) = draw_flags.as_deref() + && draw_flags.polygon_mode_config != PolygonModeConfig::default() + { + render_pass + .commands + .push(RendererCommand::UpdateDrawProperties( + RendererDrawProperties { + polygon_mode_config: draw_flags.polygon_mode_config.clone(), + ..Default::default() + }, + RendererDrawPropertiesUpdateFlags::POLYGON_MODE_CONFIG, + )); + } + + if !object_store + .contains_maybe_pending_with_id(&RendererObjectId::Asset(mesh_asset.id())) + { + object_store.insert_pending(RendererObjectId::Asset(mesh_asset.id())); + + render_pass.commands.push(RendererCommand::CreateMesh { + obj_id: RendererObjectId::Asset(mesh_asset.id()), + mesh: None, + usage: RendererMeshUsage::Static, + }); + } + + render_pass.commands.push(RendererCommand::DrawMesh( + RendererObjectId::Asset(mesh_asset.id()), + RendererDrawMeshOptions::default(), + )); + + if let Some(draw_flags) = draw_flags.as_deref() + && draw_flags.polygon_mode_config != PolygonModeConfig::default() + { + render_pass + .commands + .push(RendererCommand::UpdateDrawProperties( + RendererDrawProperties { + polygon_mode_config: PolygonModeConfig::default(), + ..Default::default() + }, + RendererDrawPropertiesUpdateFlags::POLYGON_MODE_CONFIG, + )); + } + } + } +} diff --git a/engine/src/renderer/object.rs b/engine/src/renderer/object.rs new file mode 100644 index 0000000..bdff885 --- /dev/null +++ b/engine/src/renderer/object.rs @@ -0,0 +1,137 @@ +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; + +use ecs::Sole; + +use crate::asset::Id as AssetId; + +pub type RawValue = u32; + +/// Renderer 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, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SequentialId(u64); + +/// Renderer 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 get_texture_obj(&self, id: &Id) -> Option<&Object> + { + let obj = self.get_obj(id)?; + + if !matches!(obj.kind(), Kind::Texture) { + return None; + } + + Some(obj) + } + + pub fn get_shader_program_obj(&self, id: &Id) -> Option<&Object> + { + let obj = self.get_obj(id)?; + + if !matches!(obj.kind(), Kind::ShaderProgram) { + return None; + } + + Some(obj) + } + + 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) + } +} + +/// Renderer 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 + } +} + +/// Renderer object kind. +#[derive(Debug, Clone, Copy)] +#[non_exhaustive] +pub enum Kind +{ + Texture, + ShaderProgram, + ImplementationSpecific, +} diff --git a/engine/src/renderer/opengl.rs b/engine/src/renderer/opengl.rs index 5665860..378a89d 100644 --- a/engine/src/renderer/opengl.rs +++ b/engine/src/renderer/opengl.rs @@ -1,685 +1,1338 @@ //! OpenGL renderer. +use std::borrow::Cow; use std::collections::HashMap; -use std::ffi::{c_void, CString}; -use std::io::{Error as IoError, ErrorKind as IoErrorKind}; -use std::ops::Deref; -use std::path::Path; -use std::process::abort; use ecs::actions::Actions; -use ecs::component::local::Local; -use ecs::phase::{PRESENT as PRESENT_PHASE, START as START_PHASE}; use ecs::query::term::Without; 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::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}; -use crate::opengl::debug::{ - enable_debug_output, - set_debug_message_callback, - set_debug_message_control, +use ecs::{Component, Query, Sole}; +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 opengl_bindings::blending::{ + Configuration as GlBlendingConfig, + Equation as GlBlendingEquation, + Factor as GlBlendingFactor, + configure as gl_blending_configure, +}; +use opengl_bindings::debug::{ MessageIdsAction, MessageSeverity, MessageSource, MessageType, + SetDebugMessageControlError as GlSetDebugMessageControlError, + set_debug_message_callback, + set_debug_message_control, }; -use crate::opengl::glsl::{ - preprocess as glsl_preprocess, - PreprocessingError as GlslPreprocessingError, +use opengl_bindings::misc::{ + BufferClearMask as GlBufferClearMask, + Capability, + clear_buffers, + define_scissor_box as gl_define_scissor_box, + enable, + get_viewport as gl_get_viewport, + set_enabled, + set_viewport as gl_set_viewport, }; -use crate::opengl::shader::{ +use opengl_bindings::shader::{ Error as GlShaderError, Kind as ShaderKind, Program as GlShaderProgram, Shader as GlShader, + // UniformLocation as GlUniformLocation, }; -use crate::opengl::texture::{ - set_active_texture_unit, +use opengl_bindings::texture::{ + ColorSpace as GlTextureColorSpace, + Filtering as GlTextureFiltering, + GenerateError as GlTextureGenerateError, + PixelDataFormat as GlTexturePixelDataFormat, Texture as GlTexture, - TextureUnit, + Wrapping as GlTextureWrapping, }; -use crate::opengl::vertex_array::{ - DataType as VertexArrayDataType, +use opengl_bindings::vertex_array::{ + DrawError as GlDrawError, PrimitiveKind, VertexArray, }; -use crate::opengl::{ - clear_buffers, - enable, - get_context_flags as get_opengl_context_flags, +use opengl_bindings::{ + ContextWithFns, + CurrentContextWithFns, + MakeContextCurrentError as GlMakeContextCurrentError, +}; +use raw_window_handle::WindowHandle; +use safer_ffi::layout::ReprC; +use zerocopy::{Immutable, IntoBytes}; + +use crate::asset::{Assets, Handle as AssetHandle}; +use crate::data_types::dimens::Dimens; +use crate::image::{ColorType as ImageColorType, Image}; +use crate::matrix::Matrix; +use crate::reflection::EnumReflectionExt; +use crate::renderer::blending::{Equation as BlendingEquation, Factor as BlendingFactor}; +use crate::renderer::object::{ + Id as RendererObjectId, + Kind as RendererObjectKind, + Object as RendererObject, + RawValue as RendererObjectRawValue, + Store as RendererObjectStore, +}; +use crate::renderer::opengl::glutin_compat::{ + DisplayBuilder, + Error as GlutinCompatError, +}; +use crate::renderer::opengl::graphics_mesh::GraphicsMesh; +use crate::renderer::{ BufferClearMask, - Capability, - ContextFlags, + Command as RendererCommand, + CommandQueue as RendererCommandQueue, + DrawMeshOptions, + DrawPropertiesUpdateFlags, + GraphicsProperties, + POST_RENDER_PHASE, + RENDER_PHASE, + SurfaceId, + SurfaceSpec, +}; +use crate::shader::cursor::BindingValue as ShaderBindingValue; +use crate::shader::{ + Context as ShaderContext, + Error as ShaderError, + Program as ShaderProgram, + Stage as ShaderStage, }; -use crate::projection::{ClipVolume, Projection}; -use crate::texture::{Id as TextureId, Texture}; -use crate::transform::{Position, Scale}; -use crate::util::{defer, Defer, RefOrValue}; +use crate::texture::{ + Filtering as TextureFiltering, + Properties as TextureProperties, + Texture, + Wrapping as TextureWrapping, +}; +use crate::util::OptionExt; use crate::vector::{Vec2, Vec3}; -use crate::vertex::{AttributeComponentType, Vertex}; -use crate::window::Window; - -type RenderableEntity<'a> = ( - &'a Mesh, - &'a Material, - &'a Option<MaterialFlags>, - &'a Option<Position>, - &'a Option<Scale>, - &'a Option<DrawFlags>, - &'a Option<GlObjects>, -); +use crate::windowing::Context as WindowingContext; +use crate::windowing::window::{ + Closed as WindowClosed, + CreationAttributes as WindowCreationAttributes, + CreationReady, + Window, +}; -#[derive(Debug, Default)] -#[non_exhaustive] -pub struct Extension {} +mod glutin_compat; +mod graphics_mesh; -impl ecs::extension::Extension for Extension +#[derive(Debug, Component)] +struct WindowGlConfig { - fn collect(self, mut collector: ecs::extension::Collector<'_>) - { - collector.add_system(*START_PHASE, initialize); - - collector.add_system( - *PRESENT_PHASE, - render - .into_system() - .initialize((GlobalGlObjects::default(),)), - ); - } + gl_config: GlutinConfig, } -fn initialize(window: Single<Window>) +#[derive(Sole, Default)] +struct GraphicsContext { - 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}", - ); + gl_context: Option<ContextWithFns>, + surfaces: HashMap<SurfaceId, GraphicsContextSurface>, + shader_uniform_buffer_objs: + HashMap<RendererObjectId, HashMap<u32, opengl_bindings::buffer::Buffer<u8>>>, + objects: HashMap<RendererObjectRawValue, GraphicsContextObject>, + next_object_key: RendererObjectRawValue, +} - abort(); - } - }); +#[derive(Debug)] +struct GraphicsContextSurface +{ + window_surface: GlutinSurface<GlutinWindowSurface>, + size: Dimens<u32>, +} - if get_opengl_context_flags().contains(ContextFlags::DEBUG) { - initialize_debug(); - } +#[derive(Debug)] +enum GraphicsContextObject +{ + Mesh + { + mesh: GraphicsMesh, + compatible_shader_program_obj_id: RendererObjectId, + }, +} - let window_size = window.size().expect("Failed to get window size"); +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct Extension {} - set_viewport(Vec2 { x: 0, y: 0 }, window_size); +impl ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: ecs::extension::Collector<'_>) + { + collector.add_system(*RENDER_PHASE, handle_commands); - window.set_framebuffer_size_callback(|new_window_size| { - set_viewport(Vec2::ZERO, new_window_size); - }); + collector.add_system(*POST_RENDER_PHASE, prepare_windows); + collector.add_system(*POST_RENDER_PHASE, init_window_graphics); - enable(Capability::DepthTest); - enable(Capability::MultiSample); + let _ = collector.add_sole(GraphicsContext::default()); + } } -#[allow(clippy::too_many_arguments)] -fn render( - query: Query<RenderableEntity<'_>, (Without<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>, +fn prepare_windows( + window_query: Query< + (Option<&Window>, &mut WindowCreationAttributes), + ( + Without<CreationReady>, + Without<WindowGlConfig>, + Without<WindowClosed>, + ), + >, + windowing_context: Single<WindowingContext>, + graphics_props: Single<GraphicsProperties>, mut actions: Actions, ) { - let Some((camera, camera_pos, _)) = camera_query.iter().next() else { - tracing::warn!("No current camera. Nothing will be rendered"); + let Some(display_handle) = windowing_context.display_handle() else { 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_program, - textures: gl_textures, - } = &mut *gl_objects; - - let shader_program = - shader_program.get_or_insert_with(|| create_default_shader_program().unwrap()); - - clear_buffers(BufferClearMask::COLOR | BufferClearMask::DEPTH); - - for ( - euid, - (mesh, material, material_flags, position, scale, draw_flags, gl_objects), - ) in query.iter_with_euids() + for (window_ent_id, (window, mut window_creation_attrs)) in + window_query.iter_with_euids() { - let material_flags = material_flags - .map(|material_flags| material_flags.clone()) - .unwrap_or_default(); - - let gl_objs = match gl_objects.as_deref() { - Some(gl_objs) => RefOrValue::Ref(gl_objs), - None => RefOrValue::Value(Some(GlObjects::new(&mesh))), - }; - - defer!(|gl_objs| { - if let RefOrValue::Value(opt_gl_objs) = gl_objs { - actions.add_components(euid, (opt_gl_objs.take().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"), - ); + tracing::debug!("Preparing window entity {window_ent_id} for use in rendering"); - 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 (index, texture) in material.textures.iter().enumerate() { - let gl_texture = gl_textures - .entry(texture.id()) - .or_insert_with(|| create_gl_texture(texture)); - - let texture_unit = TextureUnit::from_num(index).expect("Too many textures"); + let mut glutin_config_template_builder = + glutin::config::ConfigTemplateBuilder::new(); - set_active_texture_unit(texture_unit); - - gl_texture.bind(); + if let Some(multisampling_sample_cnt) = graphics_props.multisampling_sample_cnt { + glutin_config_template_builder = glutin_config_template_builder + .with_multisampling(multisampling_sample_cnt); } - shader_program.activate(); + 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; + } + }; - if let Some(draw_flags) = &draw_flags { - crate::opengl::set_polygon_mode( - draw_flags.polygon_mode_config.face, - draw_flags.polygon_mode_config.mode, - ); - } + 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; + } + }; - draw_mesh(gl_objs.get().unwrap()); + *window_creation_attrs = new_window_creation_attrs; - if draw_flags.is_some() { - let default_polygon_mode_config = PolygonModeConfig::default(); + actions.add_components(window_ent_id, (WindowGlConfig { gl_config },)); - crate::opengl::set_polygon_mode( - default_polygon_mode_config.face, - default_polygon_mode_config.mode, - ); + if window.is_none() { + actions.add_components(window_ent_id, (CreationReady,)); } } } -#[derive(Debug, Default, Component)] -struct GlobalGlObjects -{ - shader_program: Option<GlShaderProgram>, - textures: HashMap<TextureId, GlTexture>, -} - -fn set_viewport(position: Vec2<u32>, size: Dimens<u32>) -{ - crate::opengl::set_viewport(position, size); -} - -fn initialize_debug() +#[tracing::instrument(skip_all)] +fn init_window_graphics( + window_query: Query<(&Window, &WindowGlConfig), (Without<SurfaceSpec>,)>, + windowing_context: Single<WindowingContext>, + graphics_props: Single<GraphicsProperties>, + mut graphics_ctx: Single<GraphicsContext>, + mut actions: Actions, +) { - enable_debug_output(); - - set_debug_message_callback(opengl_debug_message_cb); - set_debug_message_control(None, None, None, &[], MessageIdsAction::Disable); -} + 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" + ); -fn draw_mesh(gl_objects: &GlObjects) -{ - gl_objects.vertex_arr.bind(); + 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; + } + }; - if gl_objects.index_buffer.is_some() { - VertexArray::draw_elements(PrimitiveKind::Triangles, 0, gl_objects.element_cnt); - } else { - VertexArray::draw_arrays(PrimitiveKind::Triangles, 0, gl_objects.element_cnt); - } -} + let Some(window_inner_size) = window.inner_size().try_into_nonzero() else { + tracing::error!( + "Cannot create a surface for a window with a width/height of 0", + ); + continue; + }; -fn create_gl_texture(texture: &Texture) -> GlTexture -{ - let mut gl_texture = GlTexture::new(); + 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; + } + }; - gl_texture.generate( - *texture.dimensions(), - texture.image().as_bytes(), - texture.pixel_data_format(), - ); + 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; + } + }; - gl_texture.apply_properties(texture.properties()); + let Ok(curr_gl_context) = gl_context.make_current(&window_surface) else { + tracing::error!("Failed to make GL context current"); + continue; + }; - gl_texture -} + if let Err(err) = gl_set_viewport( + &curr_gl_context, + &Vec2 { x: 0, y: 0 }.into(), + &window.inner_size().clone().into(), + ) { + tracing::error!("Failed to set viewport: {err}"); + } -const VERTEX_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/vertex.glsl"); -const FRAGMENT_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/fragment.glsl"); + set_enabled( + &curr_gl_context, + Capability::DepthTest, + graphics_props.depth_test, + ); -const VERTEX_DATA_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/vertex_data.glsl"); -const LIGHT_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/light.glsl"); + set_enabled( + &curr_gl_context, + Capability::MultiSample, + graphics_props.multisampling_sample_cnt.is_some(), + ); -fn create_default_shader_program() -> Result<GlShaderProgram, CreateShaderError> -{ - let mut vertex_shader = GlShader::new(ShaderKind::Vertex); + if graphics_props.debug { + enable(&curr_gl_context, Capability::DebugOutput); + enable(&curr_gl_context, Capability::DebugOutputSynchronous); + + set_debug_message_callback(&curr_gl_context, opengl_debug_message_cb); + + match set_debug_message_control( + &curr_gl_context, + None, + None, + None, + &[], + MessageIdsAction::Disable, + ) { + Ok(()) => {} + Err(GlSetDebugMessageControlError::TooManyIds { + id_cnt: _, + max_id_cnt: _, + }) => { + unreachable!() // No ids are given + } + } + } - vertex_shader.set_source(&*glsl_preprocess( - VERTEX_GLSL_SHADER_SRC, - &get_glsl_shader_content, - )?)?; + let surface_id = SurfaceId::new_unique(); - vertex_shader.compile()?; + actions.add_components(window_ent_id, (SurfaceSpec { id: surface_id },)); - let mut fragment_shader = GlShader::new(ShaderKind::Fragment); + graphics_ctx.surfaces.insert( + surface_id, + GraphicsContextSurface { + window_surface, + size: window.inner_size().clone(), + }, + ); + } +} - fragment_shader.set_source(&*glsl_preprocess( - FRAGMENT_GLSL_SHADER_SRC, - &get_glsl_shader_content, - )?)?; +#[tracing::instrument(skip_all)] +fn handle_commands( + mut graphics_ctx: Single<GraphicsContext>, + mut object_store: Single<RendererObjectStore>, + mut command_queue: Single<RendererCommandQueue>, + assets: Single<Assets>, + shader_context: Single<ShaderContext>, +) +{ + let GraphicsContext { + ref gl_context, + ref mut surfaces, + ref mut shader_uniform_buffer_objs, + objects: ref mut graphics_ctx_objects, + next_object_key: ref mut next_graphics_ctx_object_key, + } = *graphics_ctx; + + let Some(gl_context) = gl_context else { + return; + }; - fragment_shader.compile()?; + let mut opt_curr_gl_ctx: Option<CurrentContextWithFns> = None; - let mut gl_shader_program = GlShaderProgram::new(); + let mut activated_gl_shader_program: Option<(RendererObjectId, GlShaderProgram)> = + None; - gl_shader_program.attach(&vertex_shader); - gl_shader_program.attach(&fragment_shader); + 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(); - gl_shader_program.link()?; + match command { + RendererCommand::RemoveSurface(surface_id) => { + let Some(surface) = surfaces.remove(&surface_id) else { + tracing::error!(surface_id=?surface_id, "Surface does not exist"); + continue; + }; - Ok(gl_shader_program) -} + if surface.window_surface.is_current(gl_context.context()) { + opt_curr_gl_ctx = None; -#[derive(Debug, thiserror::Error)] -enum CreateShaderError -{ - #[error(transparent)] - ShaderError(#[from] GlShaderError), + if let Err(err) = gl_context.context().make_not_current_in_place() { + tracing::error!("Failed to make GL context not current: {err}"); + } + } - #[error(transparent)] - PreprocessingError(#[from] GlslPreprocessingError), + drop(surface); + } + RendererCommand::MakeCurrent(surface_id) => { + let Some(surface) = surfaces.get(&surface_id) else { + tracing::error!(surface_id=?surface_id, "Surface does not exist"); + continue; + }; + + let curr_gl_ctx = match gl_context.make_current(&surface.window_surface) { + Ok(current_graphics_context) => current_graphics_context, + Err(err) => { + tracing::error!("Failed to make graphics context current: {err}"); + continue; + } + }; + + if let Err(err) = gl_set_viewport( + &curr_gl_ctx, + &Vec2 { x: 0, y: 0 }.into(), + &surface.size.into(), + ) { + tracing::error!("Failed to set viewport: {err}"); + } + + opt_curr_gl_ctx = Some(curr_gl_ctx); + } + RendererCommand::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; + + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + continue; + }; + + if !surface.window_surface.is_current(gl_context.context()) { + continue; + } + + if let Err(err) = gl_set_viewport( + &curr_gl_ctx, + &Vec2 { x: 0, y: 0 }.into(), + &surface.size.into(), + ) { + tracing::error!("Failed to set viewport: {err}"); + } + } + RendererCommand::ClearBuffers(buffer_clear_mask) => { + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + tracing::error!("No GL context is current"); + continue; + }; + + 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(&curr_gl_ctx, clear_mask); + } + RendererCommand::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}"); + } + } + RendererCommand::CreateShaderProgram( + shader_program_obj_id, + shader_program, + ) => { + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + tracing::error!("No GL context is current"); + continue; + }; + + if object_store.contains_non_pending_with_id(&shader_program_obj_id) { + tracing::error!( + shader_program_object_id=?shader_program_obj_id, + shader_asset_label=?shader_program_obj_id + .into_asset_id() + .and_then(|asset_id| assets.get_label_by_id(asset_id)), + "Object store already contains a object with this ID" + ); + continue; + } + + let gl_shader_program = + match create_shader_program(&curr_gl_ctx, &shader_program) { + Ok(gl_shader_program) => gl_shader_program, + Err(err) => { + tracing::error!("Failed to create shader program: {err}"); + continue; + } + }; + + object_store.insert( + shader_program_obj_id, + RendererObject::from_raw( + gl_shader_program.into_raw(), + RendererObjectKind::ShaderProgram, + ), + ); + } + RendererCommand::ActivateShader(shader_program_obj_id) => { + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + tracing::error!("No GL context is current"); + continue; + }; + + let Some(shader_program_obj) = + object_store.get_shader_program_obj(&shader_program_obj_id) + else { + tracing::error!("Shader object does not exist or has a wrong kind"); + continue; + }; + + let gl_shader_program = + GlShaderProgram::from_raw(shader_program_obj.as_raw()); + + gl_shader_program.activate(&curr_gl_ctx); + + activated_gl_shader_program = + Some((shader_program_obj_id, gl_shader_program)); + } + RendererCommand::SetShaderBinding(binding_location, binding_value) => { + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + tracing::error!("No GL context is current"); + continue; + }; + + let Some((activated_gl_shader_program_obj_id, _)) = + &activated_gl_shader_program + else { + tracing::error!("No shader program is activated"); + continue; + }; + + if let ShaderBindingValue::Texture(texture_asset) = &binding_value { + let Some(texture_obj) = object_store + .get_texture_obj(&RendererObjectId::Asset(texture_asset.id())) + else { + tracing::error!( + "Texture {:?} does not exist in renderer object store", + assets.get_label(texture_asset) + ); + continue; + }; + + let gl_texture = GlTexture::from_raw(texture_obj.as_raw()); + + gl_texture.bind_to_texture_unit( + curr_gl_ctx, + binding_location.binding_index, + ); + + // gl_shader_program.set_uniform_at_location( + // curr_gl_ctx, + // GlUniformLocation::from_number( + // binding_location.binding_index as i32, + // ), + // &binding_location.binding_index, + // ); + + continue; + } + + let binding_index = binding_location.binding_index; + + let uniform_buffer_objs = shader_uniform_buffer_objs + .entry(*activated_gl_shader_program_obj_id) + .or_default(); + + let uniform_buffer = + uniform_buffer_objs.entry(binding_index).or_insert_with(|| { + let uniform_buf = + opengl_bindings::buffer::Buffer::<u8>::new(curr_gl_ctx); + + uniform_buf + .init( + curr_gl_ctx, + binding_location.binding_size, + opengl_bindings::buffer::Usage::Dynamic, + ) + .unwrap(); + + uniform_buf + }); + + // The index into the uniform buffer binding target is for whatever + // shader program is currently bound so the uniform buffer object has + // to be re-bound so that a uniform buffer from another shader isn't + // used + uniform_buffer.bind_to_indexed_target( + curr_gl_ctx, + opengl_bindings::buffer::BindingTarget::UniformBuffer, + binding_index as u32, + ); + + let fvec3_value; + + uniform_buffer + .store_at_byte_offset( + curr_gl_ctx, + 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 = CF32Vec3::from(value); + + fvec3_value.as_bytes() + } + ShaderBindingValue::FMat4x4(ref value) => { + value.items().as_bytes() + } + ShaderBindingValue::Texture(_) => unreachable!(), + }, + ) + .unwrap(); + } + RendererCommand::CreateTexture(texture_asset) => { + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + tracing::error!("No GL context is current"); + continue; + }; + + if let Err(err) = create_texture_object( + curr_gl_ctx, + &mut object_store, + &assets, + &texture_asset, + ) { + tracing::error!("Failed to create texture object: {err}"); + } + } + RendererCommand::CreateMesh { + obj_id: mesh_object_id, + mesh, + usage: mesh_usage, + } => { + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + tracing::error!("No GL context is current"); + continue; + }; + + if object_store.contains_non_pending_with_id(&mesh_object_id) { + tracing::error!( + mesh_object_id=?mesh_object_id, + mesh_asset_label=?mesh_object_id + .into_asset_id() + .and_then(|asset_id| assets.get_label_by_id(asset_id)), + "Object store already contains a object with this ID" + ); + continue; + } + + let Some((RendererObjectId::Asset(curr_shader_program_asset_id), _)) = + &activated_gl_shader_program + else { + tracing::error!("No shader program is activated"); + continue; + }; + + let curr_shader_program_metadata = shader_context + .get_program_metadata(curr_shader_program_asset_id) + .expect("Not possible"); + + let Some(vertex_desc) = &curr_shader_program_metadata.vertex_desc else { + tracing::error!( + "Current shader program does not have a vertex description" + ); + continue; + }; + + let key = *next_graphics_ctx_object_key; + + let mesh = match mesh_object_id { + RendererObjectId::Asset(mesh_asset_id) => match mesh.as_ref() { + Some(mesh) => mesh, + None => { + let Some(mesh) = + assets.get(&AssetHandle::from_id(mesh_asset_id)) + else { + tracing::error!( + asset_id=?mesh_asset_id, + "Mesh asset does not exist" + ); + continue; + }; + + mesh + } + }, + RendererObjectId::Sequential(_) => { + let Some(mesh) = mesh.as_ref() else { + tracing::error!( + object_id=?mesh_object_id, + "Object ID is sequential but no mesh data is given" + ); + continue; + }; + + mesh + } + }; + + let graphics_mesh = match GraphicsMesh::new( + &curr_gl_ctx, + &mesh, + mesh_usage, + &vertex_desc, + ) { + Ok(graphics_mesh) => graphics_mesh, + Err(err) => { + tracing::error!("Failed to create mesh: {err}"); + continue; + } + }; + + graphics_ctx_objects.insert( + key, + GraphicsContextObject::Mesh { + mesh: graphics_mesh, + compatible_shader_program_obj_id: RendererObjectId::Asset( + *curr_shader_program_asset_id, + ), + }, + ); + + object_store.insert( + mesh_object_id, + RendererObject::from_raw( + key, + RendererObjectKind::ImplementationSpecific, + ), + ); + + *next_graphics_ctx_object_key += 1; + } + RendererCommand::UpdateMesh { + obj_id: mesh_object_id, + mesh, + usage: mesh_usage, + } => { + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + tracing::error!("No GL context is current"); + continue; + }; + + let Some(mesh_graphics_ctx_obj_key) = object_store + .get_obj(&mesh_object_id) + .map(|obj| obj.as_raw()) + else { + tracing::error!( + object_id=?mesh_object_id, + "Object store does not contain a mesh object with this ID" + ); + continue; + }; + + let Some(mesh_graphics_ctx_obj) = + graphics_ctx_objects.get_mut(&mesh_graphics_ctx_obj_key) + else { + tracing::error!( + object_id=?mesh_object_id, + key=mesh_graphics_ctx_obj_key, + "Graphics context does not contain a mesh object with this key" + ); + continue; + }; + + #[allow(irrefutable_let_patterns)] + let GraphicsContextObject::Mesh { + mesh: graphics_mesh, + compatible_shader_program_obj_id: _, + } = mesh_graphics_ctx_obj + else { + tracing::error!( + object_id=?mesh_object_id, + key=mesh_graphics_ctx_obj_key, + "Graphics context object with this key is not a mesh" + ); + continue; + }; + + if let Err(err) = graphics_mesh.update(curr_gl_ctx, &mesh, mesh_usage) { + tracing::error!("Failed to update mesh: {err}"); + } + } + RendererCommand::RemoveMesh(mesh_object_id) => { + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + tracing::error!("No GL context is current"); + continue; + }; + + let Some(mesh_graphics_ctx_obj_key) = object_store + .remove(&mesh_object_id) + .flatten() + .map(|obj| obj.as_raw()) + else { + tracing::error!( + object_id=?mesh_object_id, + "Object store does not contain a mesh object with this ID" + ); + continue; + }; + + let Some(mesh_graphics_ctx_obj) = + graphics_ctx_objects.remove(&mesh_graphics_ctx_obj_key) + else { + tracing::error!( + object_id=?mesh_object_id, + key=mesh_graphics_ctx_obj_key, + "Graphics context does not contain a mesh object with this key" + ); + continue; + }; + + #[allow(irrefutable_let_patterns)] + let GraphicsContextObject::Mesh { mesh: mut graphics_mesh, .. } = + mesh_graphics_ctx_obj + else { + tracing::error!( + object_id=?mesh_object_id, + key=mesh_graphics_ctx_obj_key, + "Graphics context object with this key is not a mesh" + ); + continue; + }; + + graphics_mesh.destroy(curr_gl_ctx); + } + RendererCommand::DrawMesh(mesh_object_id, draw_mesh_opts) => { + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + tracing::error!("No GL context is current"); + continue; + }; + + let Some(mesh_graphics_ctx_obj_key) = object_store + .get_obj(&mesh_object_id) + .map(|obj| obj.as_raw()) + else { + tracing::error!( + object_id=?mesh_object_id, + "Object store does not contain a mesh object with this ID" + ); + continue; + }; + + let Some(mesh_graphics_ctx_obj) = + graphics_ctx_objects.get(&mesh_graphics_ctx_obj_key) + else { + tracing::error!( + object_id=?mesh_object_id, + key=mesh_graphics_ctx_obj_key, + "Graphics context does not contain a mesh object with this key" + ); + continue; + }; + + #[allow(irrefutable_let_patterns)] + let GraphicsContextObject::Mesh { + mesh: graphics_mesh, + compatible_shader_program_obj_id, + } = mesh_graphics_ctx_obj + else { + tracing::error!( + object_id=?mesh_object_id, + key=mesh_graphics_ctx_obj_key, + "Graphics context object with this key is not a mesh" + ); + continue; + }; + + if Some(compatible_shader_program_obj_id) + != activated_gl_shader_program.as_ref().map( + |(activated_gl_shader_program_obj_id, _)| { + activated_gl_shader_program_obj_id + }, + ) + { + tracing::error!(concat!( + "Activated shader program is not the ", + "compatible shader program of the mesh" + )); + continue; + } + + if let Err(err) = draw_mesh(&curr_gl_ctx, graphics_mesh, &draw_mesh_opts) + { + tracing::error!("Failed to draw mesh: {err}"); + }; + } + RendererCommand::UpdateDrawProperties( + draw_props, + draw_props_update_flags, + ) => { + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + tracing::error!("No GL context is current"); + continue; + }; + + if draw_props_update_flags + .contains(DrawPropertiesUpdateFlags::POLYGON_MODE_CONFIG) + { + opengl_bindings::misc::set_polygon_mode( + &curr_gl_ctx, + draw_props.polygon_mode_config.face, + draw_props.polygon_mode_config.mode, + ); + } + + if draw_props_update_flags + .contains(DrawPropertiesUpdateFlags::BLENDING_ENABLED) + { + set_enabled( + curr_gl_ctx, + Capability::Blend, + draw_props.blending_enabled, + ); + } + + if draw_props_update_flags + .contains(DrawPropertiesUpdateFlags::BLENDING_CONFIG) + { + gl_blending_configure( + curr_gl_ctx, + GlBlendingConfig::default() + .with_source_factor(blending_factor_to_gl( + draw_props.blending_config.source_factor, + )) + .with_destination_factor(blending_factor_to_gl( + draw_props.blending_config.destination_factor, + )) + .with_equation(blending_equation_to_gl( + draw_props.blending_config.equation, + )), + ); + } + + if draw_props_update_flags + .contains(DrawPropertiesUpdateFlags::DEPTH_TEST_ENABLED) + { + set_enabled( + curr_gl_ctx, + Capability::DepthTest, + draw_props.depth_test_enabled, + ); + } + + if draw_props_update_flags + .contains(DrawPropertiesUpdateFlags::SCISSOR_TEST_ENABLED) + { + set_enabled( + curr_gl_ctx, + Capability::ScissorTest, + draw_props.scissor_test_enabled, + ); + } + + if draw_props_update_flags + .contains(DrawPropertiesUpdateFlags::SCISSOR_BOX) + { + gl_define_scissor_box( + curr_gl_ctx, + draw_props.scissor_box.lower_left_corner_pos.into(), + draw_props + .scissor_box + .size + .unwrap_or_else(|| { + let (_, viewport_size) = gl_get_viewport(curr_gl_ctx); + + 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 draw_props_update_flags + .contains(DrawPropertiesUpdateFlags::FACE_CULLING_ENABLED) + { + set_enabled( + curr_gl_ctx, + Capability::CullFace, + draw_props.face_culling_enabled, + ); + } + } + } + } } -fn get_glsl_shader_content(path: &Path) -> Result<Vec<u8>, std::io::Error> +fn create_gl_context( + gl_config: &GlutinConfig, + graphics_props: &GraphicsProperties, + window_handle: WindowHandle<'_>, + surface: &GlutinSurface<GlutinWindowSurface>, +) -> Result<ContextWithFns, CreateGlContextError> { - if path == Path::new("vertex_data.glsl") { - return Ok(VERTEX_DATA_GLSL_SHADER_SRC.as_bytes().to_vec()); - } - - if path == Path::new("light.glsl") { - return Ok(LIGHT_GLSL_SHADER_SRC.as_bytes().to_vec()); + 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)?; - Err(IoError::new( - IoErrorKind::NotFound, - format!("Content for shader file {} not found", path.display()), - )) + ContextWithFns::new(glutin_context, &surface) + .map_err(CreateGlContextError::MakeContextCurrent) } -#[derive(Debug, Component)] -struct GlObjects +#[derive(Debug, thiserror::Error)] +enum CreateGlContextError { - /// Vertex and index buffer has to live as long as the vertex array - _vertex_buffer: Buffer<Vertex>, - index_buffer: Option<Buffer<u32>>, - element_cnt: u32, + #[error("Glutin context creation failed")] + CreateGlutinContext(#[source] GlutinError), - vertex_arr: VertexArray, + #[error("Making GL context current failed")] + MakeContextCurrent(#[source] GlMakeContextCurrentError), } -impl GlObjects +#[tracing::instrument(skip_all)] +fn create_texture_object( + curr_gl_ctx: &CurrentContextWithFns<'_>, + renderer_object_store: &mut RendererObjectStore, + assets: &Assets, + texture_asset: &AssetHandle<Texture>, +) -> Result<(), GlTextureGenerateError> { - #[tracing::instrument(skip_all)] - fn new(mesh: &Mesh) -> Self - { - 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); + let object_id = RendererObjectId::Asset(texture_asset.id()); - vertex_arr.bind_vertex_buffer(0, &vertex_buffer, 0); - - let mut offset = 0u32; + if renderer_object_store.contains_non_pending_with_id(&object_id) { + tracing::error!( + texture_object_id=?object_id, + texture_asset_label=?assets.get_label(texture_asset), + "Renderer object store already contains object with this ID" + ); + return Ok(()); + } - for attrib in Vertex::attrs() { - vertex_arr.enable_attrib(attrib.index); + let Some(texture) = assets.get(&texture_asset) else { + tracing::error!("Texture asset is not loaded",); + return Ok(()); + }; - vertex_arr.set_attrib_format( - attrib.index, - match attrib.component_type { - AttributeComponentType::Float => VertexArrayDataType::Float, - }, - false, - offset, + let texture_image = match texture.image.color_type() { + ImageColorType::Rgb8 if (texture.image.dimensions().width * 3) % 4 != 0 => { + // The texture will be corrupted if the alignment of each horizontal line of + // the texture pixel array is not multiple of 4. + // + // Read more about this at + // wikis.khronos.org/opengl/Common_Mistakes#Texture_upload_and_pixel_reads + // + // To prevent this, the image is converted to RGBA8. RGBA8 images have a pixel + // size of 4 bytes so they cannot have any alignment problems + + tracing::warn!( + texture_asset = %assets + .get_label(&texture_asset) + .expect("Not possible"), + concat!( + "Converting texture image from RGB8 to RGBA8 to prevent alignment ", + "problems. This conversion may be slow. Consider changing the ", + "texture image's pixel format to RGBA8" + ) ); - 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: vertex_buffer, - 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, - }; - } - - Self { - _vertex_buffer: vertex_buffer, - index_buffer: None, - element_cnt: mesh - .vertices() - .len() - .try_into() - .expect("Mesh vertex count does not fit into a 32-bit unsigned int"), - vertex_arr, + &texture.image.to_rgba8() } - } -} - -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_matrix = create_view_matrix(camera, &camera_pos.position); - - gl_shader_program.set_uniform_matrix_4fv(c"view", &view_matrix); + _ => &texture.image, + }; - #[allow(clippy::cast_precision_loss)] - let proj_matrix = match &camera.projection { - Projection::Perspective(perspective_proj) => perspective_proj.to_matrix_rh( - window_size.width as f32 / window_size.height as f32, - ClipVolume::NegOneToOne, + renderer_object_store.insert( + object_id, + RendererObject::from_raw( + create_gl_texture(curr_gl_ctx, texture_image, &texture.properties)? + .into_raw(), + RendererObjectKind::Texture, ), - Projection::Orthographic(orthographic_proj) => { - orthographic_proj.to_matrix_rh(&camera_pos.position, ClipVolume::NegOneToOne) - } - }; + ); - gl_shader_program.set_uniform_matrix_4fv(c"projection", &proj_matrix); + Ok(()) } -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>, +fn draw_mesh( + current_context: &CurrentContextWithFns<'_>, + graphics_mesh: &GraphicsMesh, + opts: &DrawMeshOptions, +) -> Result<(), GlDrawError> { - 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, - ); + 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), + )?; } - // 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, - ); - } + Ok(()) +} - // 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); +fn create_gl_texture( + current_context: &CurrentContextWithFns<'_>, + image: &Image, + texture_properties: &TextureProperties, +) -> Result<GlTexture, GlTextureGenerateError> +{ + let gl_texture = GlTexture::new(current_context); - gl_shader_program.set_uniform_vec_3fv( - c"material.ambient", - &if material_flags.use_ambient_color { - material.ambient.clone() + gl_texture.generate( + current_context, + &image.dimensions().into(), + image.as_bytes(), + match image.color_type() { + ImageColorType::Rgb8 => GlTexturePixelDataFormat::Rgb8, + ImageColorType::Rgba8 => GlTexturePixelDataFormat::Rgba8, + _ => { + unimplemented!(); + } + }, + if image.color_space_is_srgb() { + GlTextureColorSpace::Srgb } 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()); + GlTextureColorSpace::Linear + }, + )?; - let texture_map = material - .textures - .iter() - .enumerate() - .map(|(index, texture)| (texture.id(), index)) - .collect::<HashMap<_, _>>(); - - #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform_1i( - c"material.ambient_map", - *texture_map.get(&material.ambient_map).unwrap() as i32, + gl_texture.set_wrap( + current_context, + texture_wrapping_to_gl(texture_properties.wrap), ); - #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform_1i( - c"material.diffuse_map", - *texture_map.get(&material.diffuse_map).unwrap() as i32, + gl_texture.set_magnifying_filter( + current_context, + texture_filtering_to_gl(texture_properties.magnifying_filter), ); - #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform_1i( - c"material.specular_map", - *texture_map.get(&material.specular_map).unwrap() as i32, + gl_texture.set_minifying_filter( + current_context, + texture_filtering_to_gl(texture_properties.minifying_filter), ); - gl_shader_program.set_uniform_1fv(c"material.shininess", material.shininess); - - gl_shader_program.set_uniform_vec_3fv(c"view_pos", &camera_pos.position); + Ok(gl_texture) } -fn set_light_attenuation_uniforms( - gl_shader_program: &mut GlShaderProgram, - light_array: &str, - light_index: usize, - light: &PointLight, -) +fn create_shader_program( + current_context: &CurrentContextWithFns<'_>, + shader_program: &ShaderProgram, +) -> Result<GlShaderProgram, CreateShaderError> { - gl_shader_program.set_uniform_1fv( - &create_light_uniform_name( - light_array, - light_index, - "attenuation_props.constant", - ), - light.attenuation_params.constant, - ); + let shader_program_reflection = shader_program.reflection(0).expect("Not possible"); - gl_shader_program.set_uniform_1fv( - &create_light_uniform_name(light_array, light_index, "attenuation_props.linear"), - light.attenuation_params.linear, - ); + 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) + })?; - gl_shader_program.set_uniform_1fv( - &create_light_uniform_name( - light_array, - light_index, - "attenuation_props.quadratic", - ), - light.attenuation_params.quadratic, - ); -} + 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()), + })?; -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(), - ); + let vertex_shader = GlShader::new(current_context, ShaderKind::Vertex); - gl_shader_program.set_uniform_vec_3fv( - &create_light_uniform_name(light_array, light_index, "phong.specular"), - &light.specular().clone().into(), - ); -} + vertex_shader.set_source( + current_context, + &vertex_shader_entry_point_code.as_str().unwrap(), + )?; -trait Light -{ - fn diffuse(&self) -> &Color<f32>; - fn specular(&self) -> &Color<f32>; -} + vertex_shader.compile(current_context)?; -impl Light for PointLight -{ - fn diffuse(&self) -> &Color<f32> - { - &self.diffuse - } + let fragment_shader = GlShader::new(current_context, ShaderKind::Fragment); - fn specular(&self) -> &Color<f32> - { - &self.specular - } -} + fragment_shader.set_source( + current_context, + &fragment_shader_entry_point_code.as_str().unwrap(), + )?; -impl Light for DirectionalLight -{ - fn diffuse(&self) -> &Color<f32> - { - &self.diffuse - } + fragment_shader.compile(current_context)?; - fn specular(&self) -> &Color<f32> - { - &self.specular - } -} + let gl_shader_program = GlShaderProgram::new(current_context); -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(), - ) - } + 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) } -fn create_view_matrix(camera: &Camera, camera_pos: &Vec3<f32>) -> Matrix<f32, 4, 4> +#[derive(Debug, thiserror::Error)] +enum CreateShaderError { - let mut view = Matrix::new(); + #[error( + "Failed to get code of shader program entry point {entrypoint} of stage {stage:?}" + )] + GetShaderEntryPointCodeFailed + { + #[source] + err: ShaderError, + stage: ShaderStage, + entrypoint: Cow<'static, str>, + }, - view.look_at(&camera_pos, &camera.target, &camera.global_up); + #[error("No entrypoint was found for shader stage {0:?}")] + NoShaderStageEntrypointFound(ShaderStage), - view + #[error(transparent)] + ShaderError(#[from] GlShaderError), } #[tracing::instrument(skip_all)] @@ -693,7 +1346,7 @@ fn opengl_debug_message_cb( { use std::backtrace::{Backtrace, BacktraceStatus}; - use tracing::{event, Level}; + use tracing::{Level, event}; macro_rules! create_event { ($level: expr) => { @@ -712,7 +1365,8 @@ fn opengl_debug_message_cb( let backtrace = Backtrace::capture(); if matches!(backtrace.status(), BacktraceStatus::Captured) { - event!(Level::TRACE, "{backtrace}"); + tracing::error!("{backtrace}"); + // event!(Level::TRACE, "{backtrace}"); } } MessageType::Other => { @@ -724,19 +1378,131 @@ fn opengl_debug_message_cb( }; } -#[derive(Debug)] -struct Transformation +#[inline] +fn texture_wrapping_to_gl(texture_wrapping: TextureWrapping) -> GlTextureWrapping { - position: Vec3<f32>, - scale: Vec3<f32>, + match texture_wrapping { + TextureWrapping::Repeat => GlTextureWrapping::Repeat, + TextureWrapping::MirroredRepeat => GlTextureWrapping::MirroredRepeat, + TextureWrapping::ClampToEdge => GlTextureWrapping::ClampToEdge, + TextureWrapping::ClampToBorder => GlTextureWrapping::ClampToBorder, + } } -fn create_transformation_matrix(transformation: Transformation) -> Matrix<f32, 4, 4> +#[inline] +fn texture_filtering_to_gl(texture_filtering: TextureFiltering) -> GlTextureFiltering { - let mut matrix = Matrix::new_identity(); + match texture_filtering { + TextureFiltering::Linear => GlTextureFiltering::Linear, + TextureFiltering::Nearest => GlTextureFiltering::Nearest, + } +} - matrix.translate(&transformation.position); - matrix.scale(&transformation.scale); +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 } + } +} - matrix +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: ReprC + Copy> From<Matrix<Value, 4, 4>> + for opengl_bindings::data_types::Matrix<Value, 4, 4> +{ + fn from(matrix: Matrix<Value, 4, 4>) -> Self + { + Self { items: matrix.items } + } +} + +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, + } + } +} + +impl From<crate::draw_flags::PolygonMode> for opengl_bindings::misc::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, + } + } +} + +impl From<crate::draw_flags::PolygonModeFace> for opengl_bindings::misc::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, + } + } +} + +#[derive(Debug, IntoBytes, Immutable)] +#[repr(C)] +pub struct CF32Vec3 +{ + x: f32, + y: f32, + z: f32, +} + +impl From<Vec3<f32>> for CF32Vec3 +{ + fn from(src: Vec3<f32>) -> Self + { + Self { x: src.x, y: src.y, z: src.z } + } +} + +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, + } } diff --git a/engine/src/renderer/opengl/glsl/fragment.glsl b/engine/src/renderer/opengl/glsl/fragment.glsl deleted file mode 100644 index 5bf5ff2..0000000 --- a/engine/src/renderer/opengl/glsl/fragment.glsl +++ /dev/null @@ -1,73 +0,0 @@ -#version 330 core - -#preinclude "light.glsl" -#preinclude "vertex_data.glsl" - -#define MAX_LIGHT_CNT 64 - -out vec4 FragColor; - -in VertexData vertex_data; - -uniform vec3 view_pos; -uniform sampler2D input_texture; -uniform Material material; - -uniform PointLight point_lights[MAX_LIGHT_CNT]; -uniform int point_light_cnt; - -uniform DirectionalLight directional_lights[MAX_LIGHT_CNT]; -uniform int directional_light_cnt; - -void main() -{ - vec3 ambient_light = calc_ambient_light(material, vertex_data.texture_coords); - - vec3 directional_light_sum = vec3(0.0, 0.0, 0.0); - - for (int dl_index = 0; dl_index < directional_light_cnt; dl_index++) { - CalculatedLight calculated_dir_light; - - calc_light( - // Negated since we want the light to point from the light direction - normalize(-directional_lights[dl_index].direction), - directional_lights[dl_index].phong, - vertex_data, - view_pos, - material, - calculated_dir_light - ); - - directional_light_sum += - calculated_dir_light.diffuse + calculated_dir_light.specular; - } - - vec3 point_light_sum = vec3(0.0, 0.0, 0.0); - - for (int pl_index = 0; pl_index < point_light_cnt; pl_index++) { - vec3 light_direction = - normalize(point_lights[pl_index].position - vertex_data.world_space_pos); - - CalculatedLight calculated_point_light; - - calc_light( - light_direction, - point_lights[pl_index].phong, - vertex_data, - view_pos, - material, - calculated_point_light - ); - - float attenuation = - calc_attenuation(point_lights[pl_index], vertex_data.world_space_pos); - - calculated_point_light.diffuse *= attenuation; - calculated_point_light.specular *= attenuation; - - point_light_sum += - calculated_point_light.diffuse + calculated_point_light.specular; - } - - FragColor = vec4((ambient_light + directional_light_sum + point_light_sum), 1.0); -} diff --git a/engine/src/renderer/opengl/glsl/light.glsl b/engine/src/renderer/opengl/glsl/light.glsl deleted file mode 100644 index f12b5fe..0000000 --- a/engine/src/renderer/opengl/glsl/light.glsl +++ /dev/null @@ -1,133 +0,0 @@ -#version 330 core - -#ifndef LIGHT_GLSL -#define LIGHT_GLSL - -#preinclude "vertex_data.glsl" - -struct Material -{ - vec3 ambient; - vec3 diffuse; - vec3 specular; - sampler2D ambient_map; - sampler2D diffuse_map; - sampler2D specular_map; - float shininess; -}; - -struct LightPhong -{ - vec3 diffuse; - vec3 specular; -}; - -struct AttenuationProperties -{ - float constant; - float linear; - float quadratic; -}; - -struct PointLight -{ - LightPhong phong; - vec3 position; - AttenuationProperties attenuation_props; -}; - -struct DirectionalLight -{ - LightPhong phong; - vec3 direction; -}; - -struct CalculatedLight -{ - vec3 diffuse; - vec3 specular; -}; - -vec3 calc_ambient_light(in Material material, in vec2 texture_coords) -{ - return vec3(texture(material.ambient_map, texture_coords)) * material.ambient; -} - -vec3 calc_diffuse_light( - in Material material, - in LightPhong light_phong, - in vec3 light_dir, - in vec3 norm, - in vec2 texture_coords -) -{ - float diff = max(dot(norm, light_dir), 0.0); - - return light_phong.diffuse * ( - diff * (vec3(texture(material.diffuse_map, texture_coords)) * material.diffuse) - ); -} - -vec3 calc_specular_light( - in Material material, - in LightPhong light_phong, - in vec3 light_dir, - in vec3 norm, - in vec3 view_pos, - in vec3 frag_pos, - in vec2 texture_coords -) -{ - vec3 view_direction = normalize(view_pos - frag_pos); - - vec3 halfway_direction = normalize(light_dir + view_direction); - - float spec = - pow(max(dot(norm, halfway_direction), 0.0), material.shininess); - - return light_phong.specular * ( - spec * (vec3(texture(material.specular_map, texture_coords)) * material.specular) - ); -} - -float calc_attenuation(in PointLight point_light, in vec3 position) -{ - float light_distance = length(point_light.position - position); - - return 1.0 / (point_light.attenuation_props.constant - + point_light.attenuation_props.linear - * light_distance + point_light.attenuation_props.quadratic - * pow(light_distance, 2)); -} - -void calc_light( - in vec3 light_direction, - in LightPhong light_phong, - in VertexData vertex_data, - in vec3 view_pos, - in Material material, - out CalculatedLight calculated_light -) -{ - vec3 norm = normalize(vertex_data.world_space_normal); - - calculated_light.diffuse = calc_diffuse_light( - material, - light_phong, - light_direction, - norm, - vertex_data.texture_coords - ); - - calculated_light.specular = calc_specular_light( - material, - light_phong, - light_direction, - norm, - view_pos, - vertex_data.world_space_pos, - vertex_data.texture_coords - ); -} - -#endif diff --git a/engine/src/renderer/opengl/glsl/vertex.glsl b/engine/src/renderer/opengl/glsl/vertex.glsl deleted file mode 100644 index b57caa6..0000000 --- a/engine/src/renderer/opengl/glsl/vertex.glsl +++ /dev/null @@ -1,24 +0,0 @@ -#version 330 core - -#preinclude "vertex_data.glsl" - -layout (location = 0) in vec3 pos; -layout (location = 1) in vec2 texture_coords; -layout (location = 2) in vec3 normal; - -out VertexData vertex_data; - -uniform mat4 model; -uniform mat4 view; -uniform mat4 projection; - -void main() -{ - gl_Position = projection * view * model * vec4(pos, 1.0); - - vertex_data.world_space_pos = vec3(model * vec4(pos, 1.0)); - vertex_data.texture_coords = texture_coords; - - // TODO: Do this using CPU for performance increase - vertex_data.world_space_normal = mat3(transpose(inverse(model))) * normal; -} diff --git a/engine/src/renderer/opengl/glsl/vertex_data.glsl b/engine/src/renderer/opengl/glsl/vertex_data.glsl deleted file mode 100644 index 486d445..0000000 --- a/engine/src/renderer/opengl/glsl/vertex_data.glsl +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef VERTEX_DATA_GLSL -#define VERTEX_DATA_GLSL - -struct VertexData -{ - vec2 texture_coords; - vec3 world_space_pos; - vec3 world_space_normal; -}; - -#endif diff --git a/engine/src/renderer/opengl/glutin_compat.rs b/engine/src/renderer/opengl/glutin_compat.rs new file mode 100644 index 0000000..cfd6ea7 --- /dev/null +++ b/engine/src/renderer/opengl/glutin_compat.rs @@ -0,0 +1,268 @@ +// 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; +#[cfg(x11_platform)] +use glutin::platform::x11::X11GlConfigExt; +use glutin::prelude::*; +use raw_window_handle::{DisplayHandle, RawWindowHandle, WindowHandle}; + +use crate::windowing::window::CreationAttributes as WindowCreationAttributes; + +#[cfg(all(not(egl_backend), not(glx_backend), not(wgl_backend), not(cgl_backend)))] +compile_error!("Please select at least one api backend"); + +/// 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!(wgl_backend) { + 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. + #[cfg(wgl_backend)] + let template_builder = if let Some(raw_window_handle) = raw_window_handle { + 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(wgl_backend)) 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)?; + + #[cfg(not(wgl_backend))] + let window_attrs = + { finalize_window_creation_attrs(self.window_attributes, &picked_gl_config) }; + + #[cfg(wgl_backend)] + let window_attrs = self.window_attributes; + + 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> +{ + #[cfg(egl_backend)] + let _preference = DisplayApiPreference::Egl; + + #[cfg(glx_backend)] + let _preference = DisplayApiPreference::Glx(Box::new( + crate::windowing::window::platform::x11::register_xlib_error_hook, + )); + + #[cfg(cgl_backend)] + let _preference = DisplayApiPreference::Cgl; + + #[cfg(wgl_backend)] + let _preference = DisplayApiPreference::Wgl(_raw_window_handle); + + #[cfg(all(egl_backend, glx_backend))] + let _preference = 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, + )), + }; + + #[cfg(all(wgl_backend, egl_backend))] + let _preference = match _api_preference { + ApiPreference::PreferEgl => DisplayApiPreference::EglThenWgl(_raw_window_handle), + ApiPreference::FallbackEgl => { + DisplayApiPreference::WglThenEgl(_raw_window_handle) + } + }; + + 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(wgl_backend))] +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(x11_platform)] + let attributes = if let Some(x11_visual) = gl_config.x11_visual() { + attributes.with_x11_visual(x11_visual.visual_id() as _) + } else { + attributes + }; + + 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/renderer/opengl/graphics_mesh.rs b/engine/src/renderer/opengl/graphics_mesh.rs new file mode 100644 index 0000000..c5ff0c6 --- /dev/null +++ b/engine/src/renderer/opengl/graphics_mesh.rs @@ -0,0 +1,216 @@ +use opengl_bindings::CurrentContextWithFns as GlCurrentContextWithFns; +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 crate::mesh::{Mesh, VertexAttrType}; +use crate::renderer::MeshUsage; +use crate::shader::VertexDescription as ShaderVertexDescription; + +#[derive(Debug)] +pub struct GraphicsMesh +{ + /// Vertex and index buffer has to live as long as the vertex array + vertex_buffer: GlBuffer<u8>, + 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, + vertex_desc: &ShaderVertexDescription, + ) -> 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)?; + + let vertex_buf_binding_index = 0; + + 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 + ); + } + } + } + + for vertex_attr_props in mesh.vertex_buf().vertex_attr_props() { + let vertex_field_desc = vertex_desc + .fields + .iter() + .find(|vertex_field_desc| { + *vertex_field_desc.name == vertex_attr_props.name + }) + .unwrap(); + + let attrib_index: u32 = + vertex_field_desc.varying_input_offset.try_into().unwrap(); + + vertex_arr.enable_attrib(current_context, attrib_index); + + vertex_arr.set_attrib_format( + current_context, + 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(), + } + } + }, + ); + + vertex_arr.set_attrib_vertex_buf_binding( + current_context, + attrib_index, + vertex_buf_binding_index, + ); + } + + 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, + 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, + 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 destroy(&mut 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), +} + +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/shader.rs b/engine/src/shader.rs new file mode 100644 index 0000000..c4bd709 --- /dev/null +++ b/engine/src/shader.rs @@ -0,0 +1,1157 @@ +use std::any::type_name; +use std::borrow::Cow; +use std::collections::HashMap; +use std::fmt::Debug; +use std::path::Path; +use std::str::Utf8Error; + +use bitflags::{bitflags, bitflags_match}; +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{POST_UPDATE as POST_UPDATE_PHASE, Phase, START as START_PHASE}; +use ecs::sole::Single; +use ecs::{Component, Sole, declare_entity}; +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_ASSETS_PHASE, + Handle as AssetHandle, + Id as AssetId, + Submitter as AssetSubmitter, +}; +use crate::builder; +use crate::shader::default::{ + ASSET_LABEL, + enqueue_set_shader_bindings as default_shader_enqueue_set_shader_bindings, +}; + +pub mod cursor; +pub mod default; + +/// The vertex parameter of a vertex entrypoint function in a shader should have this +/// semantic name. +pub const VERTEX_PARAM_SEMANTIC_NAME: &str = "VERTEX"; + +#[derive(Debug, Clone, Component)] +pub struct Shader +{ + pub asset_handle: AssetHandle<ModuleSource>, +} + +/// 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> + { + 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 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, +} + +impl Program +{ + pub fn link(&self) -> Result<Program, Error> + { + let linked_program = self.inner.link()?; + + Ok(Program { inner: linked_program }) + } + + 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>> + { + 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 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) -> impl ExactSizeIterator<Item = VariableLayout<'a>> + { + self.inner + .fields() + .map(|field| VariableLayout { inner: field }) + } + + pub fn field_cnt(&self) -> u32 + { + self.inner.field_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 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, +} + +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, + } + } +} + +#[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, ProgramMetadata)>, +} + +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).map(|(program, _)| program) + } + + pub fn get_program_metadata(&self, asset_id: &AssetId) -> Option<&ProgramMetadata> + { + self.programs + .get(asset_id) + .map(|(_, program_metadata)| program_metadata) + } + + pub fn compose_into_program( + &self, + modules: impl IntoIterator<Item = Module>, + entry_points: impl IntoIterator<Item = EntryPoint>, + ) -> Result<Program, Error> + { + let components = + modules + .into_iter() + .map(|module| SlangComponentType::from(module.inner.clone())) + .chain(entry_points.into_iter().map(|entry_point| { + SlangComponentType::from(entry_point.inner.clone()) + })) + .collect::<Vec<_>>(); + + let program = self.session.create_composite_component_type(&components)?; + + Ok(Program { inner: program }) + } +} + +#[derive(Debug)] +#[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)] +#[non_exhaustive] +pub struct VertexDescription +{ + pub fields: Box<[VertexFieldDescription]>, +} + +impl VertexDescription +{ + pub fn new( + vs_entrypoint: &EntryPointReflection<'_>, + ) -> Result<Self, VertexDescriptionError> + { + if vs_entrypoint.stage() != Stage::Vertex { + return Err(VertexDescriptionError::EntrypointNotInVertexStage); + } + + let vs_entrypoint_vertex_param = vs_entrypoint + .parameters() + .find(|param| param.semantic_name() == Some(VERTEX_PARAM_SEMANTIC_NAME)) + .ok_or(VertexDescriptionError::EntrypointMissingVertexParam)?; + + let vs_entrypoint_vertex_param = vs_entrypoint_vertex_param + .type_layout() + .expect("Not possible"); + + if vs_entrypoint_vertex_param.parameter_category() + != ParameterCategory::VaryingInput + { + return Err(VertexDescriptionError::EntryPointVertexParamNotVaryingInput); + } + + if vs_entrypoint_vertex_param.kind() != TypeKind::Struct { + return Err(VertexDescriptionError::EntrypointVertexTypeNotStruct); + } + + let fields = vs_entrypoint_vertex_param + .fields() + .map(|field| { + let varying_input_offset = + field.varying_input_offset().expect("Not possible"); + + let field_ty = field.type_layout().expect("Maybe not possible"); + + let scalar_type = match field_ty.kind() { + TypeKind::Scalar => field_ty.scalar_type().expect("Not possible"), + TypeKind::Vector => { + let Some(scalar_type) = field_ty.scalar_type() else { + return Err( + VertexDescriptionError::UnsupportedVertexFieldType { + field_name: field.name().unwrap_or("").to_string(), + }, + ); + }; + + scalar_type + } + _ => { + return Err(VertexDescriptionError::UnsupportedVertexFieldType { + field_name: field.name().unwrap_or("").to_string(), + }); + } + }; + + Ok(VertexFieldDescription { + name: field.name().unwrap_or("").to_string().into_boxed_str(), + varying_input_offset, + type_kind: field_ty.kind(), + scalar_type, + }) + }) + .collect::<Result<Vec<_>, _>>()?; + + Ok(Self { fields: fields.into_boxed_slice() }) + } +} + +#[derive(Debug)] +pub struct VertexFieldDescription +{ + pub name: Box<str>, + pub varying_input_offset: usize, + pub type_kind: TypeKind, + pub scalar_type: ScalarType, +} + +#[derive(Debug, thiserror::Error)] +pub enum VertexDescriptionError +{ + #[error("Entrypoint is not in vertex stage")] + EntrypointNotInVertexStage, + + #[error( + "Entrypoint does not have a vertex parameter (parameter with semantic name {})", + VERTEX_PARAM_SEMANTIC_NAME + )] + EntrypointMissingVertexParam, + + #[error("Entrypoint vertex parameter is not a varying input")] + EntryPointVertexParamNotVaryingInput, + + #[error("Entrypoint vertex type is not a struct")] + EntrypointVertexTypeNotStruct, + + #[error("Type of field '{field_name}' of vertex type is not supported")] + UnsupportedVertexFieldType + { + field_name: String + }, +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct Error(#[from] shader_slang::Error); + +pub(crate) fn add_asset_importers(assets: &mut Assets) +{ + assets.set_importer::<_, _>(["slang"], import_slang_asset); +} + +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!( + IMPORT_SHADERS_PHASE, + ( + Phase, + Pair::builder() + .relation::<ChildOf>() + .target_id(*HANDLE_ASSETS_PHASE) + .build() + ) +); + +pub(crate) struct Extension; + +impl ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: 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_column(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.add_declared_entity(&IMPORT_SHADERS_PHASE); + + collector.add_system(*START_PHASE, initialize); + collector.add_system(*IMPORT_SHADERS_PHASE, load_modules); + + collector.add_system( + *POST_UPDATE_PHASE, + default_shader_enqueue_set_shader_bindings, + ); + } +} + +fn initialize(mut assets: Single<Assets>) +{ + assets.store_with_label( + ASSET_LABEL.clone(), + ModuleSource { + name: "default_shader.slang".into(), + file_path: Path::new("@engine/default_shader").into(), + source: include_str!("../res/default_shader.slang").into(), + link_entrypoints: EntrypointFlags::VERTEX | EntrypointFlags::FRAGMENT, + }, + ); +} + +#[tracing::instrument(skip_all)] +fn load_modules(mut context: Single<Context>, assets: Single<Assets>) +{ + 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 entry_points = match module_source + .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(EntrypointNotFoundError { entrypoint_name })); + }; + + Some(Ok(entry_point)) + }) + .collect::<Result<Vec<_>, EntrypointNotFoundError>>() + { + Ok(entry_points) => entry_points, + Err(EntrypointNotFoundError { entrypoint_name }) => { + tracing::error!( + "Shader module does not have a '{entrypoint_name}' entry point" + ); + continue; + } + }; + + let shader_program = + match context.compose_into_program([module], entry_points) { + Ok(shader_program) => shader_program, + Err(err) => { + tracing::error!("Failed to compose shader into program: {err}"); + continue; + } + }; + + let linked_shader_program = match shader_program.link() { + Ok(linked_shader_program) => linked_shader_program, + Err(err) => { + tracing::error!("Failed to link shader: {err}"); + continue; + } + }; + + let vertex_desc = if module_source + .link_entrypoints + .contains(EntrypointFlags::VERTEX) + { + VertexDescription::new( + &shader_program + .reflection(0) + .expect("Not possible") + .get_entry_point_by_name("vertex_main") + .expect("Not possible"), + ) + .inspect_err(|err| { + tracing::error!( + "Failed to create a vertex description for shader {}: {}", + asset_label, + err + ); + }) + .ok() + } else { + None + }; + + context.programs.insert( + *asset_id, + (linked_shader_program, ProgramMetadata { vertex_desc }), + ); + } + } +} + +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 }) +} + +#[derive(Debug)] +struct EntrypointNotFoundError +{ + entrypoint_name: &'static str, +} diff --git a/engine/src/shader/cursor.rs b/engine/src/shader/cursor.rs new file mode 100644 index 0000000..0522858 --- /dev/null +++ b/engine/src/shader/cursor.rs @@ -0,0 +1,160 @@ +use crate::asset::Handle as AssetHandle; +use crate::matrix::Matrix; +use crate::shader::{TypeKind, TypeLayout, VariableLayout}; +use crate::texture::Texture; +use crate::vector::Vec3; + +/// Shader cursor +#[derive(Clone)] +pub struct Cursor<'a> +{ + type_layout: TypeLayout<'a>, + binding_location: BindingLocation, +} + +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, + } + } + + pub fn field(&self, name: &str) -> Self + { + let Some(field_var_layout) = self.type_layout.get_field_by_name(name) else { + panic!("Field '{name}' does not exist"); + }; + + let field_type_kind = field_var_layout.ty().unwrap().kind(); + + let field_var_layout = match field_type_kind { + TypeKind::ConstantBuffer => 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"), + TypeKind::Array + | TypeKind::Matrix + | TypeKind::Scalar + | TypeKind::Vector + | TypeKind::Struct + | TypeKind::Resource => field_var_layout, + type_kind => unimplemented!("Type kind {type_kind:?} is not yet supported"), + }; + + Self { + type_layout: field_var_layout.type_layout().unwrap(), + binding_location: BindingLocation { + binding_index: self.binding_location.binding_index + + field_var_layout.binding_index(), + binding_size: if field_type_kind == TypeKind::ConstantBuffer { + field_var_layout + .type_layout() + .unwrap() + .uniform_size() + .unwrap() + } else { + self.binding_location.binding_size + }, + byte_offset: self.binding_location.byte_offset + + field_var_layout.offset(), + }, + } + } + + 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 + } + + pub fn with_field(self, name: &str, func: impl FnOnce(Self) -> Self) -> Self + { + let _ = func(self.field(name)); + + self + } + + pub fn binding_location(&self) -> &BindingLocation + { + &self.binding_location + } + + pub fn into_binding_location(self) -> BindingLocation + { + self.binding_location + } +} + +#[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>), + FMat4x4(Matrix<f32, 4, 4>), + Texture(AssetHandle<Texture>), +} + +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<Matrix<f32, 4, 4>> for BindingValue +{ + fn from(matrix: Matrix<f32, 4, 4>) -> Self + { + BindingValue::FMat4x4(matrix) + } +} diff --git a/engine/src/shader/default.rs b/engine/src/shader/default.rs new file mode 100644 index 0000000..7f0d1bb --- /dev/null +++ b/engine/src/shader/default.rs @@ -0,0 +1,368 @@ +use std::path::Path; +use std::sync::LazyLock; + +use ecs::Query; +use ecs::actions::Actions; +use ecs::query::term::Without; +use ecs::sole::Single; + +use crate::asset::{Assets, Label as AssetLabel}; +use crate::camera::{Active as ActiveCamera, Camera}; +use crate::data_types::dimens::Dimens; +use crate::draw_flags::NoDraw; +use crate::lighting::{DirectionalLight, GlobalLight, PointLight}; +use crate::material::{Flags as MaterialFlags, Material}; +use crate::matrix::Matrix; +use crate::model::{MaterialSearchResult, Model}; +use crate::projection::{ClipVolume as ProjectionClipVolume, Projection}; +use crate::renderer::{PendingShaderBindings, SurfaceSpec}; +use crate::shader::cursor::{BindingValue as ShaderBindingValue, Cursor as ShaderCursor}; +use crate::shader::{ + Context as ShaderContext, + ModuleSource as ShaderModuleSource, + Shader, +}; +use crate::texture::WHITE_1X1_ASSET_LABEL as TEXTURE_WHITE_1X1_ASSET_LABEL; +use crate::transform::{Scale, Transform, WorldPosition}; +use crate::vector::Vec3; +use crate::windowing::window::Window; + +pub static ASSET_LABEL: LazyLock<AssetLabel> = LazyLock::new(|| AssetLabel { + path: Path::new("").into(), + name: Some("default_shader".into()), +}); + +pub fn enqueue_set_shader_bindings( + renderable_query: Query<RenderableEntity<'_>, (Without<NoDraw>,)>, + camera_query: Query<(&Camera, &WorldPosition, &ActiveCamera)>, + window_query: Query<(&Window, &SurfaceSpec)>, + point_light_query: Query<(&PointLight, &WorldPosition)>, + directional_light_query: Query<(&DirectionalLight,)>, + assets: Single<Assets>, + shader_context: Single<ShaderContext>, + global_light: Single<GlobalLight>, + mut actions: Actions, +) +{ + let Some((camera, camera_world_pos, _)) = camera_query.iter().next() else { + tracing::warn!("No current camera"); + return; + }; + + let default_shader_asset = assets + .get_handle_to_loaded::<ShaderModuleSource>(ASSET_LABEL.clone()) + .unwrap(); + + for ( + entity_id, + (model, material_flags, world_pos, scale, shader, mut pending_shader_bindings), + ) in renderable_query.iter_with_euids() + { + let shader_asset = match &shader { + Some(shader) => &shader.asset_handle, + None => &default_shader_asset, + }; + + if shader_asset.id() != default_shader_asset.id() { + continue; + } + + let has_pending_shader_bindings_comp = pending_shader_bindings.is_some(); + + let shader_bindings = match pending_shader_bindings.as_deref_mut() { + Some(pending_shader_bindings) => pending_shader_bindings, + None => &mut PendingShaderBindings::default(), + }; + + // Bindings are cleared to prevent them from growing infinitely + shader_bindings.bindings.clear(); + shader_bindings.surface_specific_bindings.clear(); + + let Some(shader_program) = shader_context.get_program(&shader_asset.id()) else { + continue; + }; + + let Some(model_spec) = assets.get(&model.spec_asset) else { + continue; + }; + + let shader_cursor = ShaderCursor::new( + shader_program + .reflection(0) + .unwrap() + .global_params_var_layout() + .unwrap(), + ); + + let model_matrix = create_model_matrix(Transform { + position: world_pos.as_deref().cloned().unwrap_or_default().position, + scale: scale.as_deref().cloned().unwrap_or_default().scale, + }); + + let inverted_model_matrix = model_matrix.inverse(); + + 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 { + continue; + }; + + model_material + } + MaterialSearchResult::NotFound => { + continue; + } + MaterialSearchResult::NoMaterials => &const { Material::builder().build() }, + }; + + if [ + &model_material.ambient_map, + &model_material.diffuse_map, + &model_material.specular_map, + ] + .into_iter() + .flatten() + .any(|texture_asset| !assets.is_loaded_and_has_type(&texture_asset)) + { + continue; + } + + let material_flags = material_flags + .as_deref() + .unwrap_or(&const { MaterialFlags::builder().build() }); + + let model_3d_shader_cursor = shader_cursor.field("Uniforms").field("model_3d"); + let lighting_shader_cursor = shader_cursor.field("Uniforms").field("lighting"); + let material_shader_cursor = lighting_shader_cursor.field("material"); + + shader_bindings.extend([ + (model_3d_shader_cursor.field("model"), model_matrix.into()), + ( + model_3d_shader_cursor.field("model_inverted"), + inverted_model_matrix.into(), + ), + ( + model_3d_shader_cursor.field("view"), + create_view_matrix(&camera, &camera_world_pos.position).into(), + ), + ( + lighting_shader_cursor.field("view_pos"), + camera_world_pos.position.into(), + ), + ( + material_shader_cursor.field("ambient"), + Vec3::from( + if material_flags.use_ambient_color { + &model_material.ambient + } else { + &global_light.ambient + } + .clone(), + ) + .into(), + ), + ( + material_shader_cursor.field("diffuse"), + Vec3::from(model_material.diffuse.clone()).into(), + ), + ( + material_shader_cursor.field("specular"), + Vec3::from(model_material.specular.clone()).into(), + ), + ( + material_shader_cursor.field("shininess"), + model_material.shininess.into(), + ), + ( + lighting_shader_cursor.field("directional_light_cnt"), + u32::try_from(directional_light_query.iter().count()) + .expect( + "Directional light count does not fit in 32-bit unsigned integer", + ) + .into(), + ), + ( + lighting_shader_cursor.field("point_light_cnt"), + u32::try_from(point_light_query.iter().count()) + .expect("Point light count does not fit in 32-bit unsigned integer") + .into(), + ), + ]); + + for (window, window_surface_spec) in &window_query { + shader_bindings.surface_specific_bindings.push(( + window_surface_spec.id, + model_3d_shader_cursor + .field("projection") + .into_binding_location(), + create_projection_matrix(&camera, window.inner_size()).into(), + )); + } + + shader_bindings.extend(point_light_query.iter().enumerate().flat_map( + |(point_light_index, (point_light, point_light_world_pos))| { + 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"); + + [ + ( + phong_shader_cursor.field("diffuse"), + Vec3::from(point_light.diffuse.clone()).into(), + ), + ( + phong_shader_cursor.field("specular"), + Vec3::from(point_light.specular.clone()).into(), + ), + ( + point_light_shader_cursor.field("position"), + (point_light_world_pos.position + point_light.local_position) + .into(), + ), + ( + attenuation_props_shader_cursor.field("constant"), + point_light.attenuation_params.constant.into(), + ), + ( + attenuation_props_shader_cursor.field("linear"), + point_light.attenuation_params.linear.into(), + ), + ( + attenuation_props_shader_cursor.field("quadratic"), + point_light.attenuation_params.quadratic.into(), + ), + ] + }, + )); + + shader_bindings.extend(directional_light_query.iter().enumerate().flat_map( + |(directional_light_index, (directional_light,))| { + 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"); + + [ + ( + phong_shader_cursor.field("diffuse"), + Vec3::from(directional_light.diffuse.clone()).into(), + ), + ( + phong_shader_cursor.field("specular"), + Vec3::from(directional_light.specular.clone()).into(), + ), + ( + directional_light_shader_cursor.field("direction"), + directional_light.direction.into(), + ), + ] + }, + )); + + shader_bindings.bindings.push(( + shader_cursor.field("ambient_map").into_binding_location(), + ShaderBindingValue::Texture( + model_material + .ambient_map + .as_ref() + .map(|ambient_map| ambient_map.clone()) + .unwrap_or_else(|| { + assets + .get_handle_to_loaded(TEXTURE_WHITE_1X1_ASSET_LABEL.clone()) + .expect("Not possible") + }), + ), + )); + + shader_bindings.bindings.push(( + shader_cursor.field("diffuse_map").into_binding_location(), + ShaderBindingValue::Texture( + model_material + .diffuse_map + .as_ref() + .map(|diffuse_map| diffuse_map.clone()) + .unwrap_or_else(|| { + assets + .get_handle_to_loaded(TEXTURE_WHITE_1X1_ASSET_LABEL.clone()) + .expect("Not possible") + }), + ), + )); + + shader_bindings.bindings.push(( + shader_cursor.field("specular_map").into_binding_location(), + ShaderBindingValue::Texture( + model_material + .specular_map + .as_ref() + .map(|specular_map| specular_map.clone()) + .unwrap_or_else(|| { + assets + .get_handle_to_loaded(TEXTURE_WHITE_1X1_ASSET_LABEL.clone()) + .expect("Not possible") + }), + ), + )); + + if !has_pending_shader_bindings_comp { + actions.add_components(entity_id, (shader_bindings.clone(),)); + } + } +} + +fn create_model_matrix(transform: Transform) -> Matrix<f32, 4, 4> +{ + let mut matrix = Matrix::new_identity(); + + matrix.translate(&transform.position); + matrix.scale(&transform.scale); + + matrix +} + +fn create_view_matrix(camera: &Camera, camera_world_pos: &Vec3<f32>) +-> Matrix<f32, 4, 4> +{ + let mut view = Matrix::new(); + + // tracing::debug!("Camera target: {:?}", camera.target); + + view.look_at(camera_world_pos, &camera.target, &camera.global_up); + + view +} + +fn create_projection_matrix( + camera: &Camera, + window_size: &Dimens<u32>, +) -> Matrix<f32, 4, 4> +{ + match &camera.projection { + Projection::Perspective(perspective_proj) => perspective_proj.to_matrix_rh( + window_size.width as f32 / window_size.height as f32, + ProjectionClipVolume::NegOneToOne, + ), + Projection::Orthographic(orthographic_proj) => orthographic_proj.to_matrix_rh( + Dimens { + width: window_size.width as f32, + height: window_size.height as f32, + }, + ProjectionClipVolume::NegOneToOne, + ), + } +} + +type RenderableEntity<'a> = ( + &'a Model, + Option<&'a MaterialFlags>, + Option<&'a WorldPosition>, + Option<&'a Scale>, + Option<&'a Shader>, + Option<&'a mut PendingShaderBindings>, +); diff --git a/engine/src/texture.rs b/engine/src/texture.rs index 4a4fe86..b069228 100644 --- a/engine/src/texture.rs +++ b/engine/src/texture.rs @@ -1,192 +1,22 @@ -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 std::sync::LazyLock; +use crate::asset::{Assets, Label as AssetLabel, Submitter as AssetSubmitter}; +use crate::builder; use crate::color::Color; use crate::data_types::dimens::Dimens; -use crate::opengl::texture::PixelDataFormat; -use crate::util::builder; - -static NEXT_ID: AtomicU32 = AtomicU32::new(0); - -mod reexports -{ - pub use crate::opengl::texture::{Filtering, Wrapping}; -} +use crate::image::{Error as ImageError, Image}; -pub use reexports::*; +pub static WHITE_1X1_ASSET_LABEL: LazyLock<AssetLabel> = LazyLock::new(|| AssetLabel { + path: Path::new("").into(), + name: Some("white_1x1_texture".into()), +}); #[derive(Debug, Clone)] pub struct Texture { - id: Id, - image: DynamicImage, - pixel_data_format: PixelDataFormat, - dimensions: Dimens<u32>, - properties: Properties, -} - -impl Texture -{ - pub fn builder() -> Builder - { - Builder::default() - } - - /// Opens a texture image. - /// - /// # Errors - /// Will return `Err` if: - /// - Opening the image fails - /// - The image data is not 8-bit/color RGB - pub fn open(path: &Path) -> Result<Self, Error> - { - Self::builder().open(path) - } - - #[must_use] - pub fn new_from_color(dimensions: &Dimens<u32>, color: &Color<u8>) -> Self - { - Self::builder().build_with_single_color(dimensions, color) - } - - #[must_use] - pub fn id(&self) -> Id - { - self.id - } - - #[must_use] - pub fn properties(&self) -> &Properties - { - &self.properties - } - - pub fn properties_mut(&mut self) -> &mut Properties - { - &mut self.properties - } - - #[must_use] - pub fn dimensions(&self) -> &Dimens<u32> - { - &self.dimensions - } - - #[must_use] - pub fn pixel_data_format(&self) -> PixelDataFormat - { - self.pixel_data_format - } - - #[must_use] - pub fn image(&self) -> &DynamicImage - { - &self.image - } -} - -impl Drop for Texture -{ - fn drop(&mut self) - { - NEXT_ID.fetch_sub(1, Ordering::Relaxed); - } -} - -/// Texture builder. -#[derive(Debug, Default, Clone)] -pub struct Builder -{ - properties: Properties, -} - -impl Builder -{ - pub fn properties(mut self, properties: Properties) -> Self - { - self.properties = properties; - self - } - - /// Opens a image as a texture. - /// - /// # Errors - /// Will return `Err` if: - /// - Opening the image fails - /// - Decoding the image fails - /// - The image data is in a unsupported format - pub fn open(&self, path: &(impl AsRef<Path> + ?Sized)) -> Result<Texture, Error> - { - let image = ImageReader::open(path) - .map_err(Error::OpenImageFailed)? - .decode() - .map_err(Error::DecodeImageFailed)?; - - let pixel_data_format = match &image { - DynamicImage::ImageRgb8(_) => PixelDataFormat::Rgb8, - DynamicImage::ImageRgba8(_) => PixelDataFormat::Rgba8, - _ => { - return Err(Error::UnsupportedImageDataFormat); - } - }; - - let dimensions = Dimens { - width: image.width(), - height: image.height(), - }; - - let id = NEXT_ID.fetch_add(1, Ordering::Relaxed); - - Ok(Texture { - id: Id::new(id), - image, - pixel_data_format, - dimensions, - properties: self.properties.clone(), - }) - } - - #[must_use] - pub fn build_with_single_color( - &self, - dimensions: &Dimens<u32>, - color: &Color<u8>, - ) -> Texture - { - let image = RgbImage::from_pixel( - dimensions.width, - dimensions.height, - Rgb([color.red, color.green, color.blue]), - ); - - let id = NEXT_ID.fetch_add(1, Ordering::Relaxed); - - Texture { - id: Id::new(id), - image: image.into(), - pixel_data_format: PixelDataFormat::Rgb8, - dimensions: *dimensions, - properties: self.properties.clone(), - } - } -} - -/// Texture error. -#[derive(Debug, thiserror::Error)] -pub enum Error -{ - #[error("Failed to open texture image")] - OpenImageFailed(#[source] std::io::Error), - - #[error("Failed to decode texture image")] - DecodeImageFailed(#[source] ImageError), - - #[error("Unsupported image data format")] - UnsupportedImageDataFormat, + pub image: Image, + pub properties: Properties, } builder! { @@ -230,25 +60,79 @@ impl Default for PropertiesBuilder } } -/// Texture ID. #[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 + Repeat, + MirroredRepeat, + ClampToEdge, + ClampToBorder, +} + +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); + + assets.store_with_label( + WHITE_1X1_ASSET_LABEL.clone(), + Texture { + image: Image::from_color_and_alpha( + Dimens { width: 1, height: 1 }, + Color::WHITE_U8, + 1, + ), + properties: Properties::default(), + }, + ); +} + +fn import( + asset_submitter: &mut AssetSubmitter<'_>, + path: &Path, + settings: Option<&'_ ImportSettings>, +) -> Result<(), ImageError> +{ + asset_submitter.submit_store(Texture { + 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..05819bc 100644 --- a/engine/src/transform.rs +++ b/engine/src/transform.rs @@ -1,15 +1,54 @@ use ecs::Component; +use crate::builder; use crate::vector::Vec3; -/// A position in 3D space. +builder!( + #[builder(name = Builder, derives=(Debug))] + #[derive(Debug)] + #[non_exhaustive] + pub struct Transform + { + pub position: Vec3<f32>, + pub scale: Vec3<f32>, + } +); + +impl Transform +{ + pub fn builder() -> Builder + { + Builder::default() + } +} + +impl Default for Transform +{ + fn default() -> Self + { + Self::builder().build() + } +} + +impl Default for Builder +{ + fn default() -> Self + { + Self { + position: Vec3::from(0.0), + scale: Vec3::from(1.0), + } + } +} + +/// A position in world space. #[derive(Debug, Default, Clone, Copy, Component)] -pub struct Position +pub struct WorldPosition { pub position: Vec3<f32>, } -impl From<Vec3<f32>> for Position +impl From<Vec3<f32>> for WorldPosition { fn from(position: Vec3<f32>) -> Self { diff --git a/engine/src/util.rs b/engine/src/util.rs index 0f6c78c..f18a9c7 100644 --- a/engine/src/util.rs +++ b/engine/src/util.rs @@ -1,4 +1,98 @@ -use std::marker::PhantomData; +use ecs::util::VecExt; + +#[derive(Debug)] +pub struct MapVec<Key: Ord, Value> +{ + inner: Vec<(Key, Value)>, +} + +impl<Key: Ord, Value> MapVec<Key, Value> +{ + pub fn insert(&mut self, key: Key, value: Value) + { + self.inner + .insert_at_part_pt_by_key((key, value), |(a_key, _)| a_key); + } + + pub fn remove(&mut self, key: Key) -> Option<Value> + { + let index = self + .inner + .binary_search_by_key(&&key, |(a_key, _)| a_key) + .ok()?; + + let (_, value) = self.inner.remove(index); + + Some(value) + } + + pub fn get(&self, key: &Key) -> Option<&Value> + { + let index = self + .inner + .binary_search_by_key(&key, |(a_key, _)| a_key) + .ok()?; + + let Some((_, value)) = self.inner.get(index) else { + unreachable!(); // Reason: Index from binary search cannot be OOB + }; + + Some(value) + } + + pub fn get_mut(&mut self, key: &Key) -> Option<&mut Value> + { + let index = self + .inner + .binary_search_by_key(&key, |(a_key, _)| a_key) + .ok()?; + + let Some((_, value)) = self.inner.get_mut(index) else { + unreachable!(); // Reason: Index from binary search cannot be OOB + }; + + Some(value) + } + + pub fn values(&self) -> impl Iterator<Item = &Value> + { + self.inner.iter().map(|(_, value)| value) + } +} + +impl<Key: Ord, Value> Default for MapVec<Key, Value> +{ + fn default() -> Self + { + Self { inner: Vec::new() } + } +} + +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() }) + } +} macro_rules! try_option { ($expr: expr) => { @@ -25,6 +119,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])* @@ -36,7 +142,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, )* } @@ -46,7 +153,7 @@ macro_rules! builder { $visibility struct $name { $( - $(#[$field_attr])* + $(#[doc = $field_doc])* $field_visibility $field: $field_type, )* } @@ -62,16 +169,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, @@ -82,6 +196,7 @@ macro_rules! builder { impl From<$name> for $builder_name { + #[allow(unused_variables)] fn from(built: $name) -> Self { Self { @@ -93,77 +208,3 @@ macro_rules! builder { } }; } - -pub(crate) use builder; - -pub enum RefOrValue<'a, T> -{ - Ref(&'a T), - Value(Option<T>), -} - -impl<'a, T> RefOrValue<'a, T> -{ - pub fn get(&self) -> Option<&T> - { - match self { - Self::Ref(val_ref) => Some(val_ref), - Self::Value(val_cell) => val_cell.as_ref(), - } - } -} - -#[derive(Debug)] -pub struct Defer<'func, Func, Data> -where - Func: FnMut(&mut Data) + 'func, -{ - func: Func, - pub data: Data, - _pd: PhantomData<&'func ()>, -} - -impl<'func, Func, Data> Defer<'func, Func, Data> -where - Func: FnMut(&mut Data) + 'func, -{ - pub fn new(data: Data, func: Func) -> Self - { - Self { func, data, _pd: PhantomData } - } -} - -impl<'func, Func, Data> Drop for Defer<'func, Func, Data> -where - Func: FnMut(&mut Data) + 'func, -{ - fn drop(&mut self) - { - (self.func)(&mut self.data) - } -} - -/// Defines a function that will be called at the end of the current scope. -/// -/// Only captured variables that are later mutably borrowed needs to specified as -/// captures. -macro_rules! defer { - (|$capture: ident| {$($tt: tt)*}) => { - // This uses the automatic temporary lifetime extension behaviour introduced - // in Rust 1.79.0 (https://blog.rust-lang.org/2024/06/13/Rust-1.79.0.html) to - // create a unnamable variable for the Defer struct. The variable should be - // unnamable so that it cannot be missused and so that this macro can be used - // multiple times without having to give it a identifier for the Defer struct - // variable - let Defer { data: $capture, .. } = if true { - &Defer::new($capture, |$capture| { - $($tt)* - }) - } - else { - unreachable!(); - }; - }; -} - -pub(crate) use defer; diff --git a/engine/src/vertex.rs b/engine/src/vertex.rs deleted file mode 100644 index 30640c4..0000000 --- a/engine/src/vertex.rs +++ /dev/null @@ -1,69 +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, PartialEq)] -#[repr(C)] -#[non_exhaustive] -pub struct Vertex -{ - pub pos: Vec3<f32>, - pub texture_coords: Vec2<f32>, - pub 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 00c360e..0000000 --- a/engine/src/window.rs +++ /dev/null @@ -1,752 +0,0 @@ -use std::borrow::Cow; -use std::ffi::{CStr, CString}; - -use bitflags::bitflags; -use ecs::actions::Actions; -use ecs::extension::Collector as ExtensionCollector; -use ecs::phase::{Phase, PRESENT as PRESENT_PHASE, START as START_PHASE}; -use ecs::relationship::{ChildOf, Relationship}; -use ecs::sole::Single; -use ecs::{static_entity, Sole}; -use glfw::window::{Hint as WindowCreationHint, HintValue as WindowCreationHintValue}; -use glfw::WindowSize; -use util_macros::VariantArr; - -use crate::data_types::dimens::Dimens; -use crate::vector::Vec2; - -static_entity!( - pub UPDATE_PHASE, - (Phase, <Relationship<ChildOf, Phase>>::new(*PRESENT_PHASE)) -); - -#[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.to_glfw_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.to_glfw_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(move |key, scancode, key_state, key_modifiers| { - let Some(key_state) = KeyState::from_glfw_key_state(key_state) else { - return; - }; - - callback( - Key::from_glfw_key(key), - scancode, - key_state, - KeyModifiers::from_bits_truncate(key_modifiers.bits()), - ) - }); - } - - /// 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 mouse button callback. The given function is called when a mouse - /// button enters a new state. - pub fn set_mouse_button_callback( - &self, - callback: impl Fn(MouseButton, MouseButtonState, KeyModifiers) + 'static, - ) - { - self.inner.set_mouse_button_callback( - move |mouse_button, mouse_button_state, key_modifiers| { - callback( - MouseButton::from_glfw_mouse_button(mouse_button), - MouseButtonState::from_glfw_mouse_button_state(mouse_button_state), - KeyModifiers::from_bits_truncate(key_modifiers.bits()), - ) - }, - ); - } - - /// 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 -{ - /// Sets whether the OpenGL context should be created in debug mode, which may - /// provide additional error and diagnostic reporting functionality. - pub fn opengl_debug_context(mut self, enabled: bool) -> Self - { - self.inner = self.inner.hint( - WindowCreationHint::OpenGLDebugContext, - WindowCreationHintValue::Bool(enabled), - ); - - self - } - - /// Set the desired number of samples to use for multisampling. Zero disables - /// multisampling. - pub fn multisampling_sample_count(mut self, sample_count: u16) -> Self - { - self.inner = self.inner.hint( - WindowCreationHint::Samples, - WindowCreationHintValue::Number(sample_count as i32), - ); - - 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( - WindowCreationHint::OpenGLDebugContext, - WindowCreationHintValue::Bool(true), - ); - - let window = builder.create( - &WindowSize { - width: size.width, - height: size.height, - }, - title, - )?; - - Ok(Window { inner: window }) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, VariantArr)] -#[variant_arr(name = KEYS)] -pub enum Key -{ - Space, - Apostrophe, - Comma, - Minus, - Period, - Slash, - Digit0, - Digit1, - Digit2, - Digit3, - Digit4, - Digit5, - Digit6, - Digit7, - Digit8, - Digit9, - Semicolon, - Equal, - A, - B, - C, - D, - E, - F, - G, - H, - I, - J, - K, - L, - M, - N, - O, - P, - Q, - R, - S, - T, - U, - V, - W, - X, - Y, - Z, - LeftBracket, - Backslash, - RightBracket, - GraveAccent, - World1, - World2, - Escape, - Enter, - Tab, - Backspace, - Insert, - Delete, - Right, - Left, - Down, - Up, - PageUp, - PageDown, - Home, - End, - CapsLock, - ScrollLock, - NumLock, - PrintScreen, - Pause, - F1, - F2, - F3, - F4, - F5, - F6, - F7, - F8, - F9, - F10, - F11, - F12, - F13, - F14, - F15, - F16, - F17, - F18, - F19, - F20, - F21, - F22, - F23, - F24, - F25, - Kp0, - Kp1, - Kp2, - Kp3, - Kp4, - Kp5, - Kp6, - Kp7, - Kp8, - Kp9, - KpDecimal, - KpDivide, - KpMultiply, - KpSubtract, - KpAdd, - KpEnter, - KpEqual, - LeftShift, - LeftControl, - LeftAlt, - LeftSuper, - RightShift, - RightControl, - RightAlt, - RightSuper, - Menu, -} - -impl Key -{ - fn from_glfw_key(glfw_key: glfw::window::Key) -> Self - { - match glfw_key { - glfw::window::Key::Space => Self::Space, - glfw::window::Key::Apostrophe => Self::Apostrophe, - glfw::window::Key::Comma => Self::Comma, - glfw::window::Key::Minus => Self::Minus, - glfw::window::Key::Period => Self::Period, - glfw::window::Key::Slash => Self::Slash, - glfw::window::Key::Digit0 => Self::Digit0, - glfw::window::Key::Digit1 => Self::Digit1, - glfw::window::Key::Digit2 => Self::Digit2, - glfw::window::Key::Digit3 => Self::Digit3, - glfw::window::Key::Digit4 => Self::Digit4, - glfw::window::Key::Digit5 => Self::Digit5, - glfw::window::Key::Digit6 => Self::Digit6, - glfw::window::Key::Digit7 => Self::Digit7, - glfw::window::Key::Digit8 => Self::Digit8, - glfw::window::Key::Digit9 => Self::Digit9, - glfw::window::Key::Semicolon => Self::Semicolon, - glfw::window::Key::Equal => Self::Equal, - glfw::window::Key::A => Self::A, - glfw::window::Key::B => Self::B, - glfw::window::Key::C => Self::C, - glfw::window::Key::D => Self::D, - glfw::window::Key::E => Self::E, - glfw::window::Key::F => Self::F, - glfw::window::Key::G => Self::G, - glfw::window::Key::H => Self::H, - glfw::window::Key::I => Self::I, - glfw::window::Key::J => Self::J, - glfw::window::Key::K => Self::K, - glfw::window::Key::L => Self::L, - glfw::window::Key::M => Self::M, - glfw::window::Key::N => Self::N, - glfw::window::Key::O => Self::O, - glfw::window::Key::P => Self::P, - glfw::window::Key::Q => Self::Q, - glfw::window::Key::R => Self::R, - glfw::window::Key::S => Self::S, - glfw::window::Key::T => Self::T, - glfw::window::Key::U => Self::U, - glfw::window::Key::V => Self::V, - glfw::window::Key::W => Self::W, - glfw::window::Key::X => Self::X, - glfw::window::Key::Y => Self::Y, - glfw::window::Key::Z => Self::Z, - glfw::window::Key::LeftBracket => Self::LeftBracket, - glfw::window::Key::Backslash => Self::Backslash, - glfw::window::Key::RightBracket => Self::RightBracket, - glfw::window::Key::GraveAccent => Self::GraveAccent, - glfw::window::Key::World1 => Self::World1, - glfw::window::Key::World2 => Self::World2, - glfw::window::Key::Escape => Self::Escape, - glfw::window::Key::Enter => Self::Enter, - glfw::window::Key::Tab => Self::Tab, - glfw::window::Key::Backspace => Self::Backspace, - glfw::window::Key::Insert => Self::Insert, - glfw::window::Key::Delete => Self::Delete, - glfw::window::Key::Right => Self::Right, - glfw::window::Key::Left => Self::Left, - glfw::window::Key::Down => Self::Down, - glfw::window::Key::Up => Self::Up, - glfw::window::Key::PageUp => Self::PageUp, - glfw::window::Key::PageDown => Self::PageDown, - glfw::window::Key::Home => Self::Home, - glfw::window::Key::End => Self::End, - glfw::window::Key::CapsLock => Self::CapsLock, - glfw::window::Key::ScrollLock => Self::ScrollLock, - glfw::window::Key::NumLock => Self::NumLock, - glfw::window::Key::PrintScreen => Self::PrintScreen, - glfw::window::Key::Pause => Self::Pause, - glfw::window::Key::F1 => Self::F1, - glfw::window::Key::F2 => Self::F2, - glfw::window::Key::F3 => Self::F3, - glfw::window::Key::F4 => Self::F4, - glfw::window::Key::F5 => Self::F5, - glfw::window::Key::F6 => Self::F6, - glfw::window::Key::F7 => Self::F7, - glfw::window::Key::F8 => Self::F8, - glfw::window::Key::F9 => Self::F9, - glfw::window::Key::F10 => Self::F10, - glfw::window::Key::F11 => Self::F11, - glfw::window::Key::F12 => Self::F12, - glfw::window::Key::F13 => Self::F13, - glfw::window::Key::F14 => Self::F14, - glfw::window::Key::F15 => Self::F15, - glfw::window::Key::F16 => Self::F16, - glfw::window::Key::F17 => Self::F17, - glfw::window::Key::F18 => Self::F18, - glfw::window::Key::F19 => Self::F19, - glfw::window::Key::F20 => Self::F20, - glfw::window::Key::F21 => Self::F21, - glfw::window::Key::F22 => Self::F22, - glfw::window::Key::F23 => Self::F23, - glfw::window::Key::F24 => Self::F24, - glfw::window::Key::F25 => Self::F25, - glfw::window::Key::Kp0 => Self::Kp0, - glfw::window::Key::Kp1 => Self::Kp1, - glfw::window::Key::Kp2 => Self::Kp2, - glfw::window::Key::Kp3 => Self::Kp3, - glfw::window::Key::Kp4 => Self::Kp4, - glfw::window::Key::Kp5 => Self::Kp5, - glfw::window::Key::Kp6 => Self::Kp6, - glfw::window::Key::Kp7 => Self::Kp7, - glfw::window::Key::Kp8 => Self::Kp8, - glfw::window::Key::Kp9 => Self::Kp9, - glfw::window::Key::KpDecimal => Self::KpDecimal, - glfw::window::Key::KpDivide => Self::KpDivide, - glfw::window::Key::KpMultiply => Self::KpMultiply, - glfw::window::Key::KpSubtract => Self::KpSubtract, - glfw::window::Key::KpAdd => Self::KpAdd, - glfw::window::Key::KpEnter => Self::KpEnter, - glfw::window::Key::KpEqual => Self::KpEqual, - glfw::window::Key::LeftShift => Self::LeftShift, - glfw::window::Key::LeftControl => Self::LeftControl, - glfw::window::Key::LeftAlt => Self::LeftAlt, - glfw::window::Key::LeftSuper => Self::LeftSuper, - glfw::window::Key::RightShift => Self::RightShift, - glfw::window::Key::RightControl => Self::RightControl, - glfw::window::Key::RightAlt => Self::RightAlt, - glfw::window::Key::RightSuper => Self::RightSuper, - glfw::window::Key::Menu => Self::Menu, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum KeyState -{ - Pressed, - Released, -} - -impl KeyState -{ - fn from_glfw_key_state(glfw_key_state: glfw::window::KeyState) -> Option<Self> - { - match glfw_key_state { - glfw::window::KeyState::Pressed => Some(Self::Pressed), - glfw::window::KeyState::Released => Some(Self::Released), - glfw::window::KeyState::Repeat => None, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum MouseButton -{ - One, - Two, - Three, - Four, - Five, - Six, - Seven, - Eight, -} - -impl MouseButton -{ - pub const LEFT: Self = Self::One; - pub const MIDDLE: Self = Self::Three; - pub const RIGHT: Self = Self::Two; - - fn from_glfw_mouse_button(mouse_button: glfw::window::MouseButton) -> Self - { - match mouse_button { - glfw::window::MouseButton::One => Self::One, - glfw::window::MouseButton::Two => Self::Two, - glfw::window::MouseButton::Three => Self::Three, - glfw::window::MouseButton::Four => Self::Four, - glfw::window::MouseButton::Five => Self::Five, - glfw::window::MouseButton::Six => Self::Six, - glfw::window::MouseButton::Seven => Self::Seven, - glfw::window::MouseButton::Eight => Self::Eight, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum MouseButtonState -{ - Pressed, - Released, -} - -impl MouseButtonState -{ - fn from_glfw_mouse_button_state( - mouse_button_state: glfw::window::MouseButtonState, - ) -> Self - { - match mouse_button_state { - glfw::window::MouseButtonState::Pressed => Self::Pressed, - glfw::window::MouseButtonState::Released => Self::Released, - } - } -} - -bitflags! { - #[derive(Debug, Clone, Copy)] - pub struct KeyModifiers: i32 { - const SHIFT = glfw::window::KeyModifiers::SHIFT.bits(); - const CONTROL = glfw::window::KeyModifiers::CONTROL.bits(); - const ALT = glfw::window::KeyModifiers::ALT.bits(); - const SUPER = glfw::window::KeyModifiers::SUPER.bits(); - const CAPS_LOCK = glfw::window::KeyModifiers::CAPS_LOCK.bits(); - const NUM_LOCK = glfw::window::KeyModifiers::NUM_LOCK.bits(); - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum CursorMode -{ - /// Hides and grabs the cursor, providing virtual and unlimited cursor movement. - Disabled, - - /// Makes the cursor invisible when it is over the content area of the window but - /// does not restrict the cursor from leaving. - Hidden, - - /// Makes the cursor visible and behaving normally. - Normal, -} - -impl CursorMode -{ - fn to_glfw_cursor_mode(self) -> glfw::window::CursorMode - { - match self { - Self::Disabled => glfw::window::CursorMode::Disabled, - Self::Hidden => glfw::window::CursorMode::Hidden, - Self::Normal => glfw::window::CursorMode::Normal, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum InputMode -{ - /// When the cursor is disabled, raw (unscaled and unaccelerated) mouse motion can be - /// enabled if available. - /// - /// Raw mouse motion is closer to the actual motion of the mouse across a surface. It - /// is not affected by the scaling and acceleration applied to the motion of the - /// desktop cursor. That processing is suitable for a cursor while raw motion is - /// better for controlling for example a 3D camera. Because of this, raw mouse motion - /// is only provided when the cursor is disabled. - RawMouseMotion, -} - -impl InputMode -{ - fn to_glfw_input_mode(self) -> glfw::window::InputMode - { - match self { - Self::RawMouseMotion => glfw::window::InputMode::RawMouseMotion, - } - } -} - -#[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(*START_PHASE, initialize); - collector.add_system(*UPDATE_PHASE, 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..e07ba3e --- /dev/null +++ b/engine/src/windowing.rs @@ -0,0 +1,728 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Weak}; +use std::thread::{Builder as ThreadBuilder, JoinHandle as ThreadJoinHandle}; + +use crossbeam_channel::{ + Receiver as ChannelReceiver, + Sender as ChannelSender, + TrySendError, + bounded as bounded_channel, +}; +use ecs::actions::Actions; +use ecs::component::Component; +use ecs::entity::obtainer::Obtainer as EntityObtainer; +use ecs::event::component::{Added, Changed, EventMatchExt, Removed}; +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{Phase, UPDATE as UPDATE_PHASE}; +use ecs::sole::Single; +use ecs::system::observer::Observe; +use ecs::uid::Uid; +use ecs::{Query, Sole, declare_entity}; +use raw_window_handle::{DisplayHandle, HandleError, HasDisplayHandle, WindowHandle}; +use winit::application::ApplicationHandler; +use winit::dpi::PhysicalPosition; +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::data_types::dimens::Dimens; +use crate::util::MapVec; +use crate::vector::Vec2; +use crate::windowing::keyboard::{Key, KeyState, Keyboard, UnknownKeyCodeError}; +use crate::windowing::mouse::{ + Button as MouseButton, + ButtonState as MouseButtonState, + Buttons as MouseButtons, + Mouse, +}; +use crate::windowing::window::{ + Closed as WindowClosed, + CreationAttributes as WindowCreationAttributes, + CreationReady as WindowCreationReady, + CursorGrabMode, + Id as WindowId, + Window, +}; + +pub mod keyboard; +pub mod mouse; +pub mod window; + +const MESSAGE_FROM_APP_CHANNEL_CAP: usize = 512; + +const MESSAGE_TO_APP_CHANNEL_CAP: usize = 16; // Increase if more messages are added + +declare_entity!( + pub PHASE, + ( + Phase, + Pair::builder() + .relation::<ChildOf>() + .target_id(*UPDATE_PHASE) + .build() + ) +); + +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct Extension {} + +impl ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: ecs::extension::Collector<'_>) + { + collector.add_sole(Context::default()).ok(); + collector.add_sole(Keyboard::default()).ok(); + collector.add_sole(Mouse::default()).ok(); + collector.add_sole(MouseButtons::default()).ok(); + + collector.add_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>, +) +{ + 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, +) +{ + keyboard.make_key_states_previous(); + mouse_buttons.make_states_previous(); + mouse.curr_tick_position_delta = Vec2::default(); + + let Context { + ref message_from_app_receiver, + ref mut display_handle, + ref mut windows, + .. + } = *context; + + for message in message_from_app_receiver.try_iter() { + match message { + MessageFromApp::Init(new_display_handle) => { + *display_handle = Some(new_display_handle); + } + 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::debug!( + 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.set_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(); + } + MessageFromApp::KeyboardKeyStateChanged(key, key_state) => { + keyboard.set_key_state(key, key_state); + } + MessageFromApp::MouseMovedTo { position } => { + mouse.position = position; + } + MessageFromApp::MouseMoved { position_delta } => { + mouse.curr_tick_position_delta += position_delta; + } + MessageFromApp::MouseButtonStateChanged(mouse_button, mouse_button_state) => { + mouse_buttons.set(mouse_button, mouse_button_state); + } + } + } +} + +fn handle_window_changed( + observe: Observe<'_, Pair<Changed, Window>>, + context: Single<Context>, +) +{ + for evt_match in &observe { + let window_ent_id = evt_match.entity_id(); + + let window = evt_match.get_ent_target_comp(); + + 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; + }; + + window.apply(winit_window); + + 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, +) +{ + 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 +{ + _thread: ThreadJoinHandle<()>, + is_dropped: Arc<AtomicBool>, + message_from_app_receiver: ChannelReceiver<MessageFromApp>, + message_to_app_sender: ChannelSender<MessageToApp>, + display_handle: Option<OwnedDisplayHandle>, + windows: MapVec<WindowId, (Arc<winit::window::Window>, Uid)>, +} + +impl Context +{ + pub fn display_handle(&self) -> Option<DisplayHandle<'_>> + { + let display_handle = self.display_handle.as_ref()?; + + display_handle.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() + } + }) + } + + fn try_send_message_to_app(&self, message: MessageToApp) + { + if let Err(err) = self.message_to_app_sender.try_send(message) { + let error = match &err { + TrySendError::Full(_) => TrySendError::Full(()), + TrySendError::Disconnected(_) => TrySendError::Disconnected(()), + }; + + let message = err.into_inner(); + + tracing::error!("Failed to send message {error}: {message:?}"); + } + } +} + +impl Default for Context +{ + fn default() -> Self + { + let is_dropped = Arc::new(AtomicBool::new(false)); + + let is_dropped_b = is_dropped.clone(); + + let (message_from_app_sender, message_from_app_receiver) = + bounded_channel::<MessageFromApp>(MESSAGE_FROM_APP_CHANNEL_CAP); + + let message_from_app_receiver_b = message_from_app_receiver.clone(); + + let (message_to_app_sender, message_to_app_receiver) = + bounded_channel::<MessageToApp>(MESSAGE_TO_APP_CHANNEL_CAP); + + Self { + _thread: ThreadBuilder::new() + .name("windowing".to_string()) + .spawn(move || { + let mut app = App { + message_from_app_sender, + message_from_app_receiver: message_from_app_receiver_b, + message_to_app_receiver, + is_dropped: is_dropped_b, + windows: MapVec::default(), + focused_window_id: None, + }; + + let event_loop = match create_event_loop() { + Ok(event_loop) => event_loop, + Err(err) => { + tracing::error!("Failed to create event loop: {err}"); + return; + } + }; + + event_loop.set_control_flow(EventLoopControlFlow::Poll); + + if let Err(err) = event_loop.run_app(&mut app) { + tracing::error!("Event loop error occurred: {err}"); + } + }) + .expect("Failed to create windowing thread"), + is_dropped, + message_from_app_receiver, + message_to_app_sender, + display_handle: None, + windows: MapVec::default(), + } + } +} + +impl Drop for Context +{ + fn drop(&mut self) + { + self.is_dropped.store(true, Ordering::Relaxed); + } +} + +fn create_event_loop() -> Result<EventLoop<()>, EventLoopError> +{ + let mut event_loop_builder = EventLoop::builder(); + + #[cfg(any(x11_platform, wayland_platform))] + winit::platform::x11::EventLoopBuilderExtX11::with_any_thread( + &mut event_loop_builder, + true, + ); + + #[cfg(windows)] + winit::platform::windows::EventLoopBuilderExtWindows::with_any_thread( + &mut event_loop_builder, + true, + ); + + #[cfg(not(any(x11_platform, wayland_platform, windows)))] + compile_error!("Unsupported platform"); + + event_loop_builder.build() +} + +#[derive(Debug)] +enum MessageFromApp +{ + Init(OwnedDisplayHandle), + WindowCreated(Uid, Arc<WinitWindow>, WindowCreationAttributes), + WindowResized(WindowId, Dimens<u32>), + WindowCloseRequested(WindowId), + WindowScaleFactorChanged(WindowId, f64), + KeyboardKeyStateChanged(Key, KeyState), + MouseMovedTo + { + position: Vec2<f64>, + }, + MouseMoved + { + position_delta: Vec2<f64>, + }, + MouseButtonStateChanged(MouseButton, MouseButtonState), +} + +#[derive(Debug)] +enum MessageToApp +{ + CreateWindow(Uid, WindowCreationAttributes), + SetWindowCursorGrabMode(WindowId, CursorGrabMode), +} + +#[derive(Debug)] +struct App +{ + message_from_app_sender: ChannelSender<MessageFromApp>, + message_from_app_receiver: ChannelReceiver<MessageFromApp>, + message_to_app_receiver: ChannelReceiver<MessageToApp>, + is_dropped: Arc<AtomicBool>, + windows: MapVec<WindowId, (Weak<WinitWindow>, WindowSettings)>, + focused_window_id: Option<WindowId>, +} + +impl App +{ + #[tracing::instrument(skip_all)] + fn handle_received_messages(&mut self, event_loop: &ActiveEventLoop) + { + for message in self.message_to_app_receiver.try_iter() { + 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_inner()) + { + Ok(window) => window, + Err(err) => { + tracing::error!("Failed to create window: {err}"); + continue; + } + }, + ); + + self.windows.insert( + WindowId::from_inner(winit_window.id()), + (Arc::downgrade(&winit_window), WindowSettings::default()), + ); + + 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.message_from_app_sender.is_full() { + tracing::warn!( + "Message channel is full! Dropping oldest message from channel" + ); + + self.message_from_app_receiver.try_recv().ok(); + } + + if let Err(err) = self.message_from_app_sender.try_send(message) { + let error = match &err { + TrySendError::Full(_) => TrySendError::Full(()), + TrySendError::Disconnected(_) => TrySendError::Disconnected(()), + }; + + let message = err.into_inner(); + + tracing::error!("Failed to send message {error}: {message:?}"); + } + } +} + +impl ApplicationHandler for App +{ + fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) + { + match cause { + StartCause::Init => { + self.send_message(MessageFromApp::Init( + event_loop.owned_display_handle(), + )); + } + StartCause::Poll => { + if self.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 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; + } + }; + + self.send_message(MessageFromApp::KeyboardKeyStateChanged( + key, + keyboard_event.state.into(), + )); + } + WindowEvent::CursorMoved { device_id: _, position } => { + self.send_message(MessageFromApp::MouseMovedTo { + position: Vec2 { x: position.x, y: position.y }, + }); + } + WindowEvent::MouseInput { device_id: _, state, button } => { + self.send_message(MessageFromApp::MouseButtonStateChanged( + button.into(), + state.into(), + )); + } + WindowEvent::Focused(is_focused) => { + if is_focused { + self.focused_window_id = Some(WindowId::from_inner(window_id)); + } else { + self.focused_window_id = None; + } + } + 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 } => { + self.send_message(MessageFromApp::MouseMoved { + position_delta: Vec2 { x: delta.0, y: delta.1 }, + }); + + let Some(focused_window_id) = self.focused_window_id else { + return; + }; + + let Some((focused_window, focused_window_settings)) = + self.windows.get(&focused_window_id) + else { + tracing::error!( + window_id=?focused_window_id, + "Focused window not found" + ); + return; + }; + + if focused_window_settings.cursor_grab_mode != CursorGrabMode::Locked { + return; + } + + // TODO: This might need to be optimized + let Some(focused_window) = focused_window.upgrade() else { + return; + }; + + let focused_window_size = focused_window.inner_size(); + + if let Err(err) = focused_window.set_cursor_position(PhysicalPosition { + x: focused_window_size.width / 2, + y: focused_window_size.height / 2, + }) { + tracing::error!( + window_id=?focused_window_id, + "Failed to set cursor position in focused window: {err}" + ); + }; + } + _ => {} + } + } +} + +#[derive(Debug, Default)] +struct WindowSettings +{ + cursor_grab_mode: CursorGrabMode, +} diff --git a/engine/src/windowing/keyboard.rs b/engine/src/windowing/keyboard.rs new file mode 100644 index 0000000..a1c3e22 --- /dev/null +++ b/engine/src/windowing/keyboard.rs @@ -0,0 +1,791 @@ +use std::collections::HashMap; + +use ecs::Sole; + +#[derive(Debug, Default, Sole)] +pub struct Keyboard +{ + map: HashMap<Key, KeyData>, +} + +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 + } + + #[must_use] + pub fn get_key_state(&self, key: Key) -> KeyState + { + let Some(key_data) = self.map.get(&key) else { + return KeyState::Released; + }; + + key_data.curr_state + } + + #[must_use] + pub fn get_prev_key_state(&self, key: Key) -> KeyState + { + let Some(key_data) = self.map.get(&key) else { + return KeyState::Released; + }; + + key_data.previous_state + } + + pub fn set_key_state(&mut self, key: Key, key_state: KeyState) + { + let key_data = self.map.entry(key).or_default(); + + key_data.curr_state = key_state; + } + + pub fn make_key_states_previous(&mut self) + { + for key_data in self.map.values_mut() { + key_data.previous_state = key_data.curr_state; + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[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, + } + } +} + +#[derive(Debug)] +struct KeyData +{ + curr_state: KeyState, + previous_state: KeyState, +} + +impl Default for KeyData +{ + fn default() -> Self + { + KeyData { + curr_state: KeyState::Released, + previous_state: KeyState::Released, + } + } +} diff --git a/engine/src/windowing/mouse.rs b/engine/src/windowing/mouse.rs new file mode 100644 index 0000000..0ea04e2 --- /dev/null +++ b/engine/src/windowing/mouse.rs @@ -0,0 +1,153 @@ +use std::collections::HashMap; + +use ecs::Sole; + +use crate::vector::Vec2; + +#[derive(Debug, Default, Clone, Sole)] +#[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: Vec2<f64>, +} + +/// 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)> + { + 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(crate) fn make_states_previous(&mut self) + { + for button_data in self.map.values_mut() { + 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 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..627bdec --- /dev/null +++ b/engine/src/windowing/window.rs @@ -0,0 +1,183 @@ +use std::borrow::Cow; + +use ecs::Component; + +use crate::data_types::dimens::Dimens; + +pub mod platform; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id +{ + inner: winit::window::WindowId, +} + +impl Id +{ + pub(crate) fn from_inner(inner: winit::window::WindowId) -> Self + { + Self { inner } + } +} + +macro_rules! impl_creation_attributes_field_fns { + ($field: ident, ($($getter_ret_pre: tt)?), $getter_ret_type: ty, $field_type: ty) => { + impl CreationAttributes + { + pub fn $field(&self) -> $getter_ret_type + { + $($getter_ret_pre)? self.attrs.$field + } + + paste::paste! { + pub fn [<with_ $field>](mut self, $field: impl Into<$field_type>) -> Self { + self.attrs.$field = $field.into(); + self + } + } + } + }; +} + +#[derive(Debug, Component, Clone)] +#[non_exhaustive] +pub struct CreationAttributes +{ + attrs: winit::window::WindowAttributes, +} + +impl_creation_attributes_field_fns!(title, (&), &str, String); +impl_creation_attributes_field_fns!(transparent, (), bool, bool); + +impl CreationAttributes +{ + #[cfg(target_os = "linux")] + pub fn with_x11_visual(mut self, visual_id: XVisualID) -> Self + { + use winit::platform::x11::WindowAttributesExtX11; + + self.attrs = self.attrs.with_x11_visual(visual_id); + + self + } +} + +impl CreationAttributes +{ + pub(crate) fn into_inner(self) -> winit::window::WindowAttributes + { + self.attrs + } +} + +impl Default for CreationAttributes +{ + fn default() -> Self + { + CreationAttributes { + attrs: winit::window::WindowAttributes::default().with_title("Application"), + } + } +} + +#[derive(Debug, Component, Clone, Copy)] +pub struct CreationReady; + +#[derive(Debug, Component)] +#[non_exhaustive] +pub struct Window +{ + pub title: Cow<'static, str>, + pub cursor_visible: bool, + pub cursor_grab_mode: CursorGrabMode, + wid: Id, + inner_size: Dimens<u32>, + scale_factor: f64, +} + +impl Window +{ + pub fn wid(&self) -> Id + { + self.wid + } + + pub fn inner_size(&self) -> &Dimens<u32> + { + &self.inner_size + } + + 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().to_string().into(), + cursor_visible: true, + cursor_grab_mode: CursorGrabMode::None, + wid: Id::from_inner(winit_window.id()), + inner_size: winit_window.inner_size().into(), + scale_factor: winit_window.scale_factor(), + } + } + + pub(crate) fn apply(&self, winit_window: &winit::window::Window) + { + winit_window.set_title(&self.title); + winit_window.set_cursor_visible(self.cursor_visible); + } + + pub(crate) fn set_inner_size(&mut self, inner_size: Dimens<u32>) + { + self.inner_size = inner_size; + } + + pub(crate) fn set_scale_factor(&mut self, scale_factor: f64) + { + self.scale_factor = scale_factor; + } +} + +#[derive(Debug, Component)] +pub struct Closed; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +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; + +impl<P> From<winit::dpi::PhysicalSize<P>> for Dimens<P> +{ + fn from(size: winit::dpi::PhysicalSize<P>) -> Self + { + Self { + width: size.width, + height: size.height, + } + } +} + +impl<P> From<Dimens<P>> for winit::dpi::PhysicalSize<P> +{ + fn from(dimens: Dimens<P>) -> Self + { + Self { + width: dimens.width, + height: dimens.height, + } + } +} diff --git a/engine/src/windowing/window/platform.rs b/engine/src/windowing/window/platform.rs new file mode 100644 index 0000000..f3908a2 --- /dev/null +++ b/engine/src/windowing/window/platform.rs @@ -0,0 +1,12 @@ +#[cfg(x11_platform)] +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..48cb2ff --- /dev/null +++ b/engine/src/work_queue.rs @@ -0,0 +1,47 @@ +use std::marker::PhantomData; +use std::sync::mpsc::{Sender as MpscSender, channel as mpsc_channel}; +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: 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>>(); + + Self { + work_sender, + _thread: ThreadBuilder::new() + .name(name.to_string()) + .spawn(move || { + let work_receiver = work_receiver; + + while let Ok(work) = work_receiver.recv() { + (work.func)(work.user_data); + } + }) + .expect("Failed to create work queue thread"), + _pd: PhantomData, + } + } + + pub fn add_work(&self, work: Work<UserData>) + { + if self.work_sender.send(work).is_err() { + tracing::error!("Cannot add work to work queue. Work queue thread is dead"); + } + } +} |
