diff options
Diffstat (limited to 'engine/src')
52 files changed, 6498 insertions, 3610 deletions
diff --git a/engine/src/asset.rs b/engine/src/asset.rs new file mode 100644 index 0000000..db4d23c --- /dev/null +++ b/engine/src/asset.rs @@ -0,0 +1,777 @@ +use std::any::{type_name, Any}; +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::HashMap; +use std::convert::Infallible; +use std::ffi::{OsStr, OsString}; +use std::fmt::{Debug, Display}; +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::{ +    channel as mpsc_channel, +    Receiver as MpscReceiver, +    Sender as MpscSender, +}; +use std::sync::Arc; + +use ecs::phase::PRE_UPDATE as PRE_UPDATE_PHASE; +use ecs::sole::Single; +use ecs::Sole; + +use crate::work_queue::{Work, WorkQueue}; + +/// Asset label. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Label<'a> +{ +    pub path: Cow<'a, Path>, +    pub name: Option<Cow<'a, str>>, +} + +impl Label<'_> +{ +    pub fn to_owned(&self) -> LabelOwned +    { +        LabelOwned { +            path: self.path.to_path_buf(), +            name: self.name.as_ref().map(|name| name.to_string()), +        } +    } +} + +impl<'a> From<&'a Path> for Label<'a> +{ +    fn from(path: &'a Path) -> Self +    { +        Self { path: path.into(), name: None } +    } +} + +impl From<PathBuf> for Label<'_> +{ +    fn from(path: PathBuf) -> Self +    { +        Self { path: path.into(), name: None } +    } +} + +impl<'a> From<&'a LabelOwned> for Label<'a> +{ +    fn from(label: &'a LabelOwned) -> Self +    { +        Self { +            path: (&label.path).into(), +            name: label.name.as_ref().map(|name| Cow::Borrowed(name.as_str())), +        } +    } +} + +impl Display for Label<'_> +{ +    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result +    { +        write!(formatter, "{}", self.path.display())?; + +        if let Some(name) = &self.name { +            formatter.write_str("::")?; +            formatter.write_str(&name)?; +        } + +        Ok(()) +    } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LabelOwned +{ +    pub path: PathBuf, +    pub name: Option<String>, +} + +impl LabelOwned +{ +    pub fn to_label(&self) -> Label<'_> +    { +        Label { +            path: (&self.path).into(), +            name: self.name.as_ref().map(|name| Cow::Borrowed(name.as_str())), +        } +    } +} + +impl Display for LabelOwned +{ +    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result +    { +        write!(formatter, "{}", self.path.display())?; + +        if let Some(name) = &self.name { +            formatter.write_str("::")?; +            formatter.write_str(&name)?; +        } + +        Ok(()) +    } +} + +#[derive(Debug, Sole)] +pub struct Assets +{ +    assets: Vec<StoredAsset>, +    asset_lookup: RefCell<HashMap<LabelHash, LookupEntry>>, +    importers: Vec<WrappedImporterFn>, +    importer_lookup: HashMap<OsString, usize>, +    import_work_queue: WorkQueue<ImportWorkUserData>, +    import_work_msg_receiver: MpscReceiver<ImportWorkMessage>, +    import_work_msg_sender: MpscSender<ImportWorkMessage>, +} + +impl Assets +{ +    pub fn with_capacity(capacity: usize) -> Self +    { +        let (import_work_msg_sender, import_work_msg_receiver) = +            mpsc_channel::<ImportWorkMessage>(); + +        Self { +            assets: Vec::with_capacity(capacity), +            asset_lookup: RefCell::new(HashMap::with_capacity(capacity)), +            importers: Vec::new(), +            importer_lookup: HashMap::new(), +            import_work_queue: WorkQueue::new(), +            import_work_msg_receiver, +            import_work_msg_sender, +        } +    } + +    pub fn set_importer<'file_ext, AssetSettings, Err>( +        &mut self, +        file_extensions: impl IntoIterator<Item: Into<Cow<'file_ext, str>>>, +        func: impl Fn(&mut Submitter<'_>, &Path, Option<&AssetSettings>) -> Result<(), Err>, +    ) where +        AssetSettings: 'static, +        Err: std::error::Error + 'static, +    { +        self.importers.push(WrappedImporterFn::new(func)); + +        let importer_index = self.importers.len() - 1; + +        self.importer_lookup +            .extend(file_extensions.into_iter().map(|file_ext| { +                let file_ext: Cow<str> = file_ext.into(); + +                (file_ext.into_owned().into(), importer_index) +            })); +    } + +    #[tracing::instrument(skip_all, fields(asset_type=type_name::<Asset>()))] +    pub fn get<'this, 'handle, Asset: 'static + Send + Sync>( +        &'this self, +        handle: &'handle Handle<Asset>, +    ) -> Option<&'handle Asset> +    where +        'this: 'handle, +    { +        let LookupEntry::Occupied(asset_index) = +            *self.asset_lookup.borrow().get(&handle.id.label_hash)? +        else { +            return None; +        }; + +        let stored_asset = self.assets.get(asset_index).expect("Not possible"); + +        let Some(asset) = stored_asset.strong.downcast_ref::<Asset>() else { +            tracing::error!("Wrong asset type"); +            return None; +        }; + +        Some(asset) +    } + +    #[tracing::instrument(skip(self))] +    pub fn load<'i, Asset: 'static + Send + Sync>( +        &self, +        label: impl Into<Label<'i>> + Debug, +    ) -> Handle<Asset> +    { +        let label = label.into(); + +        let label_hash = LabelHash::new(&label); + +        let mut asset_lookup = self.asset_lookup.borrow_mut(); + +        if Self::is_pending(&asset_lookup, &label) { +            return Handle::new(label_hash); +        } + +        let Some(lookup_entry) = asset_lookup.get(&label_hash) else { +            self.add_import_work::<Infallible>( +                &label, +                label_hash, +                None, +                &mut asset_lookup, +            ); + +            return Handle::new(label_hash); +        }; + +        match *lookup_entry { +            LookupEntry::Occupied(asset_index) => { +                let stored_asset = self.assets.get(asset_index).expect("Not possible"); + +                if stored_asset.strong.downcast_ref::<Asset>().is_none() { +                    tracing::error!("Wrong asset type {}", type_name::<Asset>()); +                } +            } +            LookupEntry::Pending => {} +        } + +        Handle::new(label_hash) +    } + +    #[tracing::instrument(skip(self))] +    pub fn load_with_settings<'i, Asset, AssetSettings>( +        &self, +        label: impl Into<Label<'i>> + Debug, +        asset_settings: AssetSettings, +    ) -> Handle<Asset> +    where +        Asset: Send + Sync + 'static, +        AssetSettings: Send + Sync + Debug + 'static, +    { +        let label = label.into(); + +        let label_hash = LabelHash::new(&label); + +        let mut asset_lookup = self.asset_lookup.borrow_mut(); + +        if Self::is_pending(&asset_lookup, &label) { +            return Handle::new(label_hash); +        } + +        let Some(lookup_entry) = asset_lookup.get(&label_hash) else { +            self.add_import_work::<AssetSettings>( +                &label, +                label_hash, +                Some(asset_settings), +                &mut asset_lookup, +            ); + +            return Handle::new(label_hash); +        }; + +        match *lookup_entry { +            LookupEntry::Occupied(asset_index) => { +                let stored_asset = self.assets.get(asset_index).expect("Not possible"); + +                if stored_asset.strong.downcast_ref::<Asset>().is_none() { +                    tracing::error!( +                        "Wrong asset type {} for asset", +                        type_name::<Asset>() +                    ); +                } +            } +            LookupEntry::Pending => {} +        } + +        Handle::new(label_hash) +    } + +    pub fn store_with_name<'name, Asset: 'static + Send + Sync>( +        &mut self, +        name: impl Into<Cow<'name, str>>, +        asset: Asset, +    ) -> Handle<Asset> +    { +        self.store_with_label( +            Label { +                path: Path::new("").into(), +                name: Some(name.into()), +            }, +            asset, +        ) +    } + +    #[tracing::instrument(skip(self, asset), fields(asset_type=type_name::<Asset>()))] +    pub fn store_with_label<'i, Asset: 'static + Send + Sync>( +        &mut self, +        label: impl Into<Label<'i>> + Debug, +        asset: Asset, +    ) -> Handle<Asset> +    { +        let label = label.into(); + +        let label_hash = LabelHash::new(&label); + +        if matches!( +            self.asset_lookup.get_mut().get(&label_hash), +            Some(LookupEntry::Occupied(_)) +        ) { +            tracing::error!("Asset already exists"); + +            return Handle::new(label_hash); +        } + +        tracing::debug!("Storing asset"); + +        self.assets.push(StoredAsset::new(asset)); + +        let index = self.assets.len() - 1; + +        self.asset_lookup +            .get_mut() +            .insert(label_hash, LookupEntry::Occupied(index)); + +        if label.name.is_some() { +            let parent_asset_label_hash = +                LabelHash::new(&Label { path: label.path, name: None }); + +            if matches!( +                self.asset_lookup.get_mut().get(&parent_asset_label_hash), +                Some(LookupEntry::Pending) +            ) { +                self.asset_lookup.get_mut().remove(&parent_asset_label_hash); +            } else if self +                .asset_lookup +                .get_mut() +                .get(&parent_asset_label_hash) +                .is_none() +            { +                self.assets +                    .push(StoredAsset::new::<Option<Infallible>>(None)); + +                self.asset_lookup.get_mut().insert( +                    parent_asset_label_hash, +                    LookupEntry::Occupied(self.assets.len() - 1), +                ); +            } +        } + +        Handle::new(label_hash) +    } + +    fn is_pending(asset_lookup: &HashMap<LabelHash, LookupEntry>, label: &Label) -> bool +    { +        if label.name.is_some() { +            if let Some(LookupEntry::Pending) = +                asset_lookup.get(&LabelHash::new(&Label { +                    path: label.path.as_ref().into(), +                    name: None, +                })) +            { +                return true; +            } +        } + +        if let Some(LookupEntry::Pending) = asset_lookup.get(&LabelHash::new(label)) { +            return true; +        }; + +        false +    } + +    fn add_import_work<AssetSettings>( +        &self, +        label: &Label<'_>, +        label_hash: LabelHash, +        asset_settings: Option<AssetSettings>, +        asset_lookup: &mut HashMap<LabelHash, LookupEntry>, +    ) where +        AssetSettings: Any + Send + Sync, +    { +        let Some(file_ext) = label.path.extension() else { +            tracing::error!("Asset file is missing a file extension"); +            return; +        }; + +        let Some(importer) = self.get_importer(file_ext) else { +            tracing::error!( +                "No importer exists for asset file extension {}", +                file_ext.to_string_lossy() +            ); +            return; +        }; + +        self.import_work_queue.add_work(Work { +            func: |ImportWorkUserData { +                       import_work_msg_sender, +                       asset_path, +                       asset_settings, +                       importer, +                   }| { +                if let Err(err) = importer.call( +                    import_work_msg_sender, +                    asset_path.as_path(), +                    asset_settings.as_deref(), +                ) { +                    tracing::error!( +                        "Failed to load asset {}: {err}", +                        asset_path.display() +                    ); +                } +            }, +            user_data: ImportWorkUserData { +                import_work_msg_sender: self.import_work_msg_sender.clone(), +                asset_path: label.path.to_path_buf(), +                asset_settings: asset_settings.map(|asset_settings| { +                    Box::new(asset_settings) as Box<dyn Any + Send + Sync> +                }), +                importer: importer.clone(), +            }, +        }); + +        asset_lookup.insert(label_hash, LookupEntry::Pending); + +        if label.name.is_some() { +            asset_lookup.insert( +                LabelHash::new(&Label { +                    path: label.path.as_ref().into(), +                    name: None, +                }), +                LookupEntry::Pending, +            ); +        } +    } + +    fn get_importer(&self, file_ext: &OsStr) -> Option<&WrappedImporterFn> +    { +        let index = *self.importer_lookup.get(file_ext)?; + +        Some(self.importers.get(index).expect("Not possible")) +    } +} + +impl Default for Assets +{ +    fn default() -> Self +    { +        Self::with_capacity(0) +    } +} + +pub struct Submitter<'path> +{ +    import_work_msg_sender: MpscSender<ImportWorkMessage>, +    asset_path: &'path Path, +} + +impl Submitter<'_> +{ +    pub fn submit_load_other<'label, Asset: Send + Sync + 'static>( +        &self, +        label: impl Into<Label<'label>>, +    ) -> Handle<Asset> +    { +        let label = label.into(); + +        let _ = self.import_work_msg_sender.send(ImportWorkMessage::Load { +            do_load: |assets, label, _asset_settings| { +                let _ = assets.load::<Asset>(label); +            }, +            label: label.to_owned(), +            asset_settings: None, +        }); + +        Handle::new(LabelHash::new(&label)) +    } + +    pub fn submit_load_other_with_settings<'label, Asset, AssetSettings>( +        &self, +        label: impl Into<Label<'label>>, +        asset_settings: AssetSettings, +    ) -> Handle<Asset> +    where +        Asset: Send + Sync + 'static, +        AssetSettings: Send + Sync + Debug + 'static, +    { +        let label = label.into(); + +        let _ = self.import_work_msg_sender.send(ImportWorkMessage::Load { +            do_load: |assets, label, asset_settings| { +                let asset_settings = *asset_settings +                    .expect("Not possible") +                    .downcast::<AssetSettings>() +                    .expect("Not possible"); + +                let _ = assets +                    .load_with_settings::<Asset, AssetSettings>(label, asset_settings); +            }, +            label: label.to_owned(), +            asset_settings: Some(Box::new(asset_settings)), +        }); + +        Handle::new(LabelHash::new(&label)) +    } + +    pub fn submit_store<Asset: Send + Sync + 'static>( +        &self, +        asset: Asset, +    ) -> Handle<Asset> +    { +        let label = LabelOwned { +            path: self.asset_path.into(), +            name: None, +        }; + +        let label_hash = LabelHash::new(&label.to_label()); + +        let _ = self.import_work_msg_sender.send(ImportWorkMessage::Store { +            do_store: |assets, label, boxed_asset| { +                let Ok(asset) = boxed_asset.downcast::<Asset>() else { +                    unreachable!(); +                }; + +                assets.store_with_label::<Asset>(&label, *asset); +            }, +            label, +            asset: Box::new(asset), +        }); + +        Handle::new(label_hash) +    } + +    pub fn submit_store_named<Asset: Send + Sync + 'static>( +        &self, +        name: impl AsRef<str>, +        asset: Asset, +    ) -> Handle<Asset> +    { +        let label = LabelOwned { +            path: self.asset_path.into(), +            name: Some(name.as_ref().into()), +        }; + +        let label_hash = LabelHash::new(&label.to_label()); + +        let _ = self.import_work_msg_sender.send(ImportWorkMessage::Store { +            do_store: |assets, label, boxed_asset| { +                let Ok(asset) = boxed_asset.downcast::<Asset>() else { +                    unreachable!(); +                }; + +                assets.store_with_label::<Asset>(&label, *asset); +            }, +            label, +            asset: Box::new(asset), +        }); + +        Handle::new(label_hash) +    } +} + +/// Asset handle. +#[derive(Debug)] +pub struct Handle<Asset: 'static> +{ +    id: Id, +    _pd: PhantomData<Asset>, +} + +impl<Asset: 'static> Handle<Asset> +{ +    pub fn id(&self) -> Id +    { +        self.id +    } + +    fn new(label_hash: LabelHash) -> Self +    { +        Self { +            id: Id { label_hash }, +            _pd: PhantomData, +        } +    } +} + +impl<Asset: 'static> Clone for Handle<Asset> +{ +    fn clone(&self) -> Self +    { +        Self { id: self.id, _pd: PhantomData } +    } +} + +/// Asset ID. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id +{ +    label_hash: LabelHash, +} + +#[derive(Debug, thiserror::Error)] +enum ImporterError +{ +    #[error("Settings has a incorrect type")] +    IncorrectAssetSettingsType(PathBuf), + +    #[error(transparent)] +    Other(Box<dyn std::error::Error>), +} + +#[derive(Debug, Clone)] +struct WrappedImporterFn +{ +    wrapper_func: fn( +        MpscSender<ImportWorkMessage>, +        &Path, +        Option<&(dyn Any + Send + Sync)>, +    ) -> Result<(), ImporterError>, +} + +impl WrappedImporterFn +{ +    fn new<InnerFunc, AssetSettings, Err>(inner_func_param: InnerFunc) -> Self +    where +        InnerFunc: +            Fn(&mut Submitter<'_>, &Path, Option<&AssetSettings>) -> Result<(), Err>, +        AssetSettings: 'static, +        Err: std::error::Error + 'static, +    { +        assert_eq!(size_of::<InnerFunc>(), 0); + +        let wrapper_func = +            |import_work_msg_sender: MpscSender<ImportWorkMessage>, +             asset_path: &Path, +             asset_settings: Option<&(dyn Any + Send + Sync)>| { +                let inner_func = unsafe { std::mem::zeroed::<InnerFunc>() }; + +                let asset_settings = asset_settings +                    .map(|asset_settings| { +                        asset_settings +                            .downcast_ref::<AssetSettings>() +                            .ok_or_else(|| { +                                ImporterError::IncorrectAssetSettingsType( +                                    asset_path.to_path_buf(), +                                ) +                            }) +                    }) +                    .transpose()?; + +                inner_func( +                    &mut Submitter { import_work_msg_sender, asset_path }, +                    asset_path, +                    asset_settings, +                ) +                .map_err(|err| ImporterError::Other(Box::new(err)))?; + +                Ok(()) +            }; + +        std::mem::forget(inner_func_param); + +        Self { wrapper_func } +    } + +    fn call( +        &self, +        import_work_msg_sender: MpscSender<ImportWorkMessage>, +        asset_path: &Path, +        asset_settings: Option<&(dyn Any + Send + Sync)>, +    ) -> Result<(), ImporterError> +    { +        (self.wrapper_func)(import_work_msg_sender, asset_path, asset_settings) +    } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct LabelHash(u64); + +impl LabelHash +{ +    fn new(label: &Label<'_>) -> Self +    { +        let mut hasher = DefaultHasher::new(); + +        label.hash(&mut hasher); + +        Self(hasher.finish()) +    } +} + +#[derive(Debug, Default)] +pub(crate) struct Extension +{ +    pub assets: Assets, +} + +impl ecs::extension::Extension for Extension +{ +    fn collect(self, mut collector: ecs::extension::Collector<'_>) +    { +        let _ = collector.add_sole(self.assets); + +        collector.add_system(*PRE_UPDATE_PHASE, add_received_assets); +    } +} + +fn add_received_assets(mut assets: Single<Assets>) +{ +    while let Some(import_work_msg) = assets.import_work_msg_receiver.try_recv().ok() { +        match import_work_msg { +            ImportWorkMessage::Store { do_store, label, asset } => { +                do_store(&mut assets, label, asset); +            } +            ImportWorkMessage::Load { do_load, label, asset_settings } => { +                do_load( +                    &assets, +                    Label { +                        path: label.path.as_path().into(), +                        name: label.name.as_deref().map(|name| name.into()), +                    }, +                    asset_settings, +                ); +            } +        } +    } +} + +#[derive(Debug)] +struct ImportWorkUserData +{ +    import_work_msg_sender: MpscSender<ImportWorkMessage>, +    asset_path: PathBuf, +    asset_settings: Option<Box<dyn Any + Send + Sync>>, +    importer: WrappedImporterFn, +} + +#[derive(Debug)] +enum ImportWorkMessage +{ +    Store +    { +        do_store: fn(&mut Assets, LabelOwned, Box<dyn Any + Send + Sync>), +        label: LabelOwned, +        asset: Box<dyn Any + Send + Sync>, +    }, + +    Load +    { +        do_load: fn(&Assets, Label<'_>, Option<Box<dyn Any + Send + Sync>>), +        label: LabelOwned, +        asset_settings: Option<Box<dyn Any + Send + Sync>>, +    }, +} + +#[derive(Debug, Clone, Copy)] +enum LookupEntry +{ +    Occupied(usize), +    Pending, +} + +#[derive(Debug)] +struct StoredAsset +{ +    strong: Arc<dyn Any + Send + Sync>, +} + +impl StoredAsset +{ +    fn new<Asset: Any + Send + Sync>(asset: Asset) -> Self +    { +        let strong = Arc::new(asset); + +        Self { strong } +    } +} diff --git a/engine/src/camera/fly.rs b/engine/src/camera/fly.rs index b6ba7aa..7996b4d 100644 --- a/engine/src/camera/fly.rs +++ b/engine/src/camera/fly.rs @@ -1,15 +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::initializable::Initializable; +use ecs::system::Into;  use ecs::{Component, Query}; -use glfw::window::{Key, KeyState}; +use crate::builder;  use crate::camera::{Active as ActiveCamera, Camera};  use crate::delta_time::DeltaTime; -use crate::event::Update as UpdateEvent; -use crate::input::{Cursor, CursorFlags, Keys}; -use crate::transform::Position; -use crate::util::builder; +use crate::input::keyboard::{Key, Keyboard}; +use crate::input::mouse::Motion as MouseMotion; +use crate::transform::WorldPosition;  use crate::vector::{Vec2, Vec3};  builder! { @@ -60,12 +61,7 @@ impl ecs::extension::Extension for Extension  {      fn collect(self, mut collector: ecs::extension::Collector<'_>)      { -        collector.add_system( -            UpdateEvent, -            update -                .into_system() -                .initialize((CursorState::default(), self.0)), -        ); +        collector.add_system(*UPDATE_PHASE, update.into_system().initialize((self.0,)));      }  } @@ -76,37 +72,30 @@ pub struct Options  }  fn update( -    camera_query: Query<(Camera, Position, 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_motion: Single<MouseMotion>,      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 { -            #[cfg(feature = "debug")] -            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; +        // tracing::info!("Mouse motion: {:?}", mouse_motion.position_delta); -        cursor_state.last_pos = cursor.position; +        if mouse_motion.position_delta != (Vec2 { x: 0.0, y: 0.0 }) { +            let x_offset = +                mouse_motion.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_motion.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)] @@ -123,35 +112,30 @@ fn update(          camera.global_up = cam_right.cross(&direction).normalize(); -        if matches!(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 matches!(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 matches!(keys.get_key_state(Key::A), KeyState::Pressed) { +        if keyboard.pressed(Key::A) {              let cam_left = -direction.cross(&Vec3::UP).normalize(); -            camera_pos.position += cam_left * fly_camera.speed * delta_time.as_secs_f32(); +            camera_world_pos.position += +                cam_left * fly_camera.speed * delta_time.as_secs_f32();          } -        if matches!(keys.get_key_state(Key::D), KeyState::Pressed) { +        if keyboard.pressed(Key::D) {              let cam_right = direction.cross(&Vec3::UP).normalize(); -            camera_pos.position += +            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/collision.rs b/engine/src/collision.rs new file mode 100644 index 0000000..aefd9b6 --- /dev/null +++ b/engine/src/collision.rs @@ -0,0 +1,142 @@ +use ecs::Component; + +use crate::mesh::Mesh; +use crate::vector::Vec3; + +pub trait Collider<Other> +{ +    fn intersects(&self, other: &Other) -> bool; +} + +#[derive(Debug, Default, Clone, Component)] +#[non_exhaustive] +pub struct BoxCollider +{ +    pub min: Vec3<f32>, +    pub max: Vec3<f32>, +} + +impl BoxCollider +{ +    pub fn for_mesh(mesh: &Mesh) -> Self +    { +        let furthest_dir_points = mesh.find_furthest_vertex_positions(); + +        Self { +            min: Vec3 { +                x: furthest_dir_points.left.x, +                y: furthest_dir_points.down.y, +                z: furthest_dir_points.back.z, +            }, +            max: Vec3 { +                x: furthest_dir_points.right.x, +                y: furthest_dir_points.up.y, +                z: furthest_dir_points.front.z, +            }, +        } +    } + +    pub fn offset(self, offset: Vec3<f32>) -> Self +    { +        Self { +            min: self.min + offset, +            max: self.max + offset, +        } +    } +} + +impl Collider<BoxCollider> for BoxCollider +{ +    fn intersects(&self, other: &BoxCollider) -> bool +    { +        self.min.x <= other.max.x +            && self.max.x >= other.min.x +            && self.min.y <= other.max.y +            && self.max.y >= other.min.y +            && self.min.z <= other.max.z +            && self.max.z >= other.min.z +    } +} + +impl Collider<SphereCollider> for BoxCollider +{ +    fn intersects(&self, other: &SphereCollider) -> bool +    { +        other.intersects(self) +    } +} + +impl Collider<Vec3<f32>> for BoxCollider +{ +    fn intersects(&self, other: &Vec3<f32>) -> bool +    { +        other.x >= self.min.x +            && other.y >= self.min.y +            && other.z >= self.min.z +            && other.x <= self.max.x +            && other.y <= self.max.y +            && other.z <= self.max.z +    } +} + +#[derive(Debug, Default, Clone, Component)] +pub struct SphereCollider +{ +    pub center: Vec3<f32>, +    pub radius: f32, +} + +impl SphereCollider +{ +    pub fn offset(self, offset: Vec3<f32>) -> Self +    { +        Self { +            center: self.center + offset, +            radius: self.radius, +        } +    } +} + +impl Collider<SphereCollider> for SphereCollider +{ +    fn intersects(&self, other: &SphereCollider) -> bool +    { +        (&self.center - &other.center).length() <= self.radius + other.radius +    } +} + +impl Collider<BoxCollider> for SphereCollider +{ +    fn intersects(&self, other: &BoxCollider) -> bool +    { +        let mut min_distance = 0.0; + +        if self.center.x < other.min.x { +            min_distance += (self.center.x - other.min.x).powf(2.0); +        } else if self.center.x > other.max.x { +            min_distance += (self.center.x - other.max.x).powf(2.0); +        } + +        if self.center.y < other.min.y { +            min_distance += (self.center.y - other.min.y).powf(2.0); +        } else if self.center.y > other.max.y { +            min_distance += (self.center.y - other.max.y).powf(2.0); +        } + +        if self.center.z < other.min.z { +            min_distance += (self.center.z - other.min.z).powf(2.0); +        } else if self.center.z > other.max.z { +            min_distance += (self.center.z - other.max.z).powf(2.0); +        } + +        min_distance <= self.radius.powf(2.0) +    } +} + +impl Collider<Vec3<f32>> for SphereCollider +{ +    fn intersects(&self, other: &Vec3<f32>) -> bool +    { +        (&self.center - other).length() <= self.radius +    } +} diff --git a/engine/src/data_types/color.rs b/engine/src/data_types/color.rs index cef3b92..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 b395627..8bf239f 100644 --- a/engine/src/data_types/dimens.rs +++ b/engine/src/data_types/dimens.rs @@ -1,7 +1,70 @@ -/// Dimensions. +use std::num::NonZeroU32; + +/// 2D dimensions.  #[derive(Debug, Clone, Copy)]  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 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> +{ +    pub width: 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..b754b62 100644 --- a/engine/src/data_types/matrix.rs +++ b/engine/src/data_types/matrix.rs @@ -4,7 +4,7 @@ use crate::vector::Vec3;  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> diff --git a/engine/src/data_types/vector.rs b/engine/src/data_types/vector.rs index 17953f4..dc6df30 100644 --- a/engine/src/data_types/vector.rs +++ b/engine/src/data_types/vector.rs @@ -2,7 +2,7 @@ use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};  use crate::color::Color; -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy, PartialEq)]  pub struct Vec2<Value>  {      pub x: Value, @@ -14,6 +14,29 @@ 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 +97,7 @@ where      }  } -#[derive(Debug, Default, Clone, Copy)] -#[repr(C)] +#[derive(Debug, Default, Clone, Copy, PartialEq)]  pub struct Vec3<Value>  {      pub x: Value, @@ -85,6 +107,11 @@ pub struct Vec3<Value>  impl Vec3<f32>  { +    pub const BACK: Self = Self { x: 0.0, y: 0.0, z: -1.0 }; +    pub const DOWN: Self = Self { x: 0.0, y: -1.0, z: 0.0 }; +    pub const FRONT: Self = Self { x: 0.0, y: 0.0, z: 1.0 }; +    pub const LEFT: Self = Self { x: -1.0, y: 0.0, z: 0.0 }; +    pub const RIGHT: Self = Self { x: 1.0, y: 0.0, z: 0.0 };      pub const UP: Self = Self { x: 0.0, y: 1.0, z: 0.0 };      /// Returns the length of the vector. @@ -209,6 +236,22 @@ where      }  } +impl<Value> Mul for Vec3<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, +        } +    } +} +  impl<Value> Neg for Vec3<Value>  where      Value: Neg<Output = Value>, diff --git a/engine/src/draw_flags.rs b/engine/src/draw_flags.rs index df5eed1..426f865 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. diff --git a/engine/src/event.rs b/engine/src/event.rs deleted file mode 100644 index e5ae486..0000000 --- a/engine/src/event.rs +++ /dev/null @@ -1,27 +0,0 @@ -pub use ecs::event::start::Start; -use ecs::event::Event; - -#[derive(Debug)] -pub struct Update; - -impl Event for Update {} - -#[derive(Debug)] -pub struct PreUpdate; - -impl Event for PreUpdate {} - -#[derive(Debug)] -pub struct Present; - -impl Event for Present {} - -#[derive(Debug)] -pub struct PostPresent; - -impl Event for PostPresent {} - -#[derive(Debug)] -pub struct Conclude; - -impl Event for Conclude {} diff --git a/engine/src/file_format/wavefront/mtl.rs b/engine/src/file_format/wavefront/mtl.rs index ef6e894..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`.  /// @@ -44,25 +42,47 @@ pub fn parse(obj_content: &str) -> Result<Vec<NamedMaterial>, Error>          .filter(|(_, statement)| matches!(statement.keyword, Keyword::Newmtl))          .count(); -    #[cfg(feature = "debug")]      tracing::debug!("Material count: {material_cnt}");      statements_to_materials(statements, material_cnt)  }  #[derive(Debug, Clone)] +#[non_exhaustive]  pub struct NamedMaterial  {      pub name: String, -    pub material: Material, +    pub ambient: Color<f32>, +    pub diffuse: Color<f32>, +    pub specular: Color<f32>, +    pub ambient_map: Option<TextureMap>, +    pub diffuse_map: Option<TextureMap>, +    pub specular_map: Option<TextureMap>, +    pub shininess: f32, +} + +impl Default for NamedMaterial +{ +    fn default() -> Self +    { +        Self { +            name: String::new(), +            ambient: Color::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)] @@ -71,8 +91,14 @@ pub enum Error      #[error(transparent)]      ParsingError(#[from] ParsingError), -    #[error("Failed to open texture")] -    TextureError(#[from] TextureError), +    #[error( +        "A material start statement (newmtl) is expected before statement at line {}", +        line_no +    )] +    ExpectedMaterialStartStmtBeforeStmt +    { +        line_no: usize +    },      #[error(          "Unsupported number of arguments ({arg_count}) to {keyword} at line {line_no}" @@ -93,7 +119,7 @@ pub enum Error      },  } -#[cfg_attr(feature = "debug", tracing::instrument(skip_all))] +#[tracing::instrument(skip_all)]  fn statements_to_materials(      statements: impl IntoIterator<Item = (usize, Statement<Keyword>)>,      material_cnt: usize, @@ -101,63 +127,52 @@ fn statements_to_materials(  {      let mut materials = Vec::<NamedMaterial>::with_capacity(material_cnt); -    let mut curr_material = UnfinishedNamedMaterial { -        name: String::new(), -        material_builder: MaterialBuilder::new(), -        ready: false, -    }; -      for (line_no, statement) in statements {          if statement.keyword == Keyword::Newmtl { -            if curr_material.ready { -                #[cfg(feature = "debug")] -                tracing::debug!("Building material"); - -                let material = curr_material.material_builder.clone().build(); - -                materials.push(NamedMaterial { name: curr_material.name, material }); -            } -              let name = statement.get_text_arg(0, line_no)?; -            curr_material.name = name.to_string(); -            curr_material.ready = true; +            materials.push(NamedMaterial { +                name: name.to_string(), +                ..Default::default() +            });              continue;          } -        if !curr_material.ready { -            // Discard statements not belonging to a material -            continue; +        let Some(curr_material) = materials.last_mut() else { +            return Err(Error::ExpectedMaterialStartStmtBeforeStmt { line_no });          };          match statement.keyword {              Keyword::Ka => {                  let color = get_color_from_statement(&statement, line_no)?; -                #[cfg(feature = "debug")] -                tracing::debug!("Adding ambient color"); +                tracing::debug!( +                    "Adding ambient color {color:?} to material {}", +                    curr_material.name +                ); -                curr_material.material_builder = -                    curr_material.material_builder.ambient(color); +                curr_material.ambient = color;              }              Keyword::Kd => {                  let color = get_color_from_statement(&statement, line_no)?; -                #[cfg(feature = "debug")] -                tracing::debug!("Adding diffuse color"); +                tracing::debug!( +                    "Adding diffuse color {color:?} to material {}", +                    curr_material.name +                ); -                curr_material.material_builder = -                    curr_material.material_builder.diffuse(color); +                curr_material.diffuse = color;              }              Keyword::Ks => {                  let color = get_color_from_statement(&statement, line_no)?; -                #[cfg(feature = "debug")] -                tracing::debug!("Adding specular color"); +                tracing::debug!( +                    "Adding specular color {color:?} to material {}", +                    curr_material.name +                ); -                curr_material.material_builder = -                    curr_material.material_builder.specular(color); +                curr_material.specular = color;              }              Keyword::MapKa => {                  if statement.arguments.len() > 1 { @@ -170,55 +185,75 @@ fn statements_to_materials(                  let texture_file_path = statement.get_text_arg(0, line_no)?; -                let texture = Texture::open(Path::new(texture_file_path))?; - -                #[cfg(feature = "debug")] -                tracing::debug!("Adding ambient map"); +                tracing::debug!( +                    "Adding ambient map {texture_file_path} to material {}", +                    curr_material.name +                ); -                let texture_id = texture.id(); - -                curr_material.material_builder = curr_material -                    .material_builder -                    .texture(texture) -                    .ambient_map(texture_id); +                curr_material.ambient_map = Some(TextureMap { +                    path: Path::new(texture_file_path).to_path_buf(), +                });              }              Keyword::MapKd => { -                let texture = get_map_from_texture(&statement, line_no)?; +                if statement.arguments.len() > 1 { +                    return Err(Error::UnsupportedArgumentCount { +                        keyword: statement.keyword.to_string(), +                        arg_count: statement.arguments.len(), +                        line_no, +                    }); +                } -                #[cfg(feature = "debug")] -                tracing::debug!("Adding diffuse map"); +                let texture_file_path = statement.get_text_arg(0, line_no)?; -                let texture_id = texture.id(); +                tracing::debug!( +                    "Adding diffuse map {texture_file_path} to material {}", +                    curr_material.name +                ); -                curr_material.material_builder = curr_material -                    .material_builder -                    .texture(texture) -                    .diffuse_map(texture_id); +                curr_material.diffuse_map = Some(TextureMap { +                    path: Path::new(texture_file_path).to_path_buf(), +                });              }              Keyword::MapKs => { -                let texture = get_map_from_texture(&statement, line_no)?; +                if statement.arguments.len() > 1 { +                    return Err(Error::UnsupportedArgumentCount { +                        keyword: statement.keyword.to_string(), +                        arg_count: statement.arguments.len(), +                        line_no, +                    }); +                } -                #[cfg(feature = "debug")] -                tracing::debug!("Adding specular map"); +                let texture_file_path = statement.get_text_arg(0, line_no)?; -                let texture_id = texture.id(); +                tracing::debug!( +                    "Adding specular map {texture_file_path} to material {}", +                    curr_material.name +                ); -                curr_material.material_builder = curr_material -                    .material_builder -                    .texture(texture) -                    .specular_map(texture_id); +                curr_material.specular_map = Some(TextureMap { +                    path: Path::new(texture_file_path).to_path_buf(), +                });              } -            Keyword::Newmtl => {} -        } -    } +            Keyword::Ns => { +                if statement.arguments.len() != 1 { +                    return Err(Error::UnsupportedArgumentCount { +                        keyword: statement.keyword.to_string(), +                        arg_count: statement.arguments.len(), +                        line_no, +                    }); +                } -    if curr_material.ready { -        #[cfg(feature = "debug")] -        tracing::debug!("Building last material"); +                let shininess = statement.get_float_arg(0, line_no)?; -        let material = curr_material.material_builder.build(); +                tracing::debug!( +                    "Adding shininess {shininess} to material {}", +                    curr_material.name +                ); -        materials.push(NamedMaterial { name: curr_material.name, material }); +                curr_material.shininess = shininess; +            } +            Keyword::Newmtl => {} +        }      }      Ok(materials) @@ -244,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 { @@ -280,5 +297,7 @@ keyword! {          #[keyword(rename = "map_Ks")]          MapKs, + +        Ns,      }  } diff --git a/engine/src/file_format/wavefront/obj.rs b/engine/src/file_format/wavefront/obj.rs index 88e6580..88d566c 100644 --- a/engine/src/file_format/wavefront/obj.rs +++ b/engine/src/file_format/wavefront/obj.rs @@ -2,6 +2,7 @@  //!  //! File format documentation: <https://paulbourke.net/dataformats/obj> +use std::collections::HashMap;  use std::fs::read_to_string;  use std::path::PathBuf; @@ -12,10 +13,9 @@ use crate::file_format::wavefront::common::{      Statement,      Triplet,  }; -use crate::mesh::Mesh; +use crate::mesh::{Mesh, Vertex};  use crate::util::try_option;  use crate::vector::{Vec2, Vec3}; -use crate::vertex::{Builder as VertexBuilder, Vertex};  /// Parses the content of a Wavefront `.obj`.  /// @@ -82,26 +82,32 @@ impl Obj      /// - A face index does not fit in a [`u32`]      pub fn to_mesh(&self) -> Result<Mesh, Error>      { -        let vertices = self -            .faces -            .iter() -            .flat_map(|face| face.vertices.clone()) -            .map(|face_vertex| face_vertex.to_vertex(self)) -            .collect::<Result<Vec<_>, Error>>()?; - -        Ok(Mesh::new( -            vertices, -            Some( -                self.faces -                    .iter() -                    .flat_map(|face| face.vertices.clone()) -                    .enumerate() -                    .map(|(index, _)| { -                        u32::try_from(index).map_err(|_| Error::FaceIndexTooBig(index)) -                    }) -                    .collect::<Result<Vec<_>, _>>()?, -            ), -        )) +        let mut vertices = Vec::<Vertex>::with_capacity(self.faces.len() * 3); +        let mut indices = Vec::<u32>::with_capacity(self.faces.len() * 3); + +        let mut added_face_vertices = +            HashMap::<FaceVertex, u32>::with_capacity(self.faces.len() * 3); + +        for face in &self.faces { +            for face_vertex in &face.vertices { +                if let Some(index) = added_face_vertices.get(&face_vertex) { +                    indices.push(*index); + +                    continue; +                } + +                vertices.push(face_vertex.to_vertex(self)?); + +                let vertex_index = u32::try_from(vertices.len() - 1) +                    .map_err(|_| Error::FaceIndexTooBig(vertices.len() - 1))?; + +                indices.push(vertex_index); + +                added_face_vertices.insert(face_vertex.clone(), vertex_index); +            } +        } + +        Ok(Mesh::new(vertices, Some(indices)))      }      /// Reads and parses the material libraries of this `Obj`. @@ -142,7 +148,7 @@ pub struct Face      pub material_name: Option<String>,  } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)]  pub struct FaceVertex  {      pub position: u32, @@ -161,7 +167,7 @@ impl FaceVertex      /// - 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 mut vertex_builder = Vertex::builder();          let vertex_pos = *obj.vertex_positions.get(self.position as usize - 1).ok_or(              Error::FaceVertexPositionNotFound { vertex_pos_index: self.position }, diff --git a/engine/src/image.rs b/engine/src/image.rs new file mode 100644 index 0000000..0e04412 --- /dev/null +++ b/engine/src/image.rs @@ -0,0 +1,184 @@ +use std::fs::File; +use std::io::BufReader; +use std::path::Path; + +use image_rs::GenericImageView as _; + +use crate::asset::{Assets, Submitter as AssetSubmitter}; +use crate::color::Color; +use crate::data_types::dimens::Dimens; +use crate::builder; + +#[derive(Debug)] +pub struct Image +{ +    inner: image_rs::DynamicImage, +} + +impl Image +{ +    pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> +    { +        let buffered_reader = +            BufReader::new(File::open(&path).map_err(Error::ReadFailed)?); + +        let image_reader = image_rs::io::Reader::with_format( +            buffered_reader, +            image_rs::ImageFormat::from_path(path) +                .map_err(|_| Error::UnsupportedFormat)?, +        ); + +        Ok(Self { +            inner: image_reader +                .decode() +                .map_err(|err| Error::DecodeFailed(DecodeError(err)))?, +        }) +    } + +    pub fn from_color(dimens: impl Into<Dimens<u32>>, color: impl Into<Color<u8>>) +        -> Self +    { +        let dimens: Dimens<u32> = dimens.into(); + +        let color: Color<u8> = color.into(); + +        Self { +            inner: image_rs::RgbImage::from_pixel( +                dimens.width, +                dimens.height, +                image_rs::Rgb([color.red, color.green, color.blue]), +            ) +            .into(), +        } +    } + +    pub fn dimensions(&self) -> Dimens<u32> +    { +        self.inner.dimensions().into() +    } + +    pub fn color_type(&self) -> ColorType +    { +        self.inner.color().into() +    } + +    pub fn as_bytes(&self) -> &[u8] +    { +        self.inner.as_bytes() +    } +} + +builder! { +#[builder(name = SettingsBuilder, derives=(Debug, Clone))] +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct Settings { +} +} + +impl Settings +{ +    pub fn builder() -> SettingsBuilder +    { +        SettingsBuilder::default() +    } +} + +impl Default for SettingsBuilder +{ +    fn default() -> Self +    { +        Settings::default().into() +    } +} + +/// An enumeration over supported color types and bit depths +#[derive(Copy, PartialEq, Eq, Debug, Clone, Hash)] +#[non_exhaustive] +pub enum ColorType +{ +    /// Pixel is 8-bit luminance +    L8, + +    /// Pixel is 8-bit luminance with an alpha channel +    La8, + +    /// Pixel contains 8-bit R, G and B channels +    Rgb8, + +    /// Pixel is 8-bit RGB with an alpha channel +    Rgba8, + +    /// Pixel is 16-bit luminance +    L16, + +    /// Pixel is 16-bit luminance with an alpha channel +    La16, + +    /// Pixel is 16-bit RGB +    Rgb16, + +    /// Pixel is 16-bit RGBA +    Rgba16, + +    /// Pixel is 32-bit float RGB +    Rgb32F, + +    /// Pixel is 32-bit float RGBA +    Rgba32F, +} + +impl From<image_rs::ColorType> for ColorType +{ +    fn from(color_type: image_rs::ColorType) -> Self +    { +        match color_type { +            image_rs::ColorType::L8 => Self::L8, +            image_rs::ColorType::La8 => Self::La8, +            image_rs::ColorType::Rgb8 => Self::Rgb8, +            image_rs::ColorType::Rgba8 => Self::Rgba8, +            image_rs::ColorType::L16 => Self::L16, +            image_rs::ColorType::La16 => Self::La16, +            image_rs::ColorType::Rgb16 => Self::Rgb16, +            image_rs::ColorType::Rgba16 => Self::Rgba16, +            image_rs::ColorType::Rgb32F => Self::Rgb32F, +            image_rs::ColorType::Rgba32F => Self::Rgba32F, +            _ => { +                panic!("Unrecognized image_rs::ColorType variant"); +            } +        } +    } +} + +pub fn set_asset_importers(assets: &mut Assets) +{ +    assets.set_importer::<_, _>(["png", "jpg"], import_asset); +} + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ +    #[error("Failed to read image file")] +    ReadFailed(#[source] std::io::Error), + +    #[error("Failed to decode image")] +    DecodeFailed(DecodeError), + +    #[error("Unsupported image format")] +    UnsupportedFormat, +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct DecodeError(image_rs::ImageError); + +fn import_asset( +    asset_submitter: &mut AssetSubmitter<'_>, +    path: &Path, +    _settings: Option<&'_ Settings>, +) -> Result<(), Error> +{ +    asset_submitter.submit_store::<Image>(Image::open(path)?); + +    Ok(()) +} diff --git a/engine/src/input.rs b/engine/src/input.rs index f4166f6..f8c9dfd 100644 --- a/engine/src/input.rs +++ b/engine/src/input.rs @@ -1,145 +1,23 @@ -use std::collections::HashMap; - +use ecs::declare_entity;  use ecs::extension::Collector as ExtensionCollector; -use ecs::sole::Single; -use ecs::Sole; - -use crate::event::{ -    PostPresent as PostPresentEvent, -    PreUpdate as PreUpdateEvent, -    Start as StartEvent, -}; -use crate::vector::Vec2; -use crate::window::Window; - -mod reexports -{ -    pub use crate::window::{Key, KeyState}; -} - -pub use reexports::*; - -#[derive(Debug, Sole)] -pub struct Keys -{ -    map: HashMap<Key, KeyData>, -} - -impl Keys -{ -    #[must_use] -    pub fn new() -> Self -    { -        Self { -            map: Key::KEYS -                .iter() -                .map(|key| { -                    ( -                        *key, -                        KeyData { -                            state: KeyState::Released, -                            prev_tick_state: KeyState::Released, -                        }, -                    ) -                }) -                .collect(), -        } -    } - -    #[must_use] -    pub fn get_key_state(&self, key: Key) -> KeyState -    { -        let Some(key_data) = self.map.get(&key) else { -            unreachable!(); -        }; - -        key_data.state -    } - -    #[must_use] -    pub fn get_prev_key_state(&self, key: Key) -> KeyState -    { -        let Some(key_data) = self.map.get(&key) else { -            unreachable!(); -        }; - -        key_data.prev_tick_state -    } +use ecs::pair::{DependsOn, Pair}; +use ecs::phase::Phase; -    pub fn set_key_state(&mut self, key: Key, new_key_state: KeyState) -    { -        if matches!(new_key_state, KeyState::Repeat) { -            return; -        } - -        let Some(key_data) = self.map.get_mut(&key) else { -            unreachable!(); -        }; +use crate::windowing::PHASE as WINDOWING_PHASE; -        key_data.state = new_key_state; -    } +pub mod keyboard; +pub mod mouse; -    #[must_use] -    pub fn is_anything_pressed(&self) -> bool -    { -        self.map -            .values() -            .any(|key_data| matches!(key_data.state, KeyState::Pressed)) -    } -} - -impl Default for Keys -{ -    fn default() -> Self -    { -        Self::new() -    } -} - -#[derive(Debug, Default, Clone, Sole)] -pub struct Cursor -{ -    pub position: Vec2<f64>, -    pub has_moved: bool, -} - -#[derive(Debug, Clone, Sole)] -pub struct CursorFlags -{ -    /// This flag is set in two situations: -    /// A: The window has just started -    /// B: The window has gained focus again after losing focus. -    /// -    /// This flag only lasts a single tick then it is cleared (at the beginning of the -    /// next tick). -    pub is_first_move: CursorFlag, -} - -impl Default for CursorFlags -{ -    fn default() -> Self -    { -        Self { -            is_first_move: CursorFlag { flag: true, ..Default::default() }, -        } -    } -} - -#[derive(Debug, Default, Clone)] -pub struct CursorFlag -{ -    pub flag: bool, -    pub pending_clear: bool, -} - -impl CursorFlag -{ -    pub fn clear(&mut self) -    { -        self.flag = false; -        self.pending_clear = false; -    } -} +declare_entity!( +    pub PHASE, +    ( +        Phase, +        Pair::builder() +            .relation::<DependsOn>() +            .target_id(*WINDOWING_PHASE) +            .build() +    ) +);  /// Input extension.  #[derive(Debug, Default)] @@ -147,96 +25,8 @@ pub struct Extension {}  impl ecs::extension::Extension for Extension  { -    fn collect(self, mut collector: ExtensionCollector<'_>) +    fn collect(self, _collector: ExtensionCollector<'_>)      { -        collector.add_system(StartEvent, initialize); -        collector.add_system(PreUpdateEvent, maybe_clear_cursor_is_first_move); -        collector.add_system(PostPresentEvent, set_keys_prev_tick_state); - -        collector.add_sole(Keys::default()).ok(); -        collector.add_sole(Cursor::default()).ok(); -        collector.add_sole(CursorFlags::default()).ok(); +        // TODO: Add input mapping      }  } - -fn initialize( -    keys: Single<Keys>, -    cursor: Single<Cursor>, -    cursor_flags: Single<CursorFlags>, -    window: Single<Window>, -) -{ -    let keys_weak_ref = keys.to_weak_ref(); - -    window.set_key_callback(move |key, _scancode, key_state, _modifiers| { -        let keys_ref = keys_weak_ref.access().expect("No world"); - -        let mut keys = keys_ref.to_single(); - -        keys.set_key_state(key, key_state); -    }); - -    let cursor_weak_ref = cursor.to_weak_ref(); - -    window.set_cursor_pos_callback(move |cursor_position| { -        let cursor_ref = cursor_weak_ref.access().expect("No world"); - -        let mut cursor = cursor_ref.to_single(); - -        cursor.position = Vec2 { -            x: cursor_position.x, -            y: cursor_position.y, -        }; - -        cursor.has_moved = true; -    }); - -    let cursor_flags_weak_ref = cursor_flags.to_weak_ref(); - -    window.set_focus_callback(move |is_focused| { -        #[cfg(feature = "debug")] -        tracing::trace!("Window is focused: {is_focused}"); - -        let cursor_flags_ref = cursor_flags_weak_ref.access().expect("No world"); - -        cursor_flags_ref.to_single().is_first_move.flag = is_focused; -    }); -} - -fn maybe_clear_cursor_is_first_move( -    cursor: Single<Cursor>, -    mut cursor_flags: Single<CursorFlags>, -) -{ -    if cursor_flags.is_first_move.pending_clear { -        #[cfg(feature = "debug")] -        tracing::trace!("Clearing is_first_move"); - -        // This flag was set for the whole previous tick so it can be cleared now -        cursor_flags.is_first_move.clear(); - -        return; -    } - -    if cursor.has_moved && cursor_flags.is_first_move.flag { -        #[cfg(feature = "debug")] -        tracing::trace!("Setting flag to clear is_first_move next tick"); - -        // Make this system clear is_first_move the next time it runs -        cursor_flags.is_first_move.pending_clear = true; -    } -} - -fn set_keys_prev_tick_state(mut keys: Single<Keys>) -{ -    for key_data in keys.map.values_mut() { -        key_data.prev_tick_state = key_data.state; -    } -} - -#[derive(Debug)] -struct KeyData -{ -    state: KeyState, -    prev_tick_state: KeyState, -} diff --git a/engine/src/input/keyboard.rs b/engine/src/input/keyboard.rs new file mode 100644 index 0000000..d226df0 --- /dev/null +++ b/engine/src/input/keyboard.rs @@ -0,0 +1,6 @@ +mod reexports +{ +    pub use crate::windowing::keyboard::{Key, KeyState, Keyboard}; +} + +pub use reexports::*; diff --git a/engine/src/input/mouse.rs b/engine/src/input/mouse.rs new file mode 100644 index 0000000..90091f3 --- /dev/null +++ b/engine/src/input/mouse.rs @@ -0,0 +1,6 @@ +mod reexports +{ +    pub use crate::windowing::mouse::{Button, ButtonState, Buttons, Motion}; +} + +pub use reexports::*; diff --git a/engine/src/lib.rs b/engine/src/lib.rs index abf26f5..d5531c1 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -2,60 +2,48 @@  #![allow(clippy::needless_pass_by_value)]  use ecs::component::Sequence as ComponentSequence; -use ecs::event::component::TypeTransformComponentsToAddedEvents; -use ecs::event::{Event, Sequence as EventSequence};  use ecs::extension::Extension; +use ecs::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::tuple::Reduce as TupleReduce;  use ecs::uid::Uid;  use ecs::{SoleAlreadyExistsError, World}; +use crate::asset::{Assets, Extension as AssetExtension};  use crate::delta_time::{update as update_delta_time, DeltaTime, LastUpdate}; -use crate::event::{ -    Conclude as ConcludeEvent, -    PostPresent as PostPresentEvent, -    PreUpdate as PreUpdateEvent, -    Present as PresentEvent, -    Update as UpdateEvent, -};  mod opengl; -mod shader_preprocessor;  mod util; +mod work_queue; +pub mod asset;  pub mod camera; +pub mod collision;  pub mod data_types;  pub mod delta_time;  pub mod draw_flags; -pub mod event;  pub mod file_format; +pub mod image;  pub mod input;  pub mod lighting;  pub mod material;  pub mod math;  pub mod mesh; -pub mod performance; +pub mod model;  pub mod projection;  pub mod renderer; -pub mod shader;  pub mod 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}; -type EventOrder = ( -    PreUpdateEvent, -    UpdateEvent, -    PresentEvent, -    PostPresentEvent, -    ConcludeEvent, -); +const INITIAL_ASSET_CAPACITY: usize = 128;  #[derive(Debug)]  pub struct Engine @@ -69,35 +57,52 @@ 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();          world.register_system( -            PreUpdateEvent, +            *PRE_UPDATE_PHASE,              update_delta_time                  .into_system()                  .initialize((LastUpdate::default(),)),          ); +        let mut assets = Assets::with_capacity(INITIAL_ASSET_CAPACITY); + +        crate::model::set_asset_importers(&mut assets); +        crate::image::set_asset_importers(&mut assets); + +        world.add_extension(AssetExtension { assets }); +          Self { world }      }      pub fn spawn<Comps>(&mut self, components: Comps) -> Uid      where -        Comps: ComponentSequence + TupleReduce<TypeTransformComponentsToAddedEvents>, -        Comps::Out: EventSequence, +        Comps: ComponentSequence,      {          self.world.create_entity(components)      }      pub fn register_system<'this, SystemImpl>(          &'this mut self, -        event: impl Event, +        phase_euid: Uid,          system: impl System<'this, SystemImpl>,      )      { -        self.world.register_system(event, system); +        self.world.register_system(phase_euid, system); +    } + +    pub fn register_observer<'this, SystemImpl>( +        &'this mut self, +        observer: impl Observer<'this, SystemImpl>, +    ) +    { +        self.world.register_observer(observer);      }      /// Adds a globally shared singleton value. @@ -115,9 +120,9 @@ impl Engine      }      /// Runs the event loop. -    pub fn start(&self) +    pub fn start(&mut self)      { -        self.world.event_loop::<EventOrder>(); +        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 aae6003..56ff15f 100644 --- a/engine/src/material.rs +++ b/engine/src/material.rs @@ -1,35 +1,48 @@  use ecs::Component;  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; +use crate::builder; -#[derive(Debug, Clone, Component)] +#[derive(Debug, Clone)]  #[non_exhaustive]  pub struct Material  {      pub ambient: Color<f32>,      pub diffuse: Color<f32>,      pub specular: Color<f32>, -    pub ambient_map: TextureId, -    pub diffuse_map: TextureId, -    pub specular_map: TextureId, -    pub textures: Vec<Texture>, +    pub ambient_map: Option<Texture>, +    pub diffuse_map: Option<Texture>, +    pub specular_map: Option<Texture>,      pub shininess: f32,  } +impl Material +{ +    pub fn builder() -> Builder +    { +        Builder::default() +    } +} + +impl Default for Material +{ +    fn default() -> Self +    { +        Self::builder().build() +    } +} +  /// [`Material`] builder.  #[derive(Debug, Clone)]  pub struct Builder  { -    ambient: Option<Color<f32>>, -    diffuse: Option<Color<f32>>, -    specular: Option<Color<f32>>, -    ambient_map: Option<TextureId>, -    diffuse_map: Option<TextureId>, -    specular_map: Option<TextureId>, -    textures: Vec<Texture>, +    ambient: Color<f32>, +    diffuse: Color<f32>, +    specular: Color<f32>, +    ambient_map: Option<Texture>, +    diffuse_map: Option<Texture>, +    specular_map: Option<Texture>,      shininess: f32,  } @@ -39,13 +52,12 @@ impl Builder      pub fn new() -> Self      {          Self { -            ambient: None, -            diffuse: None, -            specular: None, +            ambient: Color::WHITE_F32, +            diffuse: Color::WHITE_F32, +            specular: Color::WHITE_F32,              ambient_map: None,              diffuse_map: None,              specular_map: None, -            textures: Vec::new(),              shininess: 32.0,          }      } @@ -53,7 +65,7 @@ impl Builder      #[must_use]      pub fn ambient(mut self, ambient: Color<f32>) -> Self      { -        self.ambient = Some(ambient); +        self.ambient = ambient;          self      } @@ -61,7 +73,7 @@ impl Builder      #[must_use]      pub fn diffuse(mut self, diffuse: Color<f32>) -> Self      { -        self.diffuse = Some(diffuse); +        self.diffuse = diffuse;          self      } @@ -69,13 +81,13 @@ impl Builder      #[must_use]      pub fn specular(mut self, specular: Color<f32>) -> Self      { -        self.specular = Some(specular); +        self.specular = specular;          self      }      #[must_use] -    pub fn ambient_map(mut self, ambient_map: TextureId) -> Self +    pub fn ambient_map(mut self, ambient_map: Texture) -> Self      {          self.ambient_map = Some(ambient_map); @@ -83,7 +95,7 @@ impl Builder      }      #[must_use] -    pub fn diffuse_map(mut self, diffuse_map: TextureId) -> Self +    pub fn diffuse_map(mut self, diffuse_map: Texture) -> Self      {          self.diffuse_map = Some(diffuse_map); @@ -91,7 +103,7 @@ impl Builder      }      #[must_use] -    pub fn specular_map(mut self, specular_map: TextureId) -> Self +    pub fn specular_map(mut self, specular_map: Texture) -> Self      {          self.specular_map = Some(specular_map); @@ -99,22 +111,6 @@ impl Builder      }      #[must_use] -    pub fn textures(mut self, textures: impl IntoIterator<Item = Texture>) -> Self -    { -        self.textures = textures.into_iter().collect(); - -        self -    } - -    #[must_use] -    pub fn texture(mut self, texture: Texture) -> Self -    { -        self.textures.push(texture); - -        self -    } - -    #[must_use]      pub fn shininess(mut self, shininess: f32) -> Self      {          self.shininess = shininess; @@ -127,43 +123,15 @@ impl Builder      /// # Panics      /// Will panic if no ambient map, diffuse map or specular map is set.      #[must_use] -    pub fn build(mut self) -> Material +    pub fn build(self) -> Material      { -        let ambient_map = self.ambient_map.unwrap_or_else(|| { -            let texture = create_1x1_white_texture(); -            let texture_id = texture.id(); - -            self.textures.push(texture); - -            texture_id -        }); - -        let diffuse_map = self.diffuse_map.unwrap_or_else(|| { -            let texture = create_1x1_white_texture(); -            let texture_id = texture.id(); - -            self.textures.push(texture); - -            texture_id -        }); - -        let specular_map = self.specular_map.unwrap_or_else(|| { -            let texture = create_1x1_white_texture(); -            let texture_id = texture.id(); - -            self.textures.push(texture); - -            texture_id -        }); -          Material { -            ambient: self.ambient.unwrap_or(Color::WHITE_F32), -            diffuse: self.diffuse.unwrap_or(Color::WHITE_F32), -            specular: self.specular.unwrap_or(Color::WHITE_F32), -            ambient_map, -            diffuse_map, -            specular_map, -            textures: self.textures, +            ambient: self.ambient, +            diffuse: self.diffuse, +            specular: self.specular, +            ambient_map: self.ambient_map, +            diffuse_map: self.diffuse_map, +            specular_map: self.specular_map,              shininess: self.shininess,          }      } @@ -198,8 +166,3 @@ impl Flags          FlagsBuilder::default()      }  } - -fn create_1x1_white_texture() -> Texture -{ -    Texture::new_from_color(&Dimens { width: 1, height: 1 }, &Color::WHITE_U8) -} diff --git a/engine/src/math.rs b/engine/src/math.rs index b86e760..0340de8 100644 --- a/engine/src/math.rs +++ b/engine/src/math.rs @@ -13,5 +13,5 @@ pub fn calc_triangle_surface_normal(      let v1 = edge_b - egde_a;      let v2 = edge_c - egde_a; -    v1.cross(&v2) +    v1.cross(&v2).normalize()  } diff --git a/engine/src/mesh.rs b/engine/src/mesh.rs index de0af70..fb977af 100644 --- a/engine/src/mesh.rs +++ b/engine/src/mesh.rs @@ -1,10 +1,9 @@ -use ecs::Component; - -use crate::vertex::Vertex; +use crate::builder; +use crate::vector::{Vec2, Vec3};  pub mod cube; -#[derive(Debug, Clone, Component)] +#[derive(Debug, Clone, Default)]  pub struct Mesh  {      vertices: Vec<Vertex>, @@ -26,8 +25,140 @@ impl Mesh      }      #[must_use] +    pub fn vertices_mut(&mut self) -> &mut [Vertex] +    { +        &mut self.vertices +    } + +    #[must_use]      pub fn indices(&self) -> Option<&[u32]>      {          self.indices.as_deref()      } + +    #[must_use] +    pub fn indices_mut(&mut self) -> Option<&mut [u32]> +    { +        self.indices.as_deref_mut() +    } + +    /// Finds the vertex positions that are furthest in every 3D direction. Keep in mind +    /// that this can be quite time-expensive if the mesh has many vertices. +    pub fn find_furthest_vertex_positions(&self) -> DirectionPositions<'_> +    { +        let mut point_iter = self.vertices().iter().map(|vertex| &vertex.pos).into_iter(); + +        let first_point = point_iter.next().unwrap(); + +        point_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), +                }, +                |mut furthest_pos_acc, pos| { +                    furthest_pos_acc.up.update_if_further(pos); +                    furthest_pos_acc.down.update_if_further(pos); +                    furthest_pos_acc.left.update_if_further(pos); +                    furthest_pos_acc.right.update_if_further(pos); +                    furthest_pos_acc.back.update_if_further(pos); +                    furthest_pos_acc.front.update_if_further(pos); + +                    furthest_pos_acc +                }, +            ) +            .into() +    } +} + +builder! { +#[builder(name = VertexBuilder, derives = (Debug, Default, Clone))] +#[derive(Debug, Clone, Default, PartialEq)] +#[non_exhaustive] +pub struct Vertex +{ +    pub pos: Vec3<f32>, +    pub texture_coords: Vec2<f32>, +    pub normal: Vec3<f32>, +} +} + +impl Vertex +{ +    #[must_use] +    pub fn builder() -> VertexBuilder +    { +        VertexBuilder::default() +    } +} + +#[derive(Debug, Clone)] +pub struct DirectionPositions<'mesh> +{ +    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>, +} + +impl<'mesh> From<FurthestPosAcc<'mesh>> for DirectionPositions<'mesh> +{ +    fn from(acc: FurthestPosAcc<'mesh>) -> Self +    { +        Self { +            up: acc.up.pos, +            down: acc.down.pos, +            left: acc.left.pos, +            right: acc.right.pos, +            back: acc.back.pos, +            front: acc.front.pos, +        } +    } +} + +#[derive(Debug)] +struct FurthestPosAcc<'mesh> +{ +    up: FurthestPos<'mesh>, +    down: FurthestPos<'mesh>, +    left: FurthestPos<'mesh>, +    right: FurthestPos<'mesh>, +    back: FurthestPos<'mesh>, +    front: FurthestPos<'mesh>, +} + +#[derive(Debug)] +struct FurthestPos<'mesh> +{ +    pos: &'mesh Vec3<f32>, +    dot_prod: f32, +    direction: &'mesh Vec3<f32>, +} + +impl<'mesh> FurthestPos<'mesh> +{ +    fn new(pos: &'mesh Vec3<f32>, direction: &'mesh Vec3<f32>) -> Self +    { +        Self { +            pos, +            dot_prod: direction.dot(&pos), +            direction, +        } +    } + +    fn update_if_further(&mut self, point: &'mesh Vec3<f32>) +    { +        let point_dot_prod = self.direction.dot(point); + +        if point_dot_prod > self.dot_prod { +            self.pos = point; +            self.dot_prod = point_dot_prod; +        } +    }  } diff --git a/engine/src/mesh/cube.rs b/engine/src/mesh/cube.rs index 7cdf885..e91cf0e 100644 --- a/engine/src/mesh/cube.rs +++ b/engine/src/mesh/cube.rs @@ -1,8 +1,8 @@ +use crate::data_types::dimens::Dimens3;  use crate::math::calc_triangle_surface_normal; -use crate::mesh::Mesh; -use crate::util::builder; -use crate::vector::Vec3; -use crate::vertex::{Builder as VertexBuilder, Vertex}; +use crate::mesh::{Mesh, Vertex}; +use crate::builder; +use crate::vector::{Vec2, Vec3};  builder! {  /// Cube mesh creation specification. @@ -27,676 +27,467 @@ impl CreationSpec      }  } -#[derive(Debug)] +impl CreationSpecBuilder +{ +    pub fn dimens(mut self, dimens: Dimens3<f32>) -> Self +    { +        self.width = dimens.width; +        self.height = dimens.height; +        self.depth = dimens.depth; + +        self +    } +} + +/// Describes a single side of a cube (obviously). +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]  pub enum Side  { +    /// +Z      Front, + +    /// -Z      Back, + +    /// -X      Left, + +    /// +X      Right, + +    /// +Y      Top, + +    /// -Y      Bottom,  } -#[derive(Debug)] -pub enum Corner +/// Describes what location on a side of a cube a face is. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum FaceLocation  { -    TopRight, -    TopLeft, -    BottomRight, -    BottomLeft, +    /// 🮝 +    RightUp, + +    /// 🮟 +    LeftDown,  }  /// Creates a cube mesh. +/// +/// 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, -    vertex_builder_cb: impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, +    face_cb: impl FnMut(FaceVertices, Side, FaceLocation) -> FaceVertices,  ) -> Mesh  { -    let mut vertices = [const { None }; VertexIndex::VARIANT_CNT]; - -    create_front(&creation_spec, &mut vertices, &vertex_builder_cb); -    create_back(&creation_spec, &mut vertices, &vertex_builder_cb); -    create_right(&creation_spec, &mut vertices, &vertex_builder_cb); -    create_left(&creation_spec, &mut vertices, &vertex_builder_cb); -    create_top(&creation_spec, &mut vertices, &vertex_builder_cb); -    create_bottom(&creation_spec, &mut vertices, &vertex_builder_cb); - -    Mesh::new( -        vertices.map(Option::unwrap).to_vec(), -        Some( -            VERTEX_INDICES -                .into_iter() -                .flatten() -                .map(|index| index as u32) -                .collect(), -        ), -    ) -} +    let mut data = Data::default(); -macro_rules! one { -    ($tt: tt) => { -        1 -    }; -} +    create_side(&SidePositions::new_top(&creation_spec), &mut data); +    create_side(&SidePositions::new_bottom(&creation_spec), &mut data); +    create_side(&SidePositions::new_left(&creation_spec), &mut data); +    create_side(&SidePositions::new_right(&creation_spec), &mut data); +    create_side(&SidePositions::new_back(&creation_spec), &mut data); +    create_side(&SidePositions::new_front(&creation_spec), &mut data); -macro_rules! enum_with_variant_cnt { -    ( -        $(#[$attr: meta])* -        enum $name: ident { -            $($variant: ident,)* -        } -    ) => { -        $(#[$attr])* -        enum $name { -            $($variant,)* -        } - -        impl $name { -            const VARIANT_CNT: usize = 0 $(+ one!($variant))*; -        } -    }; +    data.into_mesh(face_cb)  } -enum_with_variant_cnt! { -#[repr(u32)] -enum VertexIndex +#[derive(Debug, Default)] +struct Data  { -    FrontTopRight, -    FrontBottomRight, -    FrontBottomLeft, -    FrontTopLeft, - -    BackTopRight, -    BackBottomRight, -    BackBottomLeft, -    BackTopLeft, - -    RightBackTop, -    RightBackBottom, -    RightFrontTop, -    RightFrontBottom, - -    LeftBackTop, -    LeftBackBottom, -    LeftFrontTop, -    LeftFrontBottom, - -    TopBackRight, -    TopBackLeft, -    TopFrontRight, -    TopFrontLeft, - -    BottomBackRight, -    BottomBackLeft, -    BottomFrontRight, -    BottomFrontLeft, -} +    faces: Vec<Face>, +    vertex_data: VertexData,  } -fn create_front( -    creation_spec: &CreationSpec, -    vertices: &mut [Option<Vertex>], -    vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) +#[derive(Debug, Default)] +struct VertexData  { -    let front_top_right_pos = Vec3 { -        x: creation_spec.width / 2.0, -        y: creation_spec.height / 2.0, -        z: -(creation_spec.depth / 2.0), -    }; - -    let front_bottom_right_pos = Vec3 { -        x: creation_spec.width / 2.0, -        y: -(creation_spec.height / 2.0), -        z: -(creation_spec.depth / 2.0), -    }; +    vertex_positions: Vec<Vec3<f32>>, +    vertex_normals: Vec<Vec3<f32>>, +} -    let front_bottom_left_pos = Vec3 { -        x: -(creation_spec.width / 2.0), -        y: -(creation_spec.height / 2.0), -        z: -(creation_spec.depth / 2.0), -    }; +impl Data +{ +    fn into_mesh( +        self, +        mut face_cb: impl FnMut(FaceVertices, Side, FaceLocation) -> FaceVertices, +    ) -> Mesh +    { +        let mut vertices = Vec::<Vertex>::with_capacity(self.faces.len() * 3); +        let mut indices = Vec::<u32>::with_capacity(self.faces.len() * 3); -    let front_top_left_pos = Vec3 { -        x: -(creation_spec.width / 2.0), -        y: creation_spec.height / 2.0, -        z: -(creation_spec.depth / 2.0), -    }; +        let mut face_location = FaceLocation::RightUp; -    let front_normal = calc_triangle_surface_normal( -        &front_top_right_pos, -        &front_bottom_right_pos, -        &front_top_left_pos, -    ); - -    vertices[VertexIndex::FrontTopRight as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(front_top_right_pos) -                .normal(front_normal), -            Side::Front, -            Corner::TopRight, -        ) -        .build(), -    ); - -    vertices[VertexIndex::FrontBottomRight as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(front_bottom_right_pos) -                .normal(front_normal), -            Side::Front, -            Corner::BottomRight, -        ) -        .build(), -    ); - -    vertices[VertexIndex::FrontBottomLeft as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(front_bottom_left_pos) -                .normal(front_normal), -            Side::Front, -            Corner::BottomLeft, -        ) -        .build(), -    ); - -    vertices[VertexIndex::FrontTopLeft as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(front_top_left_pos) -                .normal(front_normal), -            Side::Front, -            Corner::TopLeft, -        ) -        .build(), -    ); -} +        let Self { faces, vertex_data } = self; -fn create_back( -    creation_spec: &CreationSpec, -    vertices: &mut [Option<Vertex>], -    vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) -{ -    let back_top_right_pos = Vec3 { -        x: creation_spec.width / 2.0, -        y: creation_spec.height / 2.0, -        z: creation_spec.depth / 2.0, -    }; +        for face in faces { +            let side = face.side; -    let back_bottom_right_pos = Vec3 { -        x: creation_spec.width / 2.0, -        y: -(creation_spec.height / 2.0), -        z: creation_spec.depth / 2.0, -    }; +            let face_vertices = face_cb( +                FaceVertices::new(face, &vertex_data) +                    .with_full_per_side_tex_coords(face_location), +                side, +                face_location, +            ); -    let back_bottom_left_pos = Vec3 { -        x: -(creation_spec.width / 2.0), -        y: -(creation_spec.height / 2.0), -        z: creation_spec.depth / 2.0, -    }; +            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 back_top_left_pos = Vec3 { -        x: -(creation_spec.width / 2.0), -        y: creation_spec.height / 2.0, -        z: creation_spec.depth / 2.0, -    }; +                    continue; +                } -    let back_normal = -calc_triangle_surface_normal( -        &back_top_right_pos, -        &back_bottom_right_pos, -        &back_top_left_pos, -    ); - -    vertices[VertexIndex::BackTopRight as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(back_top_right_pos) -                .normal(back_normal), -            Side::Back, -            Corner::TopRight, -        ) -        .build(), -    ); - -    vertices[VertexIndex::BackBottomRight as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(back_bottom_right_pos) -                .normal(back_normal), -            Side::Back, -            Corner::BottomRight, -        ) -        .build(), -    ); - -    vertices[VertexIndex::BackBottomLeft as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(back_bottom_left_pos) -                .normal(back_normal), -            Side::Back, -            Corner::BottomLeft, -        ) -        .build(), -    ); - -    vertices[VertexIndex::BackTopLeft as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(back_top_left_pos) -                .normal(back_normal), -            Side::Back, -            Corner::TopLeft, -        ) -        .build(), -    ); -} +                vertices.push(vertex); -fn create_right( -    creation_spec: &CreationSpec, -    vertices: &mut [Option<Vertex>], -    vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) -{ -    let right_back_top_pos = Vec3 { -        x: creation_spec.width / 2.0, -        y: creation_spec.height / 2.0, -        z: creation_spec.depth / 2.0, -    }; +                let vertex_index = u32::try_from(vertices.len() - 1) +                    .expect("Vertex index does not fit into 32-bit unsigned int"); -    let right_back_bottom_pos = Vec3 { -        x: creation_spec.width / 2.0, -        y: -(creation_spec.height / 2.0), -        z: creation_spec.depth / 2.0, -    }; +                indices.push(vertex_index); +            } -    let right_front_top_pos = Vec3 { -        x: creation_spec.width / 2.0, -        y: creation_spec.height / 2.0, -        z: -(creation_spec.depth / 2.0), -    }; +            match face_location { +                FaceLocation::RightUp => face_location = FaceLocation::LeftDown, +                FaceLocation::LeftDown => face_location = FaceLocation::RightUp, +            } +        } -    let right_front_bottom_pos = Vec3 { -        x: creation_spec.width / 2.0, -        y: -(creation_spec.height / 2.0), -        z: -(creation_spec.depth / 2.0), -    }; +        Mesh::new(vertices, Some(indices)) +    } +} -    let right_normal = calc_triangle_surface_normal( -        &right_back_top_pos, -        &right_back_bottom_pos, -        &right_front_top_pos, -    ); - -    vertices[VertexIndex::RightBackTop as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(right_back_top_pos) -                .normal(right_normal), -            Side::Right, -            Corner::TopLeft, -        ) -        .build(), -    ); - -    vertices[VertexIndex::RightBackBottom as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(right_back_bottom_pos) -                .normal(right_normal), -            Side::Right, -            Corner::BottomLeft, -        ) -        .build(), -    ); - -    vertices[VertexIndex::RightFrontTop as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(right_front_top_pos) -                .normal(right_normal), -            Side::Right, -            Corner::TopRight, -        ) -        .build(), -    ); - -    vertices[VertexIndex::RightFrontBottom as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(right_front_bottom_pos) -                .normal(right_normal), -            Side::Right, -            Corner::BottomRight, -        ) -        .build(), -    ); +/// 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],  } -fn create_left( -    creation_spec: &CreationSpec, -    vertices: &mut [Option<Vertex>], -    vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) +impl FaceVertices  { -    let left_back_top_pos = Vec3 { -        x: -(creation_spec.width / 2.0), -        y: creation_spec.height / 2.0, -        z: creation_spec.depth / 2.0, -    }; +    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) +                    .expect("Vertex position index is out of bounds") +                    .clone(); + +                let vertex_normal = vertex_data +                    .vertex_normals +                    .get(face_vertex.normal_index as usize) +                    .expect("Vertex normal index is out of bounds") +                    .clone(); + +                Vertex::builder() +                    .pos(vertex_pos) +                    .normal(vertex_normal) +                    .build() +            }), +        } +    } -    let left_back_bottom_pos = Vec3 { -        x: -(creation_spec.width / 2.0), -        y: -(creation_spec.height / 2.0), -        z: creation_spec.depth / 2.0, -    }; +    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 }; +            } +            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 }; +            } +        }; + +        self +    } +} -    let left_front_top_pos = Vec3 { -        x: -(creation_spec.width / 2.0), -        y: creation_spec.height / 2.0, -        z: -(creation_spec.depth / 2.0), -    }; +#[derive(Debug)] +struct Face +{ +    vertices: [FaceVertex; 3], +    side: Side, +} -    let left_front_bottom_pos = Vec3 { -        x: -(creation_spec.width / 2.0), -        y: -(creation_spec.height / 2.0), -        z: -(creation_spec.depth / 2.0), -    }; +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +struct FaceVertex +{ +    pos_index: u32, +    normal_index: u32, +} -    let left_normal = -calc_triangle_surface_normal( -        &left_back_top_pos, -        &left_back_bottom_pos, -        &left_front_top_pos, -    ); - -    vertices[VertexIndex::LeftBackTop as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(left_back_top_pos) -                .normal(left_normal), -            Side::Left, -            Corner::TopRight, -        ) -        .build(), -    ); - -    vertices[VertexIndex::LeftBackBottom as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(left_back_bottom_pos) -                .normal(left_normal), -            Side::Left, -            Corner::BottomRight, -        ) -        .build(), -    ); - -    vertices[VertexIndex::LeftFrontTop as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(left_front_top_pos) -                .normal(left_normal), -            Side::Left, -            Corner::TopLeft, -        ) -        .build(), -    ); - -    vertices[VertexIndex::LeftFrontBottom as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(left_front_bottom_pos) -                .normal(left_normal), -            Side::Left, -            Corner::BottomLeft, -        ) -        .build(), -    ); +#[derive(Debug)] +struct SidePositions +{ +    up_left: Vec3<f32>, +    up_right: Vec3<f32>, +    down_left: Vec3<f32>, +    down_right: Vec3<f32>, +    normal_calc_order: NormalCalcOrder, +    side: Side,  } -fn create_top( -    creation_spec: &CreationSpec, -    vertices: &mut [Option<Vertex>], -    vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) +impl SidePositions  { -    let top_back_right_pos = Vec3 { -        x: creation_spec.width / 2.0, -        y: creation_spec.height / 2.0, -        z: creation_spec.depth / 2.0, -    }; +    fn new_top(creation_spec: &CreationSpec) -> Self +    { +        let up_left = Vec3 { +            x: -(creation_spec.width / 2.0), +            y: creation_spec.height / 2.0, +            z: creation_spec.depth / 2.0, +        }; + +        let down_right = Vec3 { +            x: creation_spec.width / 2.0, +            y: creation_spec.height / 2.0, +            z: -(creation_spec.depth / 2.0), +        }; + +        Self { +            up_left, +            up_right: Vec3 { x: down_right.x, ..up_left.clone() }, +            down_left: Vec3 { x: up_left.x, ..down_right.clone() }, +            down_right, +            normal_calc_order: NormalCalcOrder::Clockwise, +            side: Side::Top, +        } +    } -    let top_back_left_pos = Vec3 { -        x: -(creation_spec.width / 2.0), -        y: creation_spec.height / 2.0, -        z: creation_spec.depth / 2.0, -    }; +    fn new_bottom(creation_spec: &CreationSpec) -> Self +    { +        let up_left = Vec3 { +            x: -(creation_spec.width / 2.0), +            y: -creation_spec.height / 2.0, +            z: creation_spec.depth / 2.0, +        }; + +        let down_right = Vec3 { +            x: creation_spec.width / 2.0, +            y: -creation_spec.height / 2.0, +            z: -(creation_spec.depth / 2.0), +        }; + +        Self { +            up_left, +            up_right: Vec3 { x: down_right.x, ..up_left.clone() }, +            down_left: Vec3 { x: up_left.x, ..down_right.clone() }, +            down_right, +            normal_calc_order: NormalCalcOrder::CounterClockwise, +            side: Side::Bottom, +        } +    } -    let top_front_left_pos = Vec3 { -        x: -(creation_spec.width / 2.0), -        y: creation_spec.height / 2.0, -        z: -(creation_spec.depth / 2.0), -    }; +    fn new_left(creation_spec: &CreationSpec) -> Self +    { +        let up_left = Vec3 { +            x: -(creation_spec.width / 2.0), +            y: creation_spec.height / 2.0, +            z: -(creation_spec.depth / 2.0), +        }; + +        let down_right = Vec3 { +            x: -(creation_spec.width / 2.0), +            y: -(creation_spec.height / 2.0), +            z: creation_spec.depth / 2.0, +        }; + +        Self { +            up_left, +            up_right: Vec3 { z: down_right.z, ..up_left.clone() }, +            down_left: Vec3 { z: up_left.z, ..down_right.clone() }, +            down_right, +            normal_calc_order: NormalCalcOrder::CounterClockwise, +            side: Side::Left, +        } +    } -    let top_front_right_pos = Vec3 { -        x: creation_spec.width / 2.0, -        y: creation_spec.height / 2.0, -        z: -(creation_spec.depth / 2.0), -    }; +    fn new_right(creation_spec: &CreationSpec) -> Self +    { +        let up_left = Vec3 { +            x: (creation_spec.width / 2.0), +            y: creation_spec.height / 2.0, +            z: -(creation_spec.depth / 2.0), +        }; + +        let down_right = Vec3 { +            x: (creation_spec.width / 2.0), +            y: -(creation_spec.height / 2.0), +            z: creation_spec.depth / 2.0, +        }; + +        Self { +            up_left, +            up_right: Vec3 { z: down_right.z, ..up_left.clone() }, +            down_left: Vec3 { z: up_left.z, ..down_right.clone() }, +            down_right, +            normal_calc_order: NormalCalcOrder::Clockwise, +            side: Side::Right, +        } +    } + +    fn new_back(creation_spec: &CreationSpec) -> Self +    { +        let up_left = Vec3 { +            x: -(creation_spec.width / 2.0), +            y: creation_spec.height / 2.0, +            z: -creation_spec.depth / 2.0, +        }; + +        let down_right = Vec3 { +            x: creation_spec.width / 2.0, +            y: -(creation_spec.height / 2.0), +            z: -creation_spec.depth / 2.0, +        }; + +        Self { +            up_left, +            up_right: Vec3 { x: down_right.x, ..up_left.clone() }, +            down_left: Vec3 { x: up_left.x, ..down_right.clone() }, +            down_right, +            normal_calc_order: NormalCalcOrder::Clockwise, +            side: Side::Back, +        } +    } -    let top_normal = -calc_triangle_surface_normal( -        &top_back_right_pos, -        &top_back_left_pos, -        &top_front_right_pos, -    ); - -    vertices[VertexIndex::TopBackRight as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(top_back_right_pos) -                .normal(top_normal), -            Side::Top, -            Corner::TopRight, -        ) -        .build(), -    ); - -    vertices[VertexIndex::TopBackLeft as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(top_back_left_pos) -                .normal(top_normal), -            Side::Top, -            Corner::TopLeft, -        ) -        .build(), -    ); - -    vertices[VertexIndex::TopFrontLeft as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(top_front_left_pos) -                .normal(top_normal), -            Side::Top, -            Corner::BottomLeft, -        ) -        .build(), -    ); - -    vertices[VertexIndex::TopFrontRight as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(top_front_right_pos) -                .normal(top_normal), -            Side::Top, -            Corner::BottomRight, -        ) -        .build(), -    ); +    fn new_front(creation_spec: &CreationSpec) -> Self +    { +        let up_left = Vec3 { +            x: -(creation_spec.width / 2.0), +            y: creation_spec.height / 2.0, +            z: creation_spec.depth / 2.0, +        }; + +        let down_right = Vec3 { +            x: creation_spec.width / 2.0, +            y: -(creation_spec.height / 2.0), +            z: creation_spec.depth / 2.0, +        }; + +        Self { +            up_left, +            up_right: Vec3 { x: down_right.x, ..up_left.clone() }, +            down_left: Vec3 { x: up_left.x, ..down_right.clone() }, +            down_right, +            normal_calc_order: NormalCalcOrder::CounterClockwise, +            side: Side::Front, +        } +    }  } -fn create_bottom( -    creation_spec: &CreationSpec, -    vertices: &mut [Option<Vertex>], -    vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) +#[derive(Debug)] +enum NormalCalcOrder  { -    let bottom_back_right_pos = Vec3 { -        x: creation_spec.width / 2.0, -        y: -(creation_spec.height / 2.0), -        z: (creation_spec.depth / 2.0), -    }; +    Clockwise, +    CounterClockwise, +} -    let bottom_back_left_pos = Vec3 { -        x: -(creation_spec.width / 2.0), -        y: -(creation_spec.height / 2.0), -        z: creation_spec.depth / 2.0, +fn create_side(side_positions: &SidePositions, data: &mut Data) +{ +    let normal = match side_positions.normal_calc_order { +        NormalCalcOrder::Clockwise => calc_triangle_surface_normal( +            &side_positions.up_left, +            &side_positions.up_right, +            &side_positions.down_left, +        ), +        NormalCalcOrder::CounterClockwise => calc_triangle_surface_normal( +            &side_positions.up_left, +            &side_positions.down_left, +            &side_positions.up_right, +        ),      }; -    let bottom_front_right_pos = Vec3 { -        x: creation_spec.width / 2.0, -        y: -(creation_spec.height / 2.0), -        z: -(creation_spec.depth / 2.0), -    }; +    data.vertex_data.vertex_normals.push(normal); -    let bottom_front_left_pos = Vec3 { -        x: -(creation_spec.width / 2.0), -        y: -(creation_spec.height / 2.0), -        z: -(creation_spec.depth / 2.0), -    }; +    let top_normal_index = data.vertex_data.vertex_normals.len() - 1; -    let bottom_normal = calc_triangle_surface_normal( -        &bottom_back_right_pos, -        &bottom_back_left_pos, -        &bottom_front_right_pos, -    ); - -    vertices[VertexIndex::BottomBackRight as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(bottom_back_right_pos) -                .normal(bottom_normal), -            Side::Bottom, -            Corner::BottomRight, -        ) -        .build(), -    ); - -    vertices[VertexIndex::BottomBackLeft as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(bottom_back_left_pos) -                .normal(bottom_normal), -            Side::Bottom, -            Corner::BottomLeft, -        ) -        .build(), -    ); - -    vertices[VertexIndex::BottomFrontRight as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(bottom_front_right_pos) -                .normal(bottom_normal), -            Side::Bottom, -            Corner::TopRight, -        ) -        .build(), -    ); - -    vertices[VertexIndex::BottomFrontLeft as usize] = Some( -        vertex_builder_cb( -            VertexBuilder::default() -                .pos(bottom_front_left_pos) -                .normal(bottom_normal), -            Side::Bottom, -            Corner::TopLeft, -        ) -        .build(), -    ); -} +    data.vertex_data +        .vertex_positions +        .push(side_positions.up_right); -const VERTEX_INDICES_FRONT: [VertexIndex; 6] = [ -    // 🮝 -    VertexIndex::FrontTopRight, -    VertexIndex::FrontBottomRight, -    VertexIndex::FrontTopLeft, -    // -    // 🮟 -    VertexIndex::FrontBottomRight, -    VertexIndex::FrontBottomLeft, -    VertexIndex::FrontTopLeft, -]; +    let up_right_pos_index = data.vertex_data.vertex_positions.len() - 1; -const VERTEX_INDICES_BACK: [VertexIndex; 6] = [ -    // 🮝 -    VertexIndex::BackTopRight, -    VertexIndex::BackBottomRight, -    VertexIndex::BackTopLeft, -    // -    // 🮟 -    VertexIndex::BackBottomRight, -    VertexIndex::BackBottomLeft, -    VertexIndex::BackTopLeft, -]; +    data.vertex_data +        .vertex_positions +        .push(side_positions.up_left); -const VERTEX_INDICES_RIGHT: [VertexIndex; 6] = [ -    // 🮝 -    VertexIndex::RightBackTop, -    VertexIndex::RightBackBottom, -    VertexIndex::RightFrontTop, -    // -    // 🮟 -    VertexIndex::RightBackBottom, -    VertexIndex::RightFrontBottom, -    VertexIndex::RightFrontTop, -]; +    let up_left_pos_index = data.vertex_data.vertex_positions.len() - 1; -const VERTEX_INDICES_LEFT: [VertexIndex; 6] = [ -    // 🮝 -    VertexIndex::LeftBackTop, -    VertexIndex::LeftBackBottom, -    VertexIndex::LeftFrontTop, -    // -    // 🮟 -    VertexIndex::LeftBackBottom, -    VertexIndex::LeftFrontBottom, -    VertexIndex::LeftFrontTop, -]; +    data.vertex_data +        .vertex_positions +        .push(side_positions.down_left); -const VERTEX_INDICES_TOP: [VertexIndex; 6] = [ -    // 🮝 -    VertexIndex::TopBackRight, -    VertexIndex::TopBackLeft, -    VertexIndex::TopFrontRight, -    // -    // 🮟 -    VertexIndex::TopBackLeft, -    VertexIndex::TopFrontLeft, -    VertexIndex::TopFrontRight, -]; +    let down_left_pos_index = data.vertex_data.vertex_positions.len() - 1; + +    data.vertex_data +        .vertex_positions +        .push(side_positions.down_right); + +    let down_right_pos_index = data.vertex_data.vertex_positions.len() - 1; -const VERTEX_INDICES_BOTTOM: [VertexIndex; 6] = [      // 🮝 -    VertexIndex::BottomBackRight, -    VertexIndex::BottomBackLeft, -    VertexIndex::BottomFrontRight, -    // +    data.faces.push(Face { +        vertices: [ +            FaceVertex { +                pos_index: up_right_pos_index as u32, +                normal_index: top_normal_index as u32, +            }, +            FaceVertex { +                pos_index: up_left_pos_index as u32, +                normal_index: top_normal_index as u32, +            }, +            FaceVertex { +                pos_index: down_right_pos_index as u32, +                normal_index: top_normal_index as u32, +            }, +        ], +        side: side_positions.side, +    }); +      // 🮟 -    VertexIndex::BottomBackLeft, -    VertexIndex::BottomFrontLeft, -    VertexIndex::BottomFrontRight, -]; - -const VERTEX_INDICES: [[VertexIndex; 6]; 6] = [ -    VERTEX_INDICES_FRONT, -    VERTEX_INDICES_BACK, -    VERTEX_INDICES_RIGHT, -    VERTEX_INDICES_LEFT, -    VERTEX_INDICES_TOP, -    VERTEX_INDICES_BOTTOM, -]; +    data.faces.push(Face { +        vertices: [ +            FaceVertex { +                pos_index: up_left_pos_index as u32, +                normal_index: top_normal_index as u32, +            }, +            FaceVertex { +                pos_index: down_left_pos_index as u32, +                normal_index: top_normal_index as u32, +            }, +            FaceVertex { +                pos_index: down_right_pos_index as u32, +                normal_index: top_normal_index as u32, +            }, +        ], +        side: side_positions.side, +    }); +} diff --git a/engine/src/model.rs b/engine/src/model.rs new file mode 100644 index 0000000..9f5840c --- /dev/null +++ b/engine/src/model.rs @@ -0,0 +1,176 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::fs::read_to_string; +use std::path::Path; + +use ecs::Component; + +use crate::asset::{Assets, Handle as AssetHandle, Submitter as AssetSubmitter}; +use crate::material::Material; +use crate::mesh::Mesh; +use crate::texture::Texture; + +#[derive(Debug, Clone, Component)] +#[non_exhaustive] +pub struct Model +{ +    pub asset_handle: AssetHandle<Data>, +} + +impl Model +{ +    pub fn new(asset_handle: AssetHandle<Data>) -> Self +    { +        Self { asset_handle } +    } +} + +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct Data +{ +    pub mesh: Mesh, +    pub materials: HashMap<String, Material>, +} + +impl Data +{ +    pub fn builder() -> DataBuilder +    { +        DataBuilder::default() +    } +} + +#[derive(Debug, Default, Clone)] +pub struct DataBuilder +{ +    mesh: Mesh, +    materials: HashMap<String, Material>, +} + +impl DataBuilder +{ +    pub fn mesh(mut self, mesh: Mesh) -> Self +    { +        self.mesh = mesh; + +        self +    } + +    pub fn material<'name>( +        mut self, +        name: impl Into<Cow<'name, str>>, +        material: Material, +    ) -> Self +    { +        self.materials.insert(name.into().into_owned(), material); + +        self +    } + +    pub fn build(self) -> Data +    { +        Data { +            mesh: self.mesh, +            materials: self.materials, +        } +    } +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct Settings {} + +#[derive(Debug, thiserror::Error)] +enum Error +{ +    #[error("Failed to read model file")] +    ReadModelFileFailed(#[source] std::io::Error), + +    #[error("Failed to read material file")] +    ReadMaterialFileFailed(#[source] std::io::Error), + +    #[error("Failed to parse model file")] +    ParsingFailed(#[from] ParsingError), +} + +pub fn set_asset_importers(assets: &mut Assets) +{ +    assets.set_importer(["obj"], import_wavefront_obj_asset); +} + +#[derive(Debug, thiserror::Error)] +enum ParsingError +{ +    #[error(transparent)] +    Obj(#[from] crate::file_format::wavefront::obj::Error), + +    #[error(transparent)] +    Mtl(#[from] crate::file_format::wavefront::mtl::Error), +} + +fn import_wavefront_obj_asset( +    asset_submitter: &mut AssetSubmitter<'_>, +    path: &Path, +    _settings: Option<&'_ Settings>, +) -> Result<(), Error> +{ +    let obj = crate::file_format::wavefront::obj::parse( +        &read_to_string(path).map_err(Error::ReadModelFileFailed)?, +    ) +    .map_err(|err| Error::ParsingFailed(ParsingError::Obj(err)))?; + +    let mesh = obj +        .to_mesh() +        .map_err(|err| Error::ParsingFailed(ParsingError::Obj(err)))?; + +    let mut materials = +        HashMap::<String, Material>::with_capacity(obj.mtl_libs.iter().flatten().count()); + +    for mtl_lib_path in obj.mtl_libs.iter().flatten() { +        materials.extend(import_mtl(asset_submitter, &mtl_lib_path)?); +    } + +    asset_submitter.submit_store(Data { mesh, materials }); + +    Ok(()) +} + +fn import_mtl<'a>( +    asset_submitter: &'a AssetSubmitter<'_>, +    path: &Path, +) -> Result<impl Iterator<Item = (String, Material)> + 'a, Error> +{ +    let named_materials = crate::file_format::wavefront::mtl::parse( +        &read_to_string(path).map_err(Error::ReadMaterialFileFailed)?, +    ) +    .map_err(|err| Error::ParsingFailed(ParsingError::Mtl(err)))?; + +    Ok(named_materials.into_iter().map(|named_material| { +        let mut material_builder = Material::builder() +            .ambient(named_material.ambient) +            .diffuse(named_material.diffuse) +            .specular(named_material.specular) +            .shininess(named_material.shininess); + +        if let Some(ambient_map) = named_material.ambient_map { +            material_builder = material_builder.ambient_map(Texture::new( +                asset_submitter.submit_load_other(ambient_map.path.as_path()), +            )); +        } + +        if let Some(diffuse_map) = named_material.diffuse_map { +            material_builder = material_builder.diffuse_map(Texture::new( +                asset_submitter.submit_load_other(diffuse_map.path.as_path()), +            )); +        } + +        if let Some(specular_map) = named_material.specular_map { +            material_builder = material_builder.specular_map(Texture::new( +                asset_submitter.submit_load_other(specular_map.path.as_path()), +            )); +        } + +        (named_material.name, material_builder.build()) +    })) +} diff --git a/engine/src/opengl/buffer.rs b/engine/src/opengl/buffer.rs deleted file mode 100644 index 2be7f12..0000000 --- a/engine/src/opengl/buffer.rs +++ /dev/null @@ -1,92 +0,0 @@ -use std::marker::PhantomData; -use std::mem::size_of_val; - -#[derive(Debug)] -pub struct Buffer<Item> -{ -    buf: gl::types::GLuint, -    _pd: PhantomData<Item>, -} - -impl<Item> Buffer<Item> -{ -    pub fn new() -> Self -    { -        let mut buffer = gl::types::GLuint::default(); - -        unsafe { -            gl::CreateBuffers(1, &mut buffer); -        }; - -        Self { buf: buffer, _pd: PhantomData } -    } - -    /// Stores items in the currently bound buffer. -    pub fn store(&mut self, items: &[Item], usage: Usage) -    { -        unsafe { -            #[allow(clippy::cast_possible_wrap)] -            gl::NamedBufferData( -                self.buf, -                size_of_val(items) as gl::types::GLsizeiptr, -                items.as_ptr().cast(), -                usage.into_gl(), -            ); -        } -    } - -    pub fn object(&self) -> gl::types::GLuint -    { -        self.buf -    } - -    /// Does a weak clone of this buffer. The buffer itself is NOT copied in any way this -    /// function only copies the internal buffer ID. -    /// -    /// # Safety -    /// The returned `Buffer` must not be dropped if another `Buffer` referencing the -    /// same buffer ID is used later or if a [`VertexArray`] is used later. -    /// -    /// [`VertexArray`]: crate::opengl::vertex_array::VertexArray -    pub unsafe fn clone_weak(&self) -> Self -    { -        Self { buf: self.buf, _pd: PhantomData } -    } -} - -impl<Item> Drop for Buffer<Item> -{ -    fn drop(&mut self) -    { -        unsafe { -            gl::DeleteBuffers(1, &self.buf); -        } -    } -} - -/// Buffer usage. -#[derive(Debug)] -#[allow(dead_code)] -pub enum Usage -{ -    /// The buffer data is set only once and used by the GPU at most a few times. -    Stream, - -    /// The buffer data is set only once and used many times. -    Static, - -    /// The buffer data is changed a lot and used many times. -    Dynamic, -} - -impl Usage -{ -    fn into_gl(self) -> gl::types::GLenum -    { -        match self { -            Self::Stream => gl::STREAM_DRAW, -            Self::Static => gl::STATIC_DRAW, -            Self::Dynamic => gl::DYNAMIC_DRAW, -        } -    } -} diff --git a/engine/src/opengl/debug.rs b/engine/src/opengl/debug.rs deleted file mode 100644 index 203590a..0000000 --- a/engine/src/opengl/debug.rs +++ /dev/null @@ -1,145 +0,0 @@ -use std::ffi::c_void; -use std::io::{stderr, Write}; -use std::panic::catch_unwind; -use std::ptr::null_mut; -use std::sync::Mutex; - -use crate::opengl::util::gl_enum; - -pub type MessageCallback = fn( -    source: MessageSource, -    ty: MessageType, -    id: u32, -    severity: MessageSeverity, -    message: &str, -); - -pub fn enable_debug_output() -{ -    unsafe { -        gl::Enable(gl::DEBUG_OUTPUT); -        gl::Enable(gl::DEBUG_OUTPUT_SYNCHRONOUS); -    } -} - -pub fn set_debug_message_callback(cb: MessageCallback) -{ -    *DEBUG_MESSAGE_CB.lock().unwrap() = Some(cb); - -    unsafe { -        gl::DebugMessageCallback(Some(debug_message_cb), null_mut()); -    } -} - -pub fn set_debug_message_control( -    source: Option<MessageSource>, -    ty: Option<MessageType>, -    severity: Option<MessageSeverity>, -    ids: &[u32], -    ids_action: MessageIdsAction, -) -{ -    // Ids shouldn't realistically be large enough to cause a panic here -    let ids_len: i32 = ids.len().try_into().unwrap(); - -    unsafe { -        gl::DebugMessageControl( -            source.map_or(gl::DONT_CARE, |source| source as u32), -            ty.map_or(gl::DONT_CARE, |ty| ty as u32), -            severity.map_or(gl::DONT_CARE, |severity| severity as u32), -            ids_len, -            ids.as_ptr(), -            ids_action as u8, -        ); -    } -} - -#[derive(Debug, Clone, Copy)] -#[allow(dead_code)] -pub enum MessageIdsAction -{ -    Enable = 1, -    Disable = 0, -} - -gl_enum! { -pub enum MessageSource -{ -    Api = gl::DEBUG_SOURCE_API, -    WindowSystem = gl::DEBUG_SOURCE_WINDOW_SYSTEM, -    ShaderCompiler = gl::DEBUG_SOURCE_SHADER_COMPILER, -    ThirdParty = gl::DEBUG_SOURCE_THIRD_PARTY, -    Application = gl::DEBUG_SOURCE_APPLICATION, -    Other = gl::DEBUG_SOURCE_OTHER, -} -} - -gl_enum! { -pub enum MessageType -{ -    DeprecatedBehavior = gl::DEBUG_TYPE_DEPRECATED_BEHAVIOR, -    Error = gl::DEBUG_TYPE_ERROR, -    Marker = gl::DEBUG_TYPE_MARKER, -    Other = gl::DEBUG_TYPE_OTHER, -    Performance = gl::DEBUG_TYPE_PERFORMANCE, -    PopGroup = gl::DEBUG_TYPE_POP_GROUP, -    PushGroup = gl::DEBUG_TYPE_PUSH_GROUP, -    Portability = gl::DEBUG_TYPE_PORTABILITY, -    UndefinedBehavior = gl::DEBUG_TYPE_UNDEFINED_BEHAVIOR, -} -} - -gl_enum! { -pub enum MessageSeverity -{ -    High = gl::DEBUG_SEVERITY_HIGH, -    Medium = gl::DEBUG_SEVERITY_MEDIUM, -    Low = gl::DEBUG_SEVERITY_LOW, -    Notification = gl::DEBUG_SEVERITY_NOTIFICATION, -} -} - -static DEBUG_MESSAGE_CB: Mutex<Option<MessageCallback>> = Mutex::new(None); - -extern "system" fn debug_message_cb( -    source: gl::types::GLenum, -    ty: gl::types::GLenum, -    id: gl::types::GLuint, -    severity: gl::types::GLenum, -    message_length: gl::types::GLsizei, -    message: *const gl::types::GLchar, -    _user_param: *mut c_void, -) -{ -    // Unwinds are catched because unwinding from Rust code into foreign code is UB. -    let res = catch_unwind(|| { -        let cb_lock = DEBUG_MESSAGE_CB.lock().unwrap(); - -        if let Some(cb) = *cb_lock { -            let msg_source = MessageSource::from_gl(source).unwrap(); -            let msg_type = MessageType::from_gl(ty).unwrap(); -            let msg_severity = MessageSeverity::from_gl(severity).unwrap(); - -            let msg_length = usize::try_from(message_length).unwrap(); - -            // SAFETY: The received message should be a valid ASCII string -            let message = unsafe { -                std::str::from_utf8_unchecked(std::slice::from_raw_parts( -                    message.cast(), -                    msg_length, -                )) -            }; - -            cb(msg_source, msg_type, id, msg_severity, message); -        } -    }); - -    if res.is_err() { -        // eprintln is not used since it can panic and unwinds are unwanted because -        // unwinding from Rust code into foreign code is UB. -        stderr() -            .write_all(b"ERROR: Panic in debug message callback") -            .ok(); -        println!(); -    } -} diff --git a/engine/src/shader_preprocessor.rs b/engine/src/opengl/glsl.rs index 70696ac..6fd5638 100644 --- a/engine/src/shader_preprocessor.rs +++ b/engine/src/opengl/glsl.rs @@ -1,183 +1,181 @@ +use std::borrow::Cow; +use std::fmt::{Display, Formatter};  use std::path::{Path, PathBuf};  use std::string::FromUtf8Error;  const PREINCLUDE_DIRECTIVE: &str = "#preinclude"; -/// Preprocessor for shaders written in the OpenGL Shading Language. -pub struct ShaderPreprocessor +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>  { -    read_file: fn(&Path) -> Result<Vec<u8>, std::io::Error>, -    base_dir: PathBuf, +    do_preprocess(shader_content, SpanPath::Original, read_file)  } -impl ShaderPreprocessor +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>  { -    pub fn new(base_dir: PathBuf) -> Self -    { -        Self { -            read_file: |path| std::fs::read(path), -            base_dir, -        } -    } +    let shader_content = shader_content.into(); -    pub fn preprocess( -        &self, -        shader_content: String, -        shader_file_path: &Path, -    ) -> Result<String, Error> -    { -        let mut preincludes = shader_content -            .match_indices(PREINCLUDE_DIRECTIVE) -            .peekable(); +    let mut preincludes = shader_content +        .match_indices(PREINCLUDE_DIRECTIVE) +        .peekable(); -        if preincludes.peek().is_none() { -            // Shader content contains no preincludes -            return Ok(shader_content); -        }; +    if preincludes.peek().is_none() { +        // Shader content contains no preincludes +        return Ok(shader_content.into()); +    }; -        let mut preprocessed = shader_content.clone(); +    let mut preprocessed = shader_content.to_string(); -        let mut curr = shader_content.find(PREINCLUDE_DIRECTIVE); +    let mut curr = shader_content.find(PREINCLUDE_DIRECTIVE); -        let mut last_start = 0; -        let mut span_line_offset = 0; +    let mut last_start = 0; +    let mut span_line_offset = 0; -        while let Some(preinclude_start) = curr { -            let replacement_job = self.handle_preinclude( -                &preprocessed, -                shader_file_path, -                preinclude_start, -                span_line_offset, -            )?; +    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 path = replacement_job.path.clone(); -            let mut included = String::from_utf8( -                (self.read_file)(&self.base_dir.join(replacement_job.path.clone())) -                    .map_err(|err| Error::ReadIncludedShaderFailed { -                        source: err, -                        path: replacement_job.path.clone(), -                    })?, -            ) -            .map_err(|err| Error::IncludedShaderInvalidUtf8 { -                source: err, -                path: path.clone(), +        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(); -                } +        if let Some(first_line) = included.lines().next() { +            if first_line.starts_with("#version") { +                included = included +                    .chars() +                    .skip_while(|character| *character != '\n') +                    .collect();              } - -            let included_preprocessed = -                self.preprocess(included, &replacement_job.path)?; - -            let start = replacement_job.start_index; -            let end = replacement_job.end_index; - -            preprocessed.replace_range(start..end, &included_preprocessed); - -            curr = preprocessed[last_start + 1..] -                .find(PREINCLUDE_DIRECTIVE) -                .map(|index| index + 1); - -            last_start = preinclude_start + included_preprocessed.len(); - -            span_line_offset += included_preprocessed.lines().count();          } -        Ok(preprocessed) -    } +        let included_preprocessed = do_preprocess( +            &included, +            SpanPath::Path(replacement_job.path.as_path().into()), +            read_file, +        )?; -    fn handle_preinclude( -        &self, -        shader_content: &str, -        shader_file_path: &Path, -        preinclude_start_index: usize, -        span_line_offset: usize, -    ) -> Result<ReplacementJob, Error> -    { -        let expect_token = |token: char, index: usize| { -            let token_found = shader_content.chars().nth(index).ok_or_else(|| { -                Error::ExpectedToken { -                    expected: token, -                    span: Span::new( -                        shader_content, -                        &self.base_dir.join(shader_file_path), -                        index, -                        span_line_offset, -                        preinclude_start_index, -                    ), -                } -            })?; +        let start = replacement_job.start_index; +        let end = replacement_job.end_index; -            if token_found != token { -                return Err(Error::InvalidToken { -                    expected: token, -                    found: token_found, -                    span: Span::new( -                        shader_content, -                        &self.base_dir.join(shader_file_path), -                        index, -                        span_line_offset, -                        preinclude_start_index, -                    ), -                }); -            } +        preprocessed.replace_range(start..end, &included_preprocessed); -            Ok(()) -        }; +        curr = preprocessed[last_start + 1..] +            .find(PREINCLUDE_DIRECTIVE) +            .map(|index| index + 1); -        let space_index = preinclude_start_index + PREINCLUDE_DIRECTIVE.len(); -        let quote_open_index = space_index + 1; +        last_start = preinclude_start + included_preprocessed.len(); -        expect_token(' ', space_index)?; -        expect_token('"', quote_open_index)?; +        span_line_offset += included_preprocessed.lines().count(); +    } -        let buf = shader_content[quote_open_index + 1..] -            .chars() -            .take_while(|character| *character != '"') -            .map(|character| character as u8) -            .collect::<Vec<_>>(); +    Ok(preprocessed.into()) +} -        if buf.is_empty() { -            return Err(Error::ExpectedToken { -                expected: '"', +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, -                    &self.base_dir.join(shader_file_path), -                    shader_content.len() - 1, +                    shader_path.to_owned(), +                    index,                      span_line_offset,                      preinclude_start_index,                  ), -            }); -        } - -        let path_len = buf.len(); +            } +        })?; -        let path = PathBuf::from(String::from_utf8(buf).map_err(|err| { -            Error::PreincludePathInvalidUtf8 { -                source: err, +        if token_found != token { +            return Err(PreprocessingError::InvalidToken { +                expected: token, +                found: token_found,                  span: Span::new(                      shader_content, -                    &self.base_dir.join(shader_file_path), -                    quote_open_index + 1, +                    shader_path.to_owned(), +                    index,                      span_line_offset,                      preinclude_start_index,                  ), -            } -        })?); +            }); +        } -        Ok(ReplacementJob { -            start_index: preinclude_start_index, -            end_index: quote_open_index + 1 + path_len + 1, -            path, -        }) +        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 @@ -187,15 +185,14 @@ struct ReplacementJob      path: PathBuf,  } -/// Shader preprocessing error.  #[derive(Debug, thiserror::Error)] -pub enum Error +pub enum PreprocessingError  {      #[error(          "Invalid token at line {}, column {} of {}. Expected '{}', found '{}'",          span.line,          span.column, -        span.file.display(), +        span.path,          expected,          found      )] @@ -211,7 +208,7 @@ pub enum Error          expected,          span.line,          span.column, -        span.file.display(), +        span.path      )]      ExpectedToken      { @@ -222,7 +219,7 @@ pub enum Error          "Preinclude path at line {}, column {} of {} is invalid UTF-8",          span.line,          span.column, -        span.file.display(), +        span.path      )]      PreincludePathInvalidUtf8      { @@ -249,18 +246,19 @@ pub enum Error  }  #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive]  pub struct Span  { -    line: usize, -    column: usize, -    file: PathBuf, +    pub line: usize, +    pub column: usize, +    pub path: SpanPath<'static>,  }  impl Span  {      fn new(          file_content: &str, -        file_path: &Path, +        path: SpanPath<'static>,          char_index: usize,          line_offset: usize,          line_start_index: usize, @@ -272,7 +270,49 @@ impl Span          Self {              line,              column: char_index - line_start_index + 1, -            file: file_path.to_path_buf(), +            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(),          }      }  } @@ -290,21 +330,17 @@ fn find_line_of_index(text: &str, index: usize) -> usize  mod tests  {      use std::ffi::OsStr; -    use std::path::{Path, PathBuf}; +    use std::path::Path; -    use super::{Error, ShaderPreprocessor}; +    use super::{preprocess, PreprocessingError}; +    use crate::opengl::glsl::SpanPath;      #[test]      fn preprocess_no_directives_is_same()      {          assert_eq!( -            ShaderPreprocessor { -                read_file: |_| { unreachable!() }, -                base_dir: PathBuf::new() -            } -            .preprocess("#version 330 core\n".to_string(), Path::new("foo.glsl"),) -            .unwrap(), -            "#version 330 core\n".to_string() +            preprocess("#version 330 core\n", &|_| { unreachable!() }).unwrap(), +            "#version 330 core\n"          );      } @@ -312,20 +348,15 @@ mod tests      fn preprocess_with_directives_works()      {          assert_eq!( -            ShaderPreprocessor { -                read_file: |_| { Ok(b"out vec4 FragColor;".to_vec()) }, -                base_dir: PathBuf::new() -            } -            .preprocess( +            preprocess(                  concat!(                      "#version 330 core\n",                      "\n",                      "#preinclude \"foo.glsl\"\n",                      "\n",                      "void main() {}", -                ) -                .to_string(), -                Path::new("foo.glsl"), +                ), +                &|_| { Ok(b"out vec4 FragColor;".to_vec()) }              )              .unwrap(),              concat!( @@ -338,11 +369,7 @@ mod tests          );          assert_eq!( -            ShaderPreprocessor { -                read_file: |_| { Ok(b"out vec4 FragColor;".to_vec()) }, -                base_dir: PathBuf::new() -            } -            .preprocess( +            preprocess(                  concat!(                      "#version 330 core\n",                      "\n", @@ -351,9 +378,8 @@ mod tests                      "in vec3 in_frag_color;\n",                      "\n",                      "void main() {}", -                ) -                .to_string(), -                Path::new("foo.glsl"), +                ), +                &|_| { Ok(b"out vec4 FragColor;".to_vec()) }              )              .unwrap(),              concat!( @@ -368,8 +394,19 @@ mod tests          );          assert_eq!( -            ShaderPreprocessor { -                read_file: |path| { +            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 { @@ -381,22 +418,6 @@ mod tests                          .to_vec())                      }                  }, -                base_dir: PathBuf::new() -            } -            .preprocess( -                concat!( -                    "#version 330 core\n", -                    "\n", -                    "#preinclude \"bar.glsl\"\n", -                    "\n", -                    "in vec3 in_frag_color;\n", -                    "\n", -                    "#preinclude \"foo.glsl\"\n", -                    "\n", -                    "void main() {}", -                ) -                .to_string(), -                Path::new("foo.glsl"),              )              .unwrap(),              concat!( @@ -415,15 +436,9 @@ mod tests      }      #[test] -    fn preprocess_invalid_shader_does_not_work() +    fn preprocess_invalid_directive_does_not_work()      { -        let path = Path::new("foo.glsl"); - -        let res = ShaderPreprocessor { -            read_file: |_| Ok(b"out vec4 FragColor;".to_vec()), -            base_dir: PathBuf::new(), -        } -        .preprocess( +        let res = preprocess(              concat!(                  "#version 330 core\n",                  "\n", @@ -431,12 +446,11 @@ mod tests                  "#preinclude foo.glsl\"\n",                  "\n",                  "void main() {}", -            ) -            .to_string(), -            path, +            ), +            &|_| Ok(b"out vec4 FragColor;".to_vec()),          ); -        let Err(Error::InvalidToken { expected, found, span }) = res else { +        let Err(PreprocessingError::InvalidToken { expected, found, span }) = res else {              panic!(                  "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}"              ); @@ -446,16 +460,25 @@ mod tests          assert_eq!(found, 'f');          assert_eq!(span.line, 3);          assert_eq!(span.column, 13); -        assert_eq!(span.file, path); +        assert_eq!(span.path, SpanPath::Original);      }      #[test]      fn preprocess_error_has_correct_span()      { -        let path = Path::new("foo.glsl"); - -        let res = ShaderPreprocessor { -            read_file: |path| { +        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", @@ -464,29 +487,25 @@ mod tests                      )                      .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 { -                    Ok(b"uniform sampler2D input_texture;".to_vec()) +                    panic!(concat!( +                        "Expected read function to be called with ", +                        "either path bar.glsl or foo.glsl" +                    ));                  }              }, -            base_dir: PathBuf::new(), -        } -        .preprocess( -            concat!( -                "#version 330 core\n", -                "\n", -                "#preinclude \"bar.glsl\"\n", -                "\n", -                "in vec3 in_frag_color;\n", -                "\n", -                "#preinclude\"foo.glsl\"\n", -                "\n", -                "void main() {}", -            ) -            .to_string(), -            Path::new("foo.glsl"),          ); -        let Err(Error::InvalidToken { expected, found, span }) = res else { +        let Err(PreprocessingError::InvalidToken { expected, found, span }) = res else {              panic!(                  "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}"              ); @@ -494,17 +513,26 @@ mod tests          assert_eq!(expected, ' ');          assert_eq!(found, '"'); -        assert_eq!(span.line, 7); +        assert_eq!(span.line, 3);          assert_eq!(span.column, 12); -        assert_eq!(span.file, path); +        assert_eq!(span.path, SpanPath::Path(Path::new("foo.glsl").into()));      }      #[test]      fn preprocess_included_shader_with_include_works()      {          assert_eq!( -            ShaderPreprocessor { -                read_file: |path| { +            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", @@ -521,21 +549,7 @@ mod tests                          .as_bytes()                          .to_vec())                      } -                }, -                base_dir: PathBuf::new() -            } -            .preprocess( -                concat!( -                    "#version 330 core\n", -                    "\n", -                    "#preinclude \"bar.glsl\"\n", -                    "\n", -                    "in vec3 in_frag_color;\n", -                    "\n", -                    "void main() {}", -                ) -                .to_string(), -                Path::new("foo.glsl"), +                }              )              .unwrap(),              concat!( @@ -556,8 +570,17 @@ mod tests      #[test]      fn preprocess_included_shader_with_include_error_span_is_correct()      { -        let res = ShaderPreprocessor { -            read_file: |path| { +        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 " @@ -576,23 +599,9 @@ mod tests                      .to_vec())                  }              }, -            base_dir: PathBuf::new(), -        } -        .preprocess( -            concat!( -                "#version 330 core\n", -                "\n", -                "#preinclude \"bar.glsl\"\n", -                "\n", -                "in vec3 in_frag_color;\n", -                "\n", -                "void main() {}", -            ) -            .to_string(), -            Path::new("foo.glsl"),          ); -        let Err(Error::InvalidToken { expected, found, span }) = res else { +        let Err(PreprocessingError::InvalidToken { expected, found, span }) = res else {              panic!(                  "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}"              ); @@ -602,6 +611,6 @@ mod tests          assert_eq!(found, '\'');          assert_eq!(span.line, 1);          assert_eq!(span.column, 13); -        assert_eq!(span.file, Path::new("bar.glsl")); +        assert_eq!(span.path, Path::new("bar.glsl"));      }  } diff --git a/engine/src/opengl/mod.rs b/engine/src/opengl/mod.rs index 0b1bb8a..2208ac6 100644 --- a/engine/src/opengl/mod.rs +++ b/engine/src/opengl/mod.rs @@ -1,107 +1 @@ -use bitflags::bitflags; - -use crate::data_types::dimens::Dimens; -use crate::vector::Vec2; - -pub mod buffer; -pub mod shader; -pub mod texture; -pub mod vertex_array; - -mod util; - -#[cfg(feature = "debug")] -pub mod debug; - -pub fn set_viewport(position: Vec2<u32>, size: Dimens<u32>) -{ -    unsafe { -        #[allow(clippy::cast_possible_wrap)] -        gl::Viewport( -            position.x as i32, -            position.y as i32, -            size.width as i32, -            size.height as i32, -        ); -    } -} - -pub fn clear_buffers(mask: BufferClearMask) -{ -    unsafe { -        gl::Clear(mask.bits()); -    } -} - -pub fn set_polygon_mode(face: impl Into<PolygonModeFace>, mode: impl Into<PolygonMode>) -{ -    unsafe { -        gl::PolygonMode(face.into() as u32, mode.into() as u32); -    } -} - -pub fn enable(capacity: Capability) -{ -    unsafe { -        gl::Enable(capacity as u32); -    } -} - -bitflags! { -    #[derive(Debug, Clone, Copy)] -    pub struct BufferClearMask: u32 { -        const COLOR = gl::COLOR_BUFFER_BIT; -        const DEPTH = gl::DEPTH_BUFFER_BIT; -        const STENCIL = gl::STENCIL_BUFFER_BIT; -    } -} - -#[derive(Debug)] -#[repr(u32)] -pub enum Capability -{ -    DepthTest = gl::DEPTH_TEST, -    MultiSample = gl::MULTISAMPLE, -} - -#[derive(Debug)] -#[repr(u32)] -pub enum PolygonMode -{ -    Point = gl::POINT, -    Line = gl::LINE, -    Fill = gl::FILL, -} - -impl From<crate::draw_flags::PolygonMode> for PolygonMode -{ -    fn from(mode: crate::draw_flags::PolygonMode) -> Self -    { -        match mode { -            crate::draw_flags::PolygonMode::Point => Self::Point, -            crate::draw_flags::PolygonMode::Fill => Self::Fill, -            crate::draw_flags::PolygonMode::Line => Self::Line, -        } -    } -} - -#[derive(Debug)] -#[repr(u32)] -pub enum PolygonModeFace -{ -    Front = gl::FRONT, -    Back = gl::BACK, -    FrontAndBack = gl::FRONT_AND_BACK, -} - -impl From<crate::draw_flags::PolygonModeFace> for PolygonModeFace -{ -    fn from(face: crate::draw_flags::PolygonModeFace) -> Self -    { -        match face { -            crate::draw_flags::PolygonModeFace::Front => Self::Front, -            crate::draw_flags::PolygonModeFace::Back => Self::Back, -            crate::draw_flags::PolygonModeFace::FrontAndBack => Self::FrontAndBack, -        } -    } -} +pub mod glsl; diff --git a/engine/src/opengl/shader.rs b/engine/src/opengl/shader.rs deleted file mode 100644 index 070897e..0000000 --- a/engine/src/opengl/shader.rs +++ /dev/null @@ -1,240 +0,0 @@ -use std::ffi::CStr; -use std::ptr::null_mut; - -use crate::matrix::Matrix; -use crate::shader::Kind; -use crate::vector::Vec3; - -#[derive(Debug)] -pub struct Shader -{ -    shader: gl::types::GLuint, -} - -impl Shader -{ -    pub fn new(kind: Kind) -> Self -    { -        let shader = unsafe { gl::CreateShader(kind.into_gl()) }; - -        Self { shader } -    } - -    pub fn set_source(&self, source: &str) -> Result<(), Error> -    { -        if !source.is_ascii() { -            return Err(Error::SourceNotAscii); -        } - -        unsafe { -            #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] -            gl::ShaderSource( -                self.shader, -                1, -                &source.as_ptr().cast(), -                &(source.len() as gl::types::GLint), -            ); -        } - -        Ok(()) -    } - -    pub fn compile(&self) -> Result<(), Error> -    { -        unsafe { -            gl::CompileShader(self.shader); -        } - -        let mut compile_success = gl::types::GLint::default(); - -        unsafe { -            gl::GetShaderiv(self.shader, gl::COMPILE_STATUS, &mut compile_success); -        } - -        if compile_success == 0 { -            let info_log = self.get_info_log(); - -            return Err(Error::CompileFailed(info_log)); -        } - -        Ok(()) -    } - -    fn get_info_log(&self) -> String -    { -        let mut buf = vec![gl::types::GLchar::default(); 512]; - -        unsafe { -            #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] -            gl::GetShaderInfoLog( -                self.shader, -                buf.len() as gl::types::GLsizei, -                null_mut(), -                buf.as_mut_ptr(), -            ); -        } - -        let info_log = unsafe { CStr::from_ptr(buf.as_ptr()) }; - -        unsafe { String::from_utf8_unchecked(info_log.to_bytes().to_vec()) } -    } -} - -impl Drop for Shader -{ -    fn drop(&mut self) -    { -        unsafe { -            gl::DeleteShader(self.shader); -        } -    } -} - -impl Kind -{ -    fn into_gl(self) -> gl::types::GLenum -    { -        match self { -            Self::Vertex => gl::VERTEX_SHADER, -            Self::Fragment => gl::FRAGMENT_SHADER, -        } -    } -} - -/// Shader program -#[derive(Debug, PartialEq, Eq, Hash)] -pub struct Program -{ -    program: gl::types::GLuint, -} - -impl Program -{ -    pub fn new() -> Self -    { -        let program = unsafe { gl::CreateProgram() }; - -        Self { program } -    } - -    pub fn attach(&self, shader: &Shader) -    { -        unsafe { -            gl::AttachShader(self.program, shader.shader); -        } -    } - -    pub fn link(&self) -> Result<(), Error> -    { -        unsafe { -            gl::LinkProgram(self.program); -        } - -        let mut link_success = gl::types::GLint::default(); - -        unsafe { -            gl::GetProgramiv(self.program, gl::LINK_STATUS, &mut link_success); -        } - -        if link_success == 0 { -            let info_log = self.get_info_log(); - -            return Err(Error::CompileFailed(info_log)); -        } - -        Ok(()) -    } - -    pub fn activate(&self) -    { -        unsafe { -            gl::UseProgram(self.program); -        } -    } - -    pub fn set_uniform_matrix_4fv(&mut self, name: &CStr, matrix: &Matrix<f32, 4, 4>) -    { -        let uniform_location = -            unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) }; - -        unsafe { -            gl::ProgramUniformMatrix4fv( -                self.program, -                uniform_location, -                1, -                gl::FALSE, -                matrix.as_ptr(), -            ); -        } -    } - -    pub fn set_uniform_vec_3fv(&mut self, name: &CStr, vec: &Vec3<f32>) -    { -        let uniform_location = -            unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) }; - -        unsafe { -            gl::ProgramUniform3fv(self.program, uniform_location, 1, vec.as_ptr()); -        } -    } - -    pub fn set_uniform_1fv(&mut self, name: &CStr, num: f32) -    { -        let uniform_location = -            unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) }; - -        unsafe { -            gl::ProgramUniform1fv(self.program, uniform_location, 1, &num); -        } -    } - -    pub fn set_uniform_1i(&mut self, name: &CStr, num: i32) -    { -        let uniform_location = -            unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) }; - -        unsafe { -            gl::ProgramUniform1i(self.program, uniform_location, num); -        } -    } - -    fn get_info_log(&self) -> String -    { -        let mut buf = vec![gl::types::GLchar::default(); 512]; - -        unsafe { -            #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] -            gl::GetProgramInfoLog( -                self.program, -                buf.len() as gl::types::GLsizei, -                null_mut(), -                buf.as_mut_ptr(), -            ); -        } - -        let info_log = unsafe { CStr::from_ptr(buf.as_ptr()) }; - -        unsafe { String::from_utf8_unchecked(info_log.to_bytes().to_vec()) } -    } -} - -impl Drop for Program -{ -    fn drop(&mut self) -    { -        unsafe { -            gl::DeleteProgram(self.program); -        } -    } -} - -/// Shader error. -#[derive(Debug, thiserror::Error)] -pub enum Error -{ -    #[error("All characters in source are not within the ASCII range")] -    SourceNotAscii, - -    #[error("Failed to compile: {0}")] -    CompileFailed(String), -} diff --git a/engine/src/opengl/texture.rs b/engine/src/opengl/texture.rs deleted file mode 100644 index 074ade7..0000000 --- a/engine/src/opengl/texture.rs +++ /dev/null @@ -1,240 +0,0 @@ -use crate::data_types::dimens::Dimens; -use crate::texture::{Id, Properties}; - -#[derive(Debug)] -pub struct Texture -{ -    texture: gl::types::GLuint, -} - -impl Texture -{ -    pub fn new() -> Self -    { -        let mut texture = gl::types::GLuint::default(); - -        unsafe { -            gl::CreateTextures(gl::TEXTURE_2D, 1, &mut texture); -        }; - -        Self { texture } -    } - -    pub fn bind(&self) -    { -        unsafe { -            gl::BindTexture(gl::TEXTURE_2D, self.texture); -        } -    } - -    pub fn generate( -        &mut self, -        dimens: Dimens<u32>, -        data: &[u8], -        pixel_data_format: PixelDataFormat, -    ) -    { -        self.alloc_image(pixel_data_format, dimens, data); - -        unsafe { -            gl::GenerateTextureMipmap(self.texture); -        } -    } - -    pub fn apply_properties(&mut self, properties: &Properties) -    { -        self.set_wrap(properties.wrap); -        self.set_magnifying_filter(properties.magnifying_filter); -        self.set_minifying_filter(properties.minifying_filter); -    } - -    pub fn set_wrap(&mut self, wrapping: Wrapping) -    { -        let wrapping_gl = wrapping.to_gl(); - -        #[allow(clippy::cast_possible_wrap)] -        unsafe { -            gl::TextureParameteri(self.texture, gl::TEXTURE_WRAP_S, wrapping_gl as i32); -            gl::TextureParameteri(self.texture, gl::TEXTURE_WRAP_T, wrapping_gl as i32); -        } -    } - -    pub fn set_magnifying_filter(&mut self, filtering: Filtering) -    { -        let filtering_gl = filtering.to_gl(); - -        #[allow(clippy::cast_possible_wrap)] -        unsafe { -            gl::TextureParameteri( -                self.texture, -                gl::TEXTURE_MAG_FILTER, -                filtering_gl as i32, -            ); -        } -    } - -    pub fn set_minifying_filter(&mut self, filtering: Filtering) -    { -        let filtering_gl = filtering.to_gl(); - -        #[allow(clippy::cast_possible_wrap)] -        unsafe { -            gl::TextureParameteri( -                self.texture, -                gl::TEXTURE_MIN_FILTER, -                filtering_gl as i32, -            ); -        } -    } - -    fn alloc_image( -        &mut self, -        pixel_data_format: PixelDataFormat, -        dimens: Dimens<u32>, -        data: &[u8], -    ) -    { -        unsafe { -            #[allow(clippy::cast_possible_wrap)] -            gl::TextureStorage2D( -                self.texture, -                1, -                pixel_data_format.to_sized_internal_format(), -                dimens.width as i32, -                dimens.height as i32, -            ); - -            #[allow(clippy::cast_possible_wrap)] -            gl::TextureSubImage2D( -                self.texture, -                0, -                0, -                0, -                dimens.width as i32, -                dimens.height as i32, -                pixel_data_format.to_format(), -                gl::UNSIGNED_BYTE, -                data.as_ptr().cast(), -            ); -        } -    } -} - -impl Drop for Texture -{ -    fn drop(&mut self) -    { -        unsafe { -            gl::DeleteTextures(1, &self.texture); -        } -    } -} - -/// Texture wrapping. -#[derive(Debug, Clone, Copy)] -pub enum Wrapping -{ -    Repeat, -    MirroredRepeat, -    ClampToEdge, -    ClampToBorder, -} - -impl Wrapping -{ -    fn to_gl(self) -> gl::types::GLenum -    { -        match self { -            Self::Repeat => gl::REPEAT, -            Self::MirroredRepeat => gl::MIRRORED_REPEAT, -            Self::ClampToEdge => gl::CLAMP_TO_EDGE, -            Self::ClampToBorder => gl::CLAMP_TO_BORDER, -        } -    } -} - -#[derive(Debug, Clone, Copy)] -pub enum Filtering -{ -    Nearest, -    Linear, -} - -impl Filtering -{ -    fn to_gl(self) -> gl::types::GLenum -    { -        match self { -            Self::Linear => gl::LINEAR, -            Self::Nearest => gl::NEAREST, -        } -    } -} - -/// Texture pixel data format. -#[derive(Debug, Clone, Copy)] -pub enum PixelDataFormat -{ -    Rgb8, -    Rgba8, -} - -impl PixelDataFormat -{ -    fn to_sized_internal_format(self) -> gl::types::GLenum -    { -        match self { -            Self::Rgb8 => gl::RGB8, -            Self::Rgba8 => gl::RGBA8, -        } -    } - -    fn to_format(self) -> gl::types::GLenum -    { -        match self { -            Self::Rgb8 => gl::RGB, -            Self::Rgba8 => gl::RGBA, -        } -    } -} - -pub fn set_active_texture_unit(texture_unit: TextureUnit) -{ -    unsafe { -        gl::ActiveTexture(texture_unit.into_gl()); -    } -} - -macro_rules! texture_unit_enum { -    (cnt=$cnt: literal) => { -        seq_macro::seq!(N in 0..$cnt { -            #[derive(Debug, Clone, Copy)] -            pub enum TextureUnit { -                #( -                    No~N, -                )* -            } - -            impl TextureUnit { -                fn into_gl(self) -> gl::types::GLenum { -                    match self { -                        #( -                            Self::No~N => gl::TEXTURE~N, -                        )* -                    } -                } - -                pub fn from_texture_id(texture_id: Id) -> Option<Self> { -                    match texture_id.into_inner() { -                        #( -                            N => Some(Self::No~N), -                        )* -                        _ => None -                    } -                } -            } -        }); -    }; -} - -texture_unit_enum!(cnt = 31); diff --git a/engine/src/opengl/util.rs b/engine/src/opengl/util.rs deleted file mode 100644 index e60778f..0000000 --- a/engine/src/opengl/util.rs +++ /dev/null @@ -1,30 +0,0 @@ -// May only be used when certain crate features are enabled -#![allow(unused_macros, unused_imports)] - -macro_rules! gl_enum { -    ( -        $visibility: vis enum $name: ident -        {$( -            $variant: ident = gl::$gl_enum: ident, -        )+} -    ) => { -        #[derive(Debug, Clone, Copy)] -        #[repr(u32)] -        $visibility enum $name -        {$( -            $variant = gl::$gl_enum, -        )+} - -        impl $name { -            fn from_gl(num: gl::types::GLenum) -> Option<Self> -            { -                match num { -                    $(gl::$gl_enum => Some(Self::$variant),)+ -                    _ => None -                } -            } -        } -    }; -} - -pub(crate) use gl_enum; diff --git a/engine/src/opengl/vertex_array.rs b/engine/src/opengl/vertex_array.rs deleted file mode 100644 index da5d91e..0000000 --- a/engine/src/opengl/vertex_array.rs +++ /dev/null @@ -1,183 +0,0 @@ -use std::mem::size_of; - -use crate::opengl::buffer::Buffer; -use crate::vertex::Vertex; - -#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] -const VERTEX_STRIDE: i32 = size_of::<Vertex>() as i32; - -#[derive(Debug)] -pub struct VertexArray -{ -    array: gl::types::GLuint, -} - -impl VertexArray -{ -    pub fn new() -> Self -    { -        let mut array = 0; - -        unsafe { -            gl::CreateVertexArrays(1, &mut array); -        } - -        Self { array } -    } - -    /// Draws the currently bound vertex array. -    pub fn draw_arrays(primitive_kind: PrimitiveKind, start_index: u32, cnt: u32) -    { -        unsafe { -            #[allow(clippy::cast_possible_wrap)] -            gl::DrawArrays( -                primitive_kind.into_gl(), -                start_index as gl::types::GLint, -                cnt as gl::types::GLsizei, -            ); -        } -    } - -    /// Draws the currently bound vertex array. -    pub fn draw_elements(primitive_kind: PrimitiveKind, offset: u32, cnt: u32) -    { -        unsafe { -            #[allow(clippy::cast_possible_wrap)] -            gl::DrawElements( -                primitive_kind.into_gl(), -                cnt as gl::types::GLsizei, -                gl::UNSIGNED_INT, -                (offset as gl::types::GLint) as *const _, -            ); -        } -    } - -    pub fn bind_element_buffer(&mut self, element_buffer: &Buffer<u32>) -    { -        unsafe { -            gl::VertexArrayElementBuffer(self.array, element_buffer.object()); -        } -    } - -    pub fn bind_vertex_buffer( -        &mut self, -        binding_index: u32, -        vertex_buffer: &Buffer<Vertex>, -        offset: isize, -    ) -    { -        unsafe { -            gl::VertexArrayVertexBuffer( -                self.array, -                binding_index, -                vertex_buffer.object(), -                offset, -                VERTEX_STRIDE, -            ); -        } -    } - -    pub fn enable_attrib(&mut self, attrib_index: u32) -    { -        unsafe { -            gl::EnableVertexArrayAttrib(self.array, attrib_index as gl::types::GLuint); -        } -    } - -    pub fn set_attrib_format( -        &mut self, -        attrib_index: u32, -        data_type: DataType, -        normalized: bool, -        offset: u32, -    ) -    { -        unsafe { -            #[allow(clippy::cast_possible_wrap)] -            gl::VertexArrayAttribFormat( -                self.array, -                attrib_index, -                data_type.size() as gl::types::GLint, -                data_type as u32, -                if normalized { gl::TRUE } else { gl::FALSE }, -                offset, -            ); -        } -    } - -    /// Associate a vertex attribute and a vertex buffer binding. -    pub fn set_attrib_vertex_buf_binding( -        &mut self, -        attrib_index: u32, -        vertex_buf_binding_index: u32, -    ) -    { -        unsafe { -            gl::VertexArrayAttribBinding( -                self.array, -                attrib_index, -                vertex_buf_binding_index, -            ); -        } -    } - -    pub fn bind(&self) -    { -        unsafe { gl::BindVertexArray(self.array) } -    } - -    /// Does a weak clone of this vertex array. The vertex array itself is NOT copied in -    /// any way this function only copies the internal vertex array ID. -    /// -    /// # Safety -    /// The returned `VertexArray` must not be dropped if another `VertexArray` -    /// referencing the same vertex array ID is used later. -    pub unsafe fn clone_unsafe(&self) -> Self -    { -        Self { array: self.array } -    } -} - -impl Drop for VertexArray -{ -    fn drop(&mut self) -    { -        unsafe { -            gl::DeleteVertexArrays(1, &self.array); -        } -    } -} - -#[derive(Debug)] -pub enum PrimitiveKind -{ -    Triangles, -} - -impl PrimitiveKind -{ -    fn into_gl(self) -> gl::types::GLenum -    { -        match self { -            Self::Triangles => gl::TRIANGLES, -        } -    } -} - -#[derive(Debug, Clone, Copy)] -#[repr(u32)] -pub enum DataType -{ -    Float = gl::FLOAT, -} - -impl DataType -{ -    pub fn size(self) -> u32 -    { -        #[allow(clippy::cast_possible_truncation)] -        match self { -            Self::Float => size_of::<gl::types::GLfloat>() as u32, -        } -    } -} diff --git a/engine/src/performance.rs b/engine/src/performance.rs deleted file mode 100644 index ffc5c27..0000000 --- a/engine/src/performance.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::time::Instant; - -use ecs::component::local::Local; -use ecs::system::{Into, System}; -use ecs::Component; - -use crate::event::PostPresent as PostPresentEvent; - -#[derive(Debug, Default)] -#[non_exhaustive] -pub struct Extension {} - -impl ecs::extension::Extension for Extension -{ -    fn collect(self, mut collector: ecs::extension::Collector<'_>) -    { -        collector.add_system( -            PostPresentEvent, -            log_perf.into_system().initialize((State::default(),)), -        ); -    } -} - -#[cfg(feature = "debug")] -macro_rules! log_perf { -    ($($tt: tt)*) => { -        tracing::info!($($tt)*); -    }; -} - -#[cfg(not(feature = "debug"))] -macro_rules! log_perf { -    ($($tt: tt)*) => { -        println!($($tt)*); -    }; -} - -fn log_perf(mut state: Local<State>) -{ -    let Some(last_time) = state.last_time else { -        state.last_time = Some(Instant::now()); -        return; -    }; - -    let time_now = Instant::now(); - -    state.last_time = Some(time_now); - -    log_perf!( -        "Frame time: {}us", -        time_now.duration_since(last_time).as_micros() -    ); -} - -#[derive(Debug, Default, Component)] -struct State -{ -    last_time: Option<Instant>, -} diff --git a/engine/src/projection.rs b/engine/src/projection.rs index aa84a9f..115ca39 100644 --- a/engine/src/projection.rs +++ b/engine/src/projection.rs @@ -1,10 +1,14 @@ +use crate::data_types::dimens::Dimens3;  use crate::matrix::Matrix; +use crate::builder; +use crate::vector::Vec3;  #[derive(Debug)]  #[non_exhaustive]  pub enum Projection  {      Perspective(Perspective), +    Orthographic(Orthographic),  }  /// Perspective projection parameters. @@ -16,6 +20,29 @@ pub struct Perspective      pub near: f32,  } +impl Perspective +{ +    /// Creates a perspective projection matrix using right-handed coordinates. +    #[inline] +    pub fn to_matrix_rh(&self, aspect: f32, clip_volume: ClipVolume) +        -> Matrix<f32, 4, 4> +    { +        let mut out = Matrix::new(); + +        match clip_volume { +            ClipVolume::NegOneToOne => { +                out.set_cell(0, 0, (1.0 / (self.fov_radians / 2.0).tan()) / aspect); +                out.set_cell(1, 1, 1.0 / (self.fov_radians / 2.0).tan()); +                out.set_cell(2, 2, (self.near + self.far) / (self.near - self.far)); +                out.set_cell(2, 3, (2.0 * self.near * self.far) / (self.near - self.far)); +                out.set_cell(3, 2, -1.0); +            } +        } + +        out +    } +} +  impl Default for Perspective  {      fn default() -> Self @@ -28,30 +55,83 @@ impl Default for Perspective      }  } -pub(crate) fn new_perspective_matrix( -    perspective: &Perspective, -    aspect: f32, -) -> Matrix<f32, 4, 4> +builder! { +#[builder(name = OrthographicBuilder, derives=(Debug, Clone))] +#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[non_exhaustive] +pub struct Orthographic  { -    let mut out = Matrix::new(); +    pub size: Dimens3<f32>, +} +} -    out.set_cell(0, 0, (1.0 / (perspective.fov_radians / 2.0).tan()) / aspect); +impl Orthographic +{ +    pub fn builder() -> OrthographicBuilder +    { +        OrthographicBuilder::default() +    } -    out.set_cell(1, 1, 1.0 / (perspective.fov_radians / 2.0).tan()); +    /// Creates a orthographic projection matrix using right-handed coordinates. +    pub fn to_matrix_rh( +        &self, +        center_pos: &Vec3<f32>, +        clip_volume: ClipVolume, +    ) -> Matrix<f32, 4, 4> +    { +        let mut result = Matrix::<f32, 4, 4>::new(); -    out.set_cell( -        2, -        2, -        (perspective.near + perspective.far) / (perspective.near - perspective.far), -    ); +        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); -    out.set_cell( -        2, -        3, -        (2.0 * perspective.near * perspective.far) / (perspective.near - perspective.far), -    ); +        match clip_volume { +            ClipVolume::NegOneToOne => { +                result.set_cell(0, 0, 2.0 / (right - left)); +                result.set_cell(1, 1, 2.0 / (top - bottom)); +                result.set_cell(2, 2, -2.0 / (far - near)); +                result.set_cell(0, 3, -(right + left) / (right - left)); +                result.set_cell(1, 3, -(top + bottom) / (top - bottom)); +                result.set_cell(2, 3, -(far + near) / (far - near)); +                result.set_cell(3, 3, 1.0); +            } +        } -    out.set_cell(3, 2, -1.0); +        result +    } +} -    out +impl Default for Orthographic +{ +    fn default() -> Self +    { +        Self { +            size: Dimens3 { +                width: 10.0, +                height: 7.0, +                depth: 10.0, +            }, +        } +    } +} + +impl Default for OrthographicBuilder +{ +    fn default() -> Self +    { +        let orthographic = Orthographic::default(); + +        OrthographicBuilder { size: orthographic.size } +    } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum ClipVolume +{ +    /// -1 to +1. This is the OpenGL clip volume definition. +    NegOneToOne,  } diff --git a/engine/src/renderer.rs b/engine/src/renderer.rs index 2544919..6d25f6b 100644 --- a/engine/src/renderer.rs +++ b/engine/src/renderer.rs @@ -1 +1,80 @@ +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{Phase, POST_UPDATE as POST_UPDATE_PHASE}; +use ecs::{declare_entity, Component}; + +use crate::builder; +  pub mod opengl; + +declare_entity!( +    pub RENDER_PHASE, +    ( +        Phase, +        Pair::builder() +            .relation::<ChildOf>() +            .target_id(*POST_UPDATE_PHASE) +            .build() +    ) +); + +builder! { +/// Window graphics properties. +#[builder(name=GraphicsPropertiesBuilder, derives=(Debug, Clone))] +#[derive(Debug, Clone, Component)] +#[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, +        } +    } +} diff --git a/engine/src/renderer/opengl.rs b/engine/src/renderer/opengl.rs index a353c6a..fb7dfbe 100644 --- a/engine/src/renderer/opengl.rs +++ b/engine/src/renderer/opengl.rs @@ -1,464 +1,1214 @@  //! OpenGL renderer. +use std::any::type_name;  use std::collections::HashMap; -use std::ffi::{c_void, CString}; -use std::ops::Deref; -use std::process::abort; +use std::ffi::CString; +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; +use std::path::Path;  use ecs::actions::Actions; -use ecs::component::local::Local; -use ecs::query::options::{Not, With}; +use ecs::component::Handle as ComponentHandle; +use ecs::entity::obtainer::Obtainer as EntityObtainer; +use ecs::event::component::{Changed, Removed}; +use ecs::pair::{ChildOf, Pair, Wildcard}; +use ecs::phase::Phase; +use ecs::query::term::Without;  use ecs::sole::Single; -use ecs::system::{Into as _, System}; -use ecs::{Component, Query}; +use ecs::system::observer::Observe; +use ecs::{declare_entity, Component, Query}; +use glutin::display::GetGlDisplay; +use glutin::prelude::GlDisplay; +use glutin::surface::GlSurface; +use opengl_bindings::debug::{ +    set_debug_message_callback, +    set_debug_message_control, +    MessageIdsAction, +    MessageSeverity, +    MessageSource, +    MessageType, +    SetDebugMessageControlError as GlSetDebugMessageControlError, +}; +use opengl_bindings::misc::{ +    clear_buffers, +    enable, +    set_enabled, +    BufferClearMask, +    Capability, +    SetViewportError as GlSetViewportError, +}; +use opengl_bindings::shader::{ +    Error as GlShaderError, +    Kind as ShaderKind, +    Program as GlShaderProgram, +    Shader as GlShader, +}; +use opengl_bindings::texture::{ +    Filtering as GlTextureFiltering, +    GenerateError as GlTextureGenerateError, +    PixelDataFormat as GlTexturePixelDataFormat, +    Texture as GlTexture, +    Wrapping as GlTextureWrapping, +}; +use opengl_bindings::vertex_array::{ +    DrawError as GlDrawError, +    PrimitiveKind, +    VertexArray, +}; +use opengl_bindings::{ContextWithFns, CurrentContextWithFns}; +use safer_ffi::layout::ReprC; +use crate::asset::{Assets, Id as AssetId};  use crate::camera::{Active as ActiveCamera, Camera};  use crate::color::Color;  use crate::data_types::dimens::Dimens;  use crate::draw_flags::{DrawFlags, NoDraw, PolygonModeConfig}; -use crate::event::{Present as PresentEvent, Start as StartEvent}; +use crate::image::{ColorType as ImageColorType, Image};  use crate::lighting::{DirectionalLight, GlobalLight, PointLight};  use crate::material::{Flags as MaterialFlags, Material};  use crate::matrix::Matrix; -use crate::mesh::Mesh; -use crate::opengl::buffer::{Buffer, Usage as BufferUsage}; -#[cfg(feature = "debug")] -use crate::opengl::debug::{MessageSeverity, MessageSource, MessageType}; -use crate::opengl::shader::{ -    Error as GlShaderError, -    Program as GlShaderProgram, -    Shader as GlShader, +use crate::model::Model; +use crate::opengl::glsl::{ +    preprocess as glsl_preprocess, +    PreprocessingError as GlslPreprocessingError,  }; -use crate::opengl::texture::{ -    set_active_texture_unit, -    Texture as GlTexture, -    TextureUnit, +use crate::projection::{ClipVolume, Projection}; +use crate::renderer::opengl::glutin_compat::{ +    DisplayBuilder, +    Error as GlutinCompatError,  }; -use crate::opengl::vertex_array::{ -    DataType as VertexArrayDataType, -    PrimitiveKind, -    VertexArray, +use crate::renderer::opengl::graphics_mesh::GraphicsMesh; +use crate::renderer::{GraphicsProperties, RENDER_PHASE}; +use crate::texture::{ +    Filtering as TextureFiltering, +    Properties as TextureProperties, +    Wrapping as TextureWrapping,  }; -use crate::opengl::{clear_buffers, enable, BufferClearMask, Capability}; -use crate::projection::{new_perspective_matrix, Projection}; -use crate::shader::Program as ShaderProgram; -use crate::texture::{Id as TextureId, Texture}; -use crate::transform::{Position, Scale}; -use crate::util::NeverDrop; +use crate::transform::{Scale, WorldPosition}; +use crate::util::MapVec;  use crate::vector::{Vec2, Vec3}; -use crate::vertex::{AttributeComponentType, Vertex}; -use crate::window::Window; - -type RenderableEntity = ( -    Mesh, -    ShaderProgram, -    Material, -    Option<MaterialFlags>, -    Option<Position>, -    Option<Scale>, -    Option<DrawFlags>, -    Option<GlObjects>, +use crate::windowing::window::{ +    Closed as WindowClosed, +    CreationAttributes as WindowCreationAttributes, +    CreationReady, +    Window, +}; +use crate::windowing::Context as WindowingContext; + +mod glutin_compat; +mod graphics_mesh; +mod vertex; + +const AMBIENT_MAP_TEXTURE_UNIT: u32 = 0; +const DIFFUSE_MAP_TEXTURE_UNIT: u32 = 1; +const SPECULAR_MAP_TEXTURE_UNIT: u32 = 2; + +type RenderableEntity<'a> = ( +    &'a Model, +    Option<&'a MaterialFlags>, +    Option<&'a WorldPosition>, +    Option<&'a Scale>, +    Option<&'a DrawFlags>, +    &'a [Pair<DataInGraphicsContext, Wildcard>], +); + +declare_entity!( +    pub POST_RENDER_PHASE, +    (Phase, Pair::builder().relation::<ChildOf>().target_id(*RENDER_PHASE).build())  ); +#[derive(Debug, Component)] +struct WithGraphicsContext; + +#[derive(Debug, Component)] +struct WindowGlConfig +{ +    gl_config: glutin::config::Config, +} + +#[derive(Debug, Component)] +struct WindowGraphicsSurface +{ +    surface: glutin::surface::Surface<glutin::surface::WindowSurface>, +} + +#[derive(Component)] +struct GraphicsContext +{ +    context: ContextWithFns, +    shader_program: Option<GlShaderProgram>, +    textures_objs: HashMap<AssetId, GlTexture>, +    default_1x1_texture_obj: Option<GlTexture>, +    graphics_mesh_store: GraphicsMeshStore, +} + +#[derive(Debug, Default)] +struct GraphicsMeshStore +{ +    graphics_meshes: MapVec<GraphicsMeshId, GraphicsMesh>, +    next_id: GraphicsMeshId, +} + +impl GraphicsMeshStore +{ +    fn insert(&mut self, graphics_mesh: GraphicsMesh) -> GraphicsMeshId +    { +        let id = self.next_id; + +        self.graphics_meshes.insert(id, graphics_mesh); + +        self.next_id.inner += 1; + +        id +    } +} + +#[derive(Debug, Component)] +struct DataInGraphicsContext +{ +    graphics_mesh_id: GraphicsMeshId, +} +  #[derive(Debug, Default)] +#[non_exhaustive]  pub struct Extension {}  impl ecs::extension::Extension for Extension  {      fn collect(self, mut collector: ecs::extension::Collector<'_>)      { -        collector.add_system(StartEvent, initialize); +        collector.add_declared_entity(&RENDER_PHASE); +        collector.add_declared_entity(&POST_RENDER_PHASE); -        collector.add_system( -            PresentEvent, -            render -                .into_system() -                .initialize((GlobalGlObjects::default(),)), -        ); +        collector.add_system(*RENDER_PHASE, render); + +        collector.add_system(*POST_RENDER_PHASE, prepare_windows); +        collector.add_system(*POST_RENDER_PHASE, init_window_graphics); + +        collector.add_observer(handle_model_removed); + +        collector.add_observer(handle_window_changed); +        collector.add_observer(handle_window_removed); +    } +} + +#[tracing::instrument(skip_all)] +fn handle_model_removed(observe: Observe<Pair<Removed, Model>>, mut actions: Actions) +{ +    for evt_match in &observe { +        let ent_id = evt_match.id(); + +        tracing::debug!(entity_id=%ent_id, "Cleaning up after model"); + +        let ent = evt_match.get_ent_infallible(); + +        for data_in_graphics_ctx_pair in +            ent.get_wildcard_pair_matches::<DataInGraphicsContext, Wildcard>() +        { +            actions.remove_components(ent_id, [data_in_graphics_ctx_pair.id()]); + +            let Some(graphics_context_ent) = data_in_graphics_ctx_pair.get_target_ent() +            else { +                tracing::trace!( +                    concat!( +                        "Graphics context referenced by pair ({}, {}) does not exist. ", +                        "Skipping cleanup of this model" +                    ), +                    type_name::<DataInGraphicsContext>(), +                    data_in_graphics_ctx_pair.id().target_entity() +                ); + +                continue; +            }; + +            let Some(data_in_graphics_ctx) = +                data_in_graphics_ctx_pair.get_data_as_relation() +            else { +                unreachable!(); +            }; + +            let Some(mut graphics_context) = +                graphics_context_ent.get_mut::<GraphicsContext>() +            else { +                tracing::trace!( +                    "Graphics context entity {} does not have a {} component", +                    graphics_context_ent.uid(), +                    type_name::<GraphicsContext>() +                ); +                continue; +            }; + +            graphics_context +                .graphics_mesh_store +                .graphics_meshes +                .remove(data_in_graphics_ctx.graphics_mesh_id); +        }      }  } -fn initialize(window: Single<Window>) +#[tracing::instrument(skip_all)] +fn handle_window_changed( +    observe: Observe<Pair<Changed, Window>>, +    entity_obtainer: EntityObtainer, +)  { -    window -        .make_context_current() -        .expect("Failed to make window context current"); +    for evt_match in &observe { +        let window_ent = evt_match.get_ent_infallible(); + +        tracing::trace!( +            new_state = ?evt_match.get_changed_comp(), +            "Handling window change" +        ); + +        let Some(window_graphics_surface) = window_ent.get::<WindowGraphicsSurface>() +        else { +            continue; +        }; + +        let Some(graphics_context_ent_id) = window_ent +            .get_matching_components( +                Pair::builder() +                    .relation::<WithGraphicsContext>() +                    .target_id(Wildcard::uid()) +                    .build() +                    .id(), +            ) +            .next() +            .map(|comp_ref| comp_ref.id().target_entity()) +        else { +            continue; +        }; + +        let Some(graphics_context_ent) = +            entity_obtainer.get_entity(graphics_context_ent_id) +        else { +            tracing::error!("Graphics context entity does not exist"); +            continue; +        }; -    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}", +        let Some(graphics_context) = graphics_context_ent.get::<GraphicsContext>() else { +            tracing::error!( +                "Graphics context entity does not have a GraphicsContext component"              ); +            continue; +        }; + +        let Ok(current_graphics_context) = graphics_context +            .context +            .make_current(&window_graphics_surface.surface) +        else { +            tracing::error!("Failed to make graphics context current"); +            continue; +        }; -            abort(); +        if let Err(err) = set_viewport( +            ¤t_graphics_context, +            Vec2::default(), +            evt_match.get_changed_comp().inner_size(), +        ) { +            tracing::error!("Failed to set viewport: {err}");          } -    }); +    } +} -    #[cfg(feature = "debug")] -    initialize_debug(); +#[tracing::instrument(skip_all)] +fn handle_window_removed(observe: Observe<Pair<Removed, Window>>, mut actions: Actions) +{ +    for evt_match in &observe { +        let window_ent_id = evt_match.id(); -    let window_size = window.size().expect("Failed to get window size"); +        let window_ent = evt_match.get_ent_infallible(); -    set_viewport(Vec2 { x: 0, y: 0 }, window_size); +        tracing::debug!( +            entity_id = %window_ent_id, +            title = %evt_match.get_removed_comp().title, +            "Handling removal of window" +        ); -    window.set_framebuffer_size_callback(|new_window_size| { -        set_viewport(Vec2::ZERO, new_window_size); -    }); +        actions.remove_comps::<(WindowGraphicsSurface, WindowGlConfig)>(window_ent_id); + +        let Some(with_graphics_ctx_pair_handle) = +            window_ent.get_first_wildcard_pair_match::<WithGraphicsContext, Wildcard>() +        else { +            tracing::warn!("Window entity is missing a (WithGraphicsContext, *) pair"); +            continue; +        }; -    enable(Capability::DepthTest); -    enable(Capability::MultiSample); +        let graphics_context_ent_id = with_graphics_ctx_pair_handle.id().target_entity(); + +        actions.remove_comps::<(GraphicsContext,)>(graphics_context_ent_id); + +        actions.remove_components(window_ent_id, [with_graphics_ctx_pair_handle.id()]); +    }  } -#[allow(clippy::too_many_arguments)] -fn render( -    query: Query<RenderableEntity, Not<With<NoDraw>>>, -    point_light_query: Query<(PointLight,)>, -    directional_lights: Query<(DirectionalLight,)>, -    camera_query: Query<(Camera, Position, ActiveCamera)>, -    window: Single<Window>, -    global_light: Single<GlobalLight>, -    mut gl_objects: Local<GlobalGlObjects>, +#[derive(Debug, Component)] +struct SetupFailed; + +// fn on_window_creation_attrs_added( +//     observe: Observe<Pair<Added, WindowCreationAttributes>>, +//     windowing: Single<Windowing>, +//     window_store: Single<WindowStore>, +//     mut actions: Actions, +// ) +// { +//     for evt_match in &observe { +//         let Some(ent) = evt_match.get_entity() else { +//             unreachable!(); +//         }; +// +//         if ent.has_component(WindowGlConfig::id()) || +// ent.has_component(WindowClosed::id()) || ent.has_component() {}     } +// } + +fn prepare_windows( +    window_query: Query< +        ( +            Option<&Window>, +            &mut WindowCreationAttributes, +            Option<&GraphicsProperties>, +        ), +        ( +            Without<CreationReady>, +            Without<WindowGlConfig>, +            Without<WindowClosed>, +            Without<SetupFailed>, +        ), +    >, +    windowing_context: Single<WindowingContext>,      mut actions: Actions,  )  { -    let Some((camera, camera_pos, _)) = camera_query.iter().next() else { -        #[cfg(feature = "debug")] -        tracing::warn!("No current camera. Nothing will be rendered"); +    let Some(display_handle) = windowing_context.display_handle() else {          return;      }; -    let point_lights = point_light_query -        .iter() -        .map(|(point_light,)| point_light) -        .collect::<Vec<_>>(); +    for (window_ent_id, (window, mut window_creation_attrs, graphics_props)) in +        window_query.iter_with_euids() +    { +        tracing::debug!("Preparing window entity {window_ent_id} for use in rendering"); -    let directional_lights = directional_lights.iter().collect::<Vec<_>>(); +        let mut glutin_config_template_builder = +            glutin::config::ConfigTemplateBuilder::new(); -    let GlobalGlObjects { -        shader_programs: gl_shader_programs, -        textures: gl_textures, -    } = &mut *gl_objects; +        let graphics_props = match graphics_props.as_ref() { +            Some(graphics_props) => &*graphics_props, +            None => { +                actions.add_components(window_ent_id, (GraphicsProperties::default(),)); -    clear_buffers(BufferClearMask::COLOR | BufferClearMask::DEPTH); +                &GraphicsProperties::default() +            } +        }; -    for ( -        entity_index, -        ( -            mesh, -            shader_program, -            material, -            material_flags, -            position, -            scale, -            draw_flags, -            gl_objects, -        ), -    ) in query.iter().enumerate() -    { -        let material_flags = material_flags -            .map(|material_flags| material_flags.clone()) -            .unwrap_or_default(); +        if let Some(multisampling_sample_cnt) = graphics_props.multisampling_sample_cnt { +            glutin_config_template_builder = glutin_config_template_builder +                .with_multisampling(multisampling_sample_cnt); +        } -        let shader_program = gl_shader_programs -            .entry(shader_program.u64_hash()) -            .or_insert_with(|| create_gl_shader_program(&shader_program).unwrap()); +        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}"); +                actions.add_components(window_ent_id, (SetupFailed,)); +                continue; +            } +        }; -        let new_gl_objects; +        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}"); +                actions.add_components(window_ent_id, (SetupFailed,)); +                continue; +            } +        }; -        let gl_objects = if let Some(gl_objects) = gl_objects.as_deref() { -            gl_objects -        } else { -            // TODO: Account for when meshes are changed -            let gl_objects = GlObjects::new(&mesh); +        *window_creation_attrs = new_window_creation_attrs; + +        // let gl_config_template = glutin_config_template_builder.build(); +        // +        // let display = match glutin_winit_compat::create_display( +        //     unsafe { engine_display.as_display_handle() }, +        //     glutin_winit_compat::ApiPreference::default(), +        //     None, +        // ) { +        //     Ok(gl_display) => gl_display, +        //     Err(err) => { +        //         tracing::error!("Failed to create graphics platform display: {err}"); +        //         continue; +        //     } +        // }; +        // +        // let mut gl_configs = match unsafe { display.find_configs(gl_config_template) } +        // {     Ok(gl_configs) => gl_configs, +        //     Err(err) => { +        //         tracing::error!("Failed to find GL configs: {err:?}"); +        //         continue; +        //     } +        // }; +        // +        // let Some(first_gl_config) = gl_configs.next() else { +        //     tracing::error!("No matching GL configuration exists"); +        //     continue; +        // }; +        // +        // *window_creation_attrs = finalize_window_creation_attrs( +        //     window_creation_attrs.clone(), +        //     &first_gl_config, +        // ); + +        actions.add_components(window_ent_id, (WindowGlConfig { gl_config },)); + +        if window.is_none() { +            actions.add_components(window_ent_id, (CreationReady,)); +        } +    } +} -            new_gl_objects = Some(gl_objects.clone()); +#[tracing::instrument(skip_all)] +fn init_window_graphics( +    window_query: Query< +        (&Window, &WindowGlConfig, &GraphicsProperties), +        (Without<WindowGraphicsSurface>, Without<SetupFailed>), +    >, +    mut actions: Actions, +    windowing_context: Single<WindowingContext>, +) +{ +    for (window_ent_id, (window, window_gl_config, graphics_props)) in +        window_query.iter_with_euids() +    { +        tracing::info!("Initializing graphics for window {window_ent_id}"); + +        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" +                    ); +                    actions.add_components(window_ent_id, (SetupFailed,)); +                    continue; +                } +                Err(err) => { +                    tracing::error!("Failed to get window handle: {err}"); +                    actions.add_components(window_ent_id, (SetupFailed,)); +                    continue; +                } +            }; -            actions.add_components( -                query.get_entity_uid(entity_index).unwrap(), -                (gl_objects,), +        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; +        }; -            &*new_gl_objects.unwrap() +        let 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(surface) => surface, +            Err(err) => { +                tracing::error!("Failed to create window surface: {err}"); +                continue; +            }          }; -        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"), -        ); +        let context = match unsafe { +            display.create_context( +                &window_gl_config.gl_config, +                &glutin::context::ContextAttributesBuilder::new() +                    .with_debug(graphics_props.debug) +                    .build(Some(window_handle.as_raw())), +            ) +        } { +            Ok(context) => context, +            Err(err) => { +                tracing::error!("Failed to create graphics context: {err}"); +                continue; +            } +        }; + +        let context = match ContextWithFns::new(context, &surface) { +            Ok(context) => context, +            Err(err) => { +                tracing::error!("Failed to create graphics context: {err}"); +                continue; +            } +        }; + +        let Ok(current_graphics_context) = context.make_current(&surface) else { +            tracing::error!("Failed to make graphics context current"); +            continue; +        }; + +        if let Err(err) = set_viewport( +            ¤t_graphics_context, +            Vec2 { x: 0, y: 0 }, +            window.inner_size(), +        ) { +            tracing::error!("Failed to set viewport: {err}"); +        } -        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, +        set_enabled( +            ¤t_graphics_context, +            Capability::DepthTest, +            graphics_props.depth_test,          ); -        for texture in &material.textures { -            let gl_texture = gl_textures -                .entry(texture.id()) -                .or_insert_with(|| create_gl_texture(texture)); +        set_enabled( +            ¤t_graphics_context, +            Capability::MultiSample, +            graphics_props.multisampling_sample_cnt.is_some(), +        ); -            let texture_unit = -                TextureUnit::from_texture_id(texture.id()).unwrap_or_else(|| { -                    panic!("Texture id {} is a invalid texture unit", texture.id()); -                }); +        if graphics_props.debug { +            enable(¤t_graphics_context, Capability::DebugOutput); +            enable( +                ¤t_graphics_context, +                Capability::DebugOutputSynchronous, +            ); -            set_active_texture_unit(texture_unit); +            set_debug_message_callback( +                ¤t_graphics_context, +                opengl_debug_message_cb, +            ); -            gl_texture.bind(); +            match set_debug_message_control( +                ¤t_graphics_context, +                None, +                None, +                None, +                &[], +                MessageIdsAction::Disable, +            ) { +                Ok(()) => {} +                Err(GlSetDebugMessageControlError::TooManyIds { +                    id_cnt: _, +                    max_id_cnt: _, +                }) => { +                    unreachable!() // No ids are given +                } +            }          } -        shader_program.activate(); +        let graphics_context_ent_id = actions.spawn((GraphicsContext { +            context, +            shader_program: None, +            textures_objs: HashMap::new(), +            default_1x1_texture_obj: None, +            graphics_mesh_store: GraphicsMeshStore::default(), +        },)); + +        actions.add_components( +            window_ent_id, +            ( +                WindowGraphicsSurface { surface }, +                Pair::builder() +                    .relation::<WithGraphicsContext>() +                    .target_id(graphics_context_ent_id) +                    .build(), +            ), +        ); +    } +} + +#[tracing::instrument(skip_all)] +#[allow(clippy::too_many_arguments)] +fn render( +    query: Query<RenderableEntity<'_>, (Without<NoDraw>,)>, +    point_light_query: Query<(&PointLight, &WorldPosition)>, +    directional_lights: Query<(&DirectionalLight,)>, +    camera_query: Query<(&Camera, &WorldPosition, &ActiveCamera)>, +    window_query: Query<( +        &Window, +        &WindowGraphicsSurface, +        &GraphicsProperties, +        Pair<WithGraphicsContext, Wildcard>, +    )>, +    global_light: Single<GlobalLight>, +    assets: Single<Assets>, +    mut actions: Actions, +) +{ +    for ( +        window_ent_id, +        (window, window_graphics_surface, window_graphics_props, graphics_context_pair), +    ) in window_query.iter_with_euids() +    { +        let Some(graphics_context_ent) = graphics_context_pair.get_target_ent() else { +            tracing::error!("Window's associated graphics context entity does not exist"); +            actions.remove_components(window_ent_id, [graphics_context_pair.id()]); +            continue; +        }; + +        let Some(mut graphics_context) = +            graphics_context_ent.get_mut::<GraphicsContext>() +        else { +            tracing::error!( +                "Graphics context entity does not have a GraphicsContext component" +            ); +            return; +        }; + +        let GraphicsContext { +            ref context, +            ref mut shader_program, +            ref mut textures_objs, +            ref mut default_1x1_texture_obj, +            ref mut graphics_mesh_store, +        } = *graphics_context; + +        let Some((camera, camera_world_pos, _)) = camera_query.iter().next() else { +            tracing::warn!("No current camera. Nothing will be rendered"); +            return; +        }; + +        let Ok(current_graphics_context) = +            context.make_current(&window_graphics_surface.surface) +        else { +            tracing::error!("Failed to make graphics context current"); +            continue; +        }; + +        let directional_lights = directional_lights.iter().collect::<Vec<_>>(); + +        let shader_program = shader_program.get_or_insert_with(|| { +            create_default_shader_program(¤t_graphics_context).unwrap() +        }); + +        let mut clear_mask = BufferClearMask::COLOR; + +        clear_mask.set(BufferClearMask::DEPTH, window_graphics_props.depth_test); + +        clear_buffers(¤t_graphics_context, clear_mask); + +        for ( +            euid, +            ( +                model, +                material_flags, +                position, +                scale, +                draw_flags, +                data_in_graphics_ctx_pairs, +            ), +        ) in query.iter_with_euids() +        { +            let Some(model_data) = assets.get(&model.asset_handle) else { +                tracing::trace!("Missing model asset"); +                continue; +            }; + +            let material_flags = material_flags +                .map(|material_flags| material_flags.clone()) +                .unwrap_or_default(); + +            let graphics_mesh_id = match data_in_graphics_ctx_pairs +                .get_with_target_id(graphics_context_ent.uid()) +            { +                Some(data_in_graphics_ctx_pair) => { +                    let Some(data_in_graphics_ctx) = +                        data_in_graphics_ctx_pair.get_data::<DataInGraphicsContext>() +                    else { +                        tracing::warn!( +                            concat!( +                                "Pair with relation {} ({}) has no data or data with a ", +                                "wrong type. This pair will be removed" +                            ), +                            type_name::<DataInGraphicsContext>(), +                            data_in_graphics_ctx_pair.id() +                        ); + +                        actions.remove_components(euid, [data_in_graphics_ctx_pair.id()]); +                        continue; +                    }; + +                    data_in_graphics_ctx.graphics_mesh_id +                } +                None => { +                    let graphics_mesh = match GraphicsMesh::new( +                        ¤t_graphics_context, +                        &model_data.mesh, +                    ) { +                        Ok(graphics_mesh) => graphics_mesh, +                        Err(err) => { +                            tracing::error!( +                                "Failed to create {}: {err}", +                                type_name::<GraphicsMesh>() +                            ); + +                            // This system should not try again +                            actions.add_components(euid, (NoDraw,)); + +                            continue; +                        } +                    }; + +                    let graphics_mesh_id = graphics_mesh_store.insert(graphics_mesh); + +                    actions.add_components( +                        euid, +                        (Pair::builder() +                            .relation_as_data(DataInGraphicsContext { graphics_mesh_id }) +                            .target_id(graphics_context_ent.uid()) +                            .build(),), +                    ); + +                    graphics_mesh_id +                } +            }; + +            let Some(graphics_mesh) = +                graphics_mesh_store.graphics_meshes.get(&graphics_mesh_id) +            else { +                tracing::error!("Graphics mesh with ID: {graphics_mesh_id:?} not found"); +                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, +            apply_transformation_matrices( +                ¤t_graphics_context, +                Transformation { +                    position: position.map(|pos| *pos).unwrap_or_default().position, +                    scale: scale.map(|scale| *scale).unwrap_or_default().scale, +                }, +                shader_program, +                &camera, +                &camera_world_pos, +                window.inner_size(),              ); -        } -        draw_mesh(gl_objects); +            if model_data.materials.len() > 1 { +                tracing::warn!(concat!( +                    "Multiple model materials are not supported ", +                    "so only the first material will be used" +                )); +            } + +            let material = match model_data.materials.values().next() { +                Some(material) => material, +                None => { +                    tracing::warn!("Model has no materials. Using default material"); -        if draw_flags.is_some() { -            let default_polygon_mode_config = PolygonModeConfig::default(); +                    &Material::default() +                } +            }; -            crate::opengl::set_polygon_mode( -                default_polygon_mode_config.face, -                default_polygon_mode_config.mode, +            apply_light( +                ¤t_graphics_context, +                &material, +                &material_flags, +                &global_light, +                shader_program, +                (point_light_query.iter(), point_light_query.iter().count()), +                directional_lights +                    .iter() +                    .map(|(dir_light,)| &**dir_light) +                    .collect::<Vec<_>>() +                    .as_slice(), +                &camera_world_pos,              ); + +            match create_bind_material_textures( +                ¤t_graphics_context, +                &material, +                &assets, +                textures_objs, +                default_1x1_texture_obj, +            ) { +                Ok(()) => {} +                Err(CreateBindMaterialTexturesError::MissingTextureAsset) => { +                    continue; +                } +                Err( +                    err @ CreateBindMaterialTexturesError::CreateTextureFailed { .. }, +                ) => { +                    tracing::error!( +                        "Creating &/ binding material textures failed: {err}" +                    ); + +                    // This system should not try again +                    actions.add_components(euid, (NoDraw,)); + +                    continue; +                } +            } + +            shader_program.activate(¤t_graphics_context); + +            if let Some(draw_flags) = &draw_flags { +                opengl_bindings::misc::set_polygon_mode( +                    ¤t_graphics_context, +                    draw_flags.polygon_mode_config.face, +                    draw_flags.polygon_mode_config.mode, +                ); +            } + +            if let Err(err) = draw_mesh(¤t_graphics_context, &graphics_mesh) { +                tracing::error!( +                    entity_id = %euid, +                    graphics_context_entity_id = %graphics_context_ent.uid(), +                    "Failed to draw mesh: {err}", +                ); + +                // This system should not try again +                actions.add_components(euid, (NoDraw,)); + +                continue; +            }; + +            if draw_flags.is_some() { +                let default_polygon_mode_config = PolygonModeConfig::default(); + +                opengl_bindings::misc::set_polygon_mode( +                    ¤t_graphics_context, +                    default_polygon_mode_config.face, +                    default_polygon_mode_config.mode, +                ); +            } +        } + +        if let Err(err) = window_graphics_surface +            .surface +            .swap_buffers(context.context()) +        { +            tracing::error!("Failed to swap buffers: {err}");          }      }  } -#[derive(Debug, Default, Component)] -struct GlobalGlObjects +fn create_default_texture(current_context: &CurrentContextWithFns<'_>) -> GlTexture  { -    shader_programs: HashMap<u64, GlShaderProgram>, -    textures: HashMap<TextureId, GlTexture>, +    match create_gl_texture( +        current_context, +        &Image::from_color(Dimens { width: 1, height: 1 }, Color::WHITE_U8), +        &TextureProperties::default(), +    ) { +        Ok(gl_texture) => gl_texture, +        Err( +            GlTextureGenerateError::SizeWidthValueTooLarge { value: _, max_value: _ } +            | GlTextureGenerateError::SizeHeightValueTooLarge { value: _, max_value: _ }, +        ) => unreachable!(), +    }  } -fn set_viewport(position: Vec2<u32>, size: Dimens<u32>) +fn create_bind_material_textures( +    current_context: &CurrentContextWithFns<'_>, +    material: &Material, +    assets: &Assets, +    texture_objs: &mut HashMap<AssetId, GlTexture>, +    default_1x1_texture_obj: &mut Option<GlTexture>, +) -> Result<(), CreateBindMaterialTexturesError>  { -    crate::opengl::set_viewport(position, size); -} +    let material_texture_maps = [ +        (&material.ambient_map, AMBIENT_MAP_TEXTURE_UNIT), +        (&material.diffuse_map, DIFFUSE_MAP_TEXTURE_UNIT), +        (&material.specular_map, SPECULAR_MAP_TEXTURE_UNIT), +    ]; -#[cfg(feature = "debug")] -fn initialize_debug() -{ -    use crate::opengl::debug::{ -        enable_debug_output, -        set_debug_message_callback, -        set_debug_message_control, -        MessageIdsAction, -    }; +    for (texture, texture_unit) in material_texture_maps { +        let Some(texture) = texture else { +            let gl_texture = default_1x1_texture_obj +                .get_or_insert_with(|| create_default_texture(current_context)); + +            gl_texture.bind_to_texture_unit(current_context, texture_unit); -    enable_debug_output(); +            continue; +        }; -    set_debug_message_callback(opengl_debug_message_cb); +        let texture_image_asset_id = texture.asset_handle.id(); + +        let gl_texture = match texture_objs.get(&texture_image_asset_id) { +            Some(gl_texture) => gl_texture, +            None => { +                let Some(image) = assets.get::<Image>(&texture.asset_handle) else { +                    tracing::trace!(handle=?texture.asset_handle, "Missing texture asset"); +                    return Err(CreateBindMaterialTexturesError::MissingTextureAsset); +                }; + +                texture_objs.entry(texture_image_asset_id).or_insert( +                    create_gl_texture(current_context, image, &texture.properties) +                        .map_err(|err| { +                            CreateBindMaterialTexturesError::CreateTextureFailed { +                                err, +                                image_asset_id: texture_image_asset_id, +                            } +                        })?, +                ) +            } +        }; -    set_debug_message_control(None, None, None, &[], MessageIdsAction::Disable); +        gl_texture.bind_to_texture_unit(current_context, texture_unit); +    } + +    Ok(())  } -fn draw_mesh(gl_objects: &GlObjects) +#[derive(Debug, thiserror::Error)] +enum CreateBindMaterialTexturesError  { -    gl_objects.vertex_arr.bind(); +    #[error("Missing texture asset")] +    MissingTextureAsset, -    if gl_objects.index_buffer.is_some() { -        VertexArray::draw_elements(PrimitiveKind::Triangles, 0, gl_objects.index_cnt); -    } else { -        VertexArray::draw_arrays(PrimitiveKind::Triangles, 0, 3); -    } +    #[error("Failed to create texture from image asset with ID {image_asset_id:?}")] +    CreateTextureFailed +    { +        #[source] +        err: GlTextureGenerateError, +        image_asset_id: AssetId, +    },  } -fn create_gl_texture(texture: &Texture) -> GlTexture +fn set_viewport( +    current_context: &CurrentContextWithFns<'_>, +    position: Vec2<u32>, +    size: &Dimens<u32>, +) -> Result<(), GlSetViewportError>  { -    let mut gl_texture = GlTexture::new(); +    let position = +        opengl_bindings::data_types::Vec2::<u32> { x: position.x, y: position.y }; -    gl_texture.generate( -        *texture.dimensions(), -        texture.image().as_bytes(), -        texture.pixel_data_format(), -    ); - -    gl_texture.apply_properties(texture.properties()); +    let size = opengl_bindings::data_types::Dimens::<u32> { +        width: size.width, +        height: size.height, +    }; -    gl_texture +    opengl_bindings::misc::set_viewport(current_context, &position, &size)  } -fn create_gl_shader_program( -    shader_program: &ShaderProgram, -) -> Result<GlShaderProgram, GlShaderError> +fn draw_mesh( +    current_context: &CurrentContextWithFns<'_>, +    graphics_mesh: &GraphicsMesh, +) -> Result<(), GlDrawError>  { -    let gl_shaders = shader_program -        .shaders() -        .iter() -        .map(|shader| { -            let gl_shader = GlShader::new(shader.kind()); +    graphics_mesh.vertex_arr.bind(current_context); + +    if graphics_mesh.index_buffer.is_some() { +        VertexArray::draw_elements( +            current_context, +            PrimitiveKind::Triangles, +            0, +            graphics_mesh.element_cnt, +        )?; +    } else { +        VertexArray::draw_arrays( +            current_context, +            PrimitiveKind::Triangles, +            0, +            graphics_mesh.element_cnt, +        )?; +    } -            gl_shader.set_source(shader.source())?; -            gl_shader.compile()?; +    Ok(()) +} -            Ok(gl_shader) -        }) -        .collect::<Result<Vec<_>, _>>()?; +fn create_gl_texture( +    current_context: &CurrentContextWithFns<'_>, +    image: &Image, +    texture_properties: &TextureProperties, +) -> Result<GlTexture, GlTextureGenerateError> +{ +    let gl_texture = GlTexture::new(current_context); -    let gl_shader_program = GlShaderProgram::new(); +    gl_texture.generate( +        current_context, +        &image.dimensions().into(), +        image.as_bytes(), +        match image.color_type() { +            ImageColorType::Rgb8 => GlTexturePixelDataFormat::Rgb8, +            ImageColorType::Rgba8 => GlTexturePixelDataFormat::Rgba8, +            _ => { +                unimplemented!(); +            } +        }, +    )?; -    for gl_shader in &gl_shaders { -        gl_shader_program.attach(gl_shader); -    } +    gl_texture.set_wrap( +        current_context, +        texture_wrapping_to_gl(texture_properties.wrap), +    ); -    gl_shader_program.link()?; +    gl_texture.set_magnifying_filter( +        current_context, +        texture_filtering_to_gl(texture_properties.magnifying_filter), +    ); -    Ok(gl_shader_program) +    gl_texture.set_minifying_filter( +        current_context, +        texture_filtering_to_gl(texture_properties.minifying_filter), +    ); + +    Ok(gl_texture)  } -#[derive(Debug, Component)] -struct GlObjects -{ -    /// Vertex and index buffer has to live as long as the vertex array -    vertex_buffer: Buffer<Vertex>, -    index_buffer: Option<Buffer<u32>>, -    index_cnt: u32, +const VERTEX_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/vertex.glsl"); +const FRAGMENT_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/fragment.glsl"); -    vertex_arr: VertexArray, -} +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"); -impl GlObjects +fn create_default_shader_program( +    current_context: &CurrentContextWithFns<'_>, +) -> Result<GlShaderProgram, CreateShaderError>  { -    #[cfg_attr(feature = "debug", tracing::instrument(skip_all))] -    fn new(mesh: &Mesh) -> Self -    { -        #[cfg(feature = "debug")] -        tracing::trace!( -            "Creating vertex array, vertex buffer{}", -            if mesh.indices().is_some() { -                " and index buffer" -            } else { -                "" -            } -        ); +    let vertex_shader = GlShader::new(current_context, ShaderKind::Vertex); -        let mut vertex_arr = VertexArray::new(); -        let mut vertex_buffer = Buffer::new(); +    vertex_shader.set_source( +        current_context, +        &*glsl_preprocess(VERTEX_GLSL_SHADER_SRC, &get_glsl_shader_content)?, +    )?; -        vertex_buffer.store(mesh.vertices(), BufferUsage::Static); +    vertex_shader.compile(current_context)?; -        vertex_arr.bind_vertex_buffer(0, &vertex_buffer, 0); +    let fragment_shader = GlShader::new(current_context, ShaderKind::Fragment); -        let mut offset = 0u32; +    fragment_shader.set_source( +        current_context, +        &*glsl_preprocess(FRAGMENT_GLSL_SHADER_SRC, &get_glsl_shader_content)?, +    )?; -        for attrib in Vertex::attrs() { -            vertex_arr.enable_attrib(attrib.index); +    fragment_shader.compile(current_context)?; -            vertex_arr.set_attrib_format( -                attrib.index, -                match attrib.component_type { -                    AttributeComponentType::Float => VertexArrayDataType::Float, -                }, -                false, -                offset, -            ); +    let gl_shader_program = GlShaderProgram::new(current_context); -            vertex_arr.set_attrib_vertex_buf_binding(attrib.index, 0); - -            offset += attrib.component_size * attrib.component_cnt as u32; -        } +    gl_shader_program.attach(current_context, &vertex_shader); +    gl_shader_program.attach(current_context, &fragment_shader); -        if let Some(indices) = mesh.indices() { -            let mut index_buffer = Buffer::new(); +    gl_shader_program.link(current_context)?; -            index_buffer.store(indices, BufferUsage::Static); +    Ok(gl_shader_program) +} -            vertex_arr.bind_element_buffer(&index_buffer); +#[derive(Debug, thiserror::Error)] +enum CreateShaderError +{ +    #[error(transparent)] +    ShaderError(#[from] GlShaderError), -            return Self { -                vertex_buffer, -                index_buffer: Some(index_buffer), -                index_cnt: indices.len().try_into().unwrap(), -                vertex_arr, -            }; -        } +    #[error(transparent)] +    PreprocessingError(#[from] GlslPreprocessingError), +} -        Self { -            vertex_buffer, -            index_buffer: None, -            index_cnt: 0, -            vertex_arr, -        } +fn get_glsl_shader_content(path: &Path) -> Result<Vec<u8>, std::io::Error> +{ +    if path == Path::new("vertex_data.glsl") { +        return Ok(VERTEX_DATA_GLSL_SHADER_SRC.as_bytes().to_vec());      } -    pub fn clone(&self) -> NeverDrop<Self> -    { -        NeverDrop::new(Self { -            // SAFETY: The vertex buffer will never become dropped (NeverDrop ensures it) -            vertex_buffer: unsafe { self.vertex_buffer.clone_weak() }, -            index_buffer: self -                .index_buffer -                .as_ref() -                // SAFETY: The index buffer will never become dropped (NeverDrop ensures -                // it) -                .map(|index_buffer| unsafe { index_buffer.clone_weak() }), -            index_cnt: self.index_cnt, -            // SAFETY: The vertex array will never become dropped (NeverDrop ensures it) -            vertex_arr: unsafe { self.vertex_arr.clone_unsafe() }, -        }) +    if path == Path::new("light.glsl") { +        return Ok(LIGHT_GLSL_SHADER_SRC.as_bytes().to_vec());      } + +    Err(IoError::new( +        IoErrorKind::NotFound, +        format!("Content for shader file {} not found", path.display()), +    )) +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct GraphicsMeshId +{ +    inner: usize,  }  fn apply_transformation_matrices( +    current_context: &CurrentContextWithFns<'_>,      transformation: Transformation,      gl_shader_program: &mut GlShaderProgram,      camera: &Camera, -    camera_pos: &Position, -    window_size: Dimens<u32>, +    camera_world_pos: &WorldPosition, +    window_size: &Dimens<u32>,  )  { -    gl_shader_program -        .set_uniform_matrix_4fv(c"model", &create_transformation_matrix(transformation)); +    gl_shader_program.set_uniform( +        current_context, +        c"model", +        &opengl_bindings::data_types::Matrix { +            items: create_transformation_matrix(transformation).items, +        }, +    ); -    let view = create_view(camera, camera_pos); +    let view_matrix = create_view_matrix(camera, &camera_world_pos.position); -    gl_shader_program.set_uniform_matrix_4fv(c"view", &view); +    gl_shader_program.set_uniform( +        current_context, +        c"view", +        &opengl_bindings::data_types::Matrix { items: view_matrix.items }, +    );      #[allow(clippy::cast_precision_loss)] -    let projection = match &camera.projection { -        Projection::Perspective(perspective) => new_perspective_matrix( -            perspective, +    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,          ), +        Projection::Orthographic(orthographic_proj) => orthographic_proj +            .to_matrix_rh(&camera_world_pos.position, ClipVolume::NegOneToOne),      }; -    gl_shader_program.set_uniform_matrix_4fv(c"projection", &projection); +    gl_shader_program.set_uniform( +        current_context, +        c"projection", +        &opengl_bindings::data_types::Matrix { items: proj_matrix.items }, +    );  } -fn apply_light<PointLightHolder>( +fn apply_light<'point_light>( +    current_context: &CurrentContextWithFns<'_>,      material: &Material,      material_flags: &MaterialFlags,      global_light: &GlobalLight,      gl_shader_program: &mut GlShaderProgram, -    point_lights: &[PointLightHolder], +    (point_light_iter, point_light_cnt): ( +        impl Iterator< +            Item = ( +                ComponentHandle<'point_light, PointLight>, +                ComponentHandle<'point_light, WorldPosition>, +            ), +        >, +        usize, +    ),      directional_lights: &[&DirectionalLight], -    camera_pos: &Position, -) where -    PointLightHolder: Deref<Target = PointLight>, +    camera_world_pos: &WorldPosition, +)  {      debug_assert!( -        point_lights.len() < 64, +        point_light_cnt < 64,          "Shader cannot handle more than 64 point lights"      ); @@ -468,16 +1218,20 @@ fn apply_light<PointLightHolder>(      );      for (dir_light_index, dir_light) in directional_lights.iter().enumerate() { -        gl_shader_program.set_uniform_vec_3fv( +        let direction: opengl_bindings::data_types::Vec3<_> = dir_light.direction.into(); + +        gl_shader_program.set_uniform( +            current_context,              &create_light_uniform_name(                  "directional_lights",                  dir_light_index,                  "direction",              ), -            &dir_light.direction, +            &direction,          );          set_light_phong_uniforms( +            current_context,              gl_shader_program,              "directional_lights",              dir_light_index, @@ -487,120 +1241,163 @@ fn apply_light<PointLightHolder>(      // 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); +    gl_shader_program.set_uniform( +        current_context, +        c"directional_light_cnt", +        &(directional_lights.len() as i32), +    ); + +    for (point_light_index, (point_light, point_light_world_pos)) in +        point_light_iter.enumerate() +    { +        let pos: opengl_bindings::data_types::Vec3<_> = +            (point_light_world_pos.position + point_light.local_position).into(); -    for (point_light_index, point_light) in point_lights.iter().enumerate() { -        gl_shader_program.set_uniform_vec_3fv( +        gl_shader_program.set_uniform( +            current_context,              &create_light_uniform_name("point_lights", point_light_index, "position"), -            &point_light.position, +            &pos,          );          set_light_phong_uniforms( +            current_context,              gl_shader_program,              "point_lights",              point_light_index, -            &**point_light, +            &*point_light,          );          set_light_attenuation_uniforms( +            current_context,              gl_shader_program,              "point_lights",              point_light_index, -            point_light, +            &*point_light,          );      }      // There probably won't be more than 2147483648 point lights      #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] -    gl_shader_program.set_uniform_1i(c"point_light_cnt", point_lights.len() as i32); +    gl_shader_program.set_uniform( +        current_context, +        c"point_light_cnt", +        &(point_light_cnt as i32), +    ); -    gl_shader_program.set_uniform_vec_3fv( -        c"material.ambient", -        &if material_flags.use_ambient_color { +    let ambient: opengl_bindings::data_types::Vec3<_> = +        Vec3::from(if material_flags.use_ambient_color {              material.ambient.clone()          } else {              global_light.ambient.clone() -        } -        .into(), -    ); +        }) +        .into(); + +    gl_shader_program.set_uniform(current_context, c"material.ambient", &ambient); -    gl_shader_program -        .set_uniform_vec_3fv(c"material.diffuse", &material.diffuse.clone().into()); +    let diffuse: opengl_bindings::data_types::Vec3<_> = +        Vec3::from(material.diffuse.clone()).into(); + +    gl_shader_program.set_uniform(current_context, c"material.diffuse", &diffuse); + +    let specular: opengl_bindings::data_types::Vec3<_> = +        Vec3::from(material.specular.clone()).into();      #[allow(clippy::cast_possible_wrap)] -    gl_shader_program -        .set_uniform_vec_3fv(c"material.specular", &material.specular.clone().into()); +    gl_shader_program.set_uniform(current_context, c"material.specular", &specular);      #[allow(clippy::cast_possible_wrap)] -    gl_shader_program.set_uniform_1i( +    gl_shader_program.set_uniform( +        current_context,          c"material.ambient_map", -        material.ambient_map.into_inner() as i32, +        &(AMBIENT_MAP_TEXTURE_UNIT as i32),      );      #[allow(clippy::cast_possible_wrap)] -    gl_shader_program.set_uniform_1i( +    gl_shader_program.set_uniform( +        current_context,          c"material.diffuse_map", -        material.diffuse_map.into_inner() as i32, +        &(DIFFUSE_MAP_TEXTURE_UNIT as i32),      );      #[allow(clippy::cast_possible_wrap)] -    gl_shader_program.set_uniform_1i( +    gl_shader_program.set_uniform( +        current_context,          c"material.specular_map", -        material.specular_map.into_inner() as i32, +        &(SPECULAR_MAP_TEXTURE_UNIT as i32), +    ); + +    gl_shader_program.set_uniform( +        current_context, +        c"material.shininess", +        &material.shininess,      ); -    gl_shader_program.set_uniform_1fv(c"material.shininess", material.shininess); +    let view_pos: opengl_bindings::data_types::Vec3<_> = camera_world_pos.position.into(); -    gl_shader_program.set_uniform_vec_3fv(c"view_pos", &camera_pos.position); +    gl_shader_program.set_uniform(current_context, c"view_pos", &view_pos);  }  fn set_light_attenuation_uniforms( +    current_context: &CurrentContextWithFns<'_>,      gl_shader_program: &mut GlShaderProgram,      light_array: &str,      light_index: usize,      light: &PointLight,  )  { -    gl_shader_program.set_uniform_1fv( +    gl_shader_program.set_uniform( +        current_context,          &create_light_uniform_name(              light_array,              light_index,              "attenuation_props.constant",          ), -        light.attenuation_params.constant, +        &light.attenuation_params.constant,      ); -    gl_shader_program.set_uniform_1fv( +    gl_shader_program.set_uniform( +        current_context,          &create_light_uniform_name(light_array, light_index, "attenuation_props.linear"), -        light.attenuation_params.linear, +        &light.attenuation_params.linear,      ); -    gl_shader_program.set_uniform_1fv( +    gl_shader_program.set_uniform( +        current_context,          &create_light_uniform_name(              light_array,              light_index,              "attenuation_props.quadratic",          ), -        light.attenuation_params.quadratic, +        &light.attenuation_params.quadratic,      );  }  fn set_light_phong_uniforms( +    current_context: &CurrentContextWithFns<'_>,      gl_shader_program: &mut GlShaderProgram,      light_array: &str,      light_index: usize,      light: &impl Light,  )  { -    gl_shader_program.set_uniform_vec_3fv( +    gl_shader_program.set_uniform( +        current_context,          &create_light_uniform_name(light_array, light_index, "phong.diffuse"), -        &light.diffuse().clone().into(), +        &opengl_bindings::data_types::Vec3 { +            x: light.diffuse().red, +            y: light.diffuse().green, +            z: light.diffuse().blue, +        },      ); -    gl_shader_program.set_uniform_vec_3fv( +    gl_shader_program.set_uniform( +        current_context,          &create_light_uniform_name(light_array, light_index, "phong.specular"), -        &light.specular().clone().into(), +        &opengl_bindings::data_types::Vec3 { +            x: light.specular().red, +            y: light.specular().green, +            z: light.specular().blue, +        },      );  } @@ -649,16 +1446,15 @@ fn create_light_uniform_name(      }  } -fn create_view(camera: &Camera, camera_pos: &Position) -> Matrix<f32, 4, 4> +fn create_view_matrix(camera: &Camera, camera_pos: &Vec3<f32>) -> Matrix<f32, 4, 4>  {      let mut view = Matrix::new(); -    view.look_at(&camera_pos.position, &camera.target, &camera.global_up); +    view.look_at(&camera_pos, &camera.target, &camera.global_up);      view  } -#[cfg(feature = "debug")]  #[tracing::instrument(skip_all)]  fn opengl_debug_message_cb(      source: MessageSource, @@ -689,7 +1485,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 => { @@ -717,3 +1514,74 @@ fn create_transformation_matrix(transformation: Transformation) -> Matrix<f32, 4      matrix  } + +#[inline] +fn texture_wrapping_to_gl(texture_wrapping: TextureWrapping) -> GlTextureWrapping +{ +    match texture_wrapping { +        TextureWrapping::Repeat => GlTextureWrapping::Repeat, +        TextureWrapping::MirroredRepeat => GlTextureWrapping::MirroredRepeat, +        TextureWrapping::ClampToEdge => GlTextureWrapping::ClampToEdge, +        TextureWrapping::ClampToBorder => GlTextureWrapping::ClampToBorder, +    } +} + +#[inline] +fn texture_filtering_to_gl(texture_filtering: TextureFiltering) -> GlTextureFiltering +{ +    match texture_filtering { +        TextureFiltering::Linear => GlTextureFiltering::Linear, +        TextureFiltering::Nearest => GlTextureFiltering::Nearest, +    } +} + +impl<Value: ReprC + Copy> From<Vec2<Value>> for opengl_bindings::data_types::Vec2<Value> +{ +    fn from(vec2: Vec2<Value>) -> Self +    { +        Self { x: vec2.x, y: vec2.y } +    } +} + +impl<Value: ReprC + Copy> From<Vec3<Value>> for opengl_bindings::data_types::Vec3<Value> +{ +    fn from(vec3: Vec3<Value>) -> Self +    { +        Self { x: vec3.x, y: vec3.y, z: vec3.z } +    } +} + +impl<Value: Copy> From<Dimens<Value>> for opengl_bindings::data_types::Dimens<Value> +{ +    fn from(dimens: Dimens<Value>) -> Self +    { +        Self { +            width: dimens.width, +            height: dimens.height, +        } +    } +} + +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, +        } +    } +} diff --git a/engine/src/renderer/opengl/glsl/fragment.glsl b/engine/src/renderer/opengl/glsl/fragment.glsl new file mode 100644 index 0000000..5bf5ff2 --- /dev/null +++ b/engine/src/renderer/opengl/glsl/fragment.glsl @@ -0,0 +1,73 @@ +#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 new file mode 100644 index 0000000..f12b5fe --- /dev/null +++ b/engine/src/renderer/opengl/glsl/light.glsl @@ -0,0 +1,133 @@ +#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 new file mode 100644 index 0000000..b57caa6 --- /dev/null +++ b/engine/src/renderer/opengl/glsl/vertex.glsl @@ -0,0 +1,24 @@ +#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 new file mode 100644 index 0000000..486d445 --- /dev/null +++ b/engine/src/renderer/opengl/glsl/vertex_data.glsl @@ -0,0 +1,11 @@ +#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..9b929c0 --- /dev/null +++ b/engine/src/renderer/opengl/graphics_mesh.rs @@ -0,0 +1,120 @@ +use opengl_bindings::buffer::{Buffer as GlBuffer, Usage as GlBufferUsage}; +use opengl_bindings::vertex_array::{ +    DataType as GlVertexArrayDataType, +    VertexArray as GlVertexArray, +}; +use opengl_bindings::CurrentContextWithFns as GlCurrentContextWithFns; + +use crate::mesh::Mesh; +use crate::renderer::opengl::vertex::{ +    AttributeComponentType as VertexAttributeComponentType, +    Vertex as RendererVertex, +}; + +#[derive(Debug)] +pub struct GraphicsMesh +{ +    /// Vertex and index buffer has to live as long as the vertex array +    _vertex_buffer: GlBuffer<RendererVertex>, +    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, +    ) -> Result<Self, Error> +    { +        tracing::trace!( +            "Creating vertex array, vertex buffer{}", +            if mesh.indices().is_some() { +                " and index buffer" +            } else { +                "" +            } +        ); + +        let vertex_arr = GlVertexArray::new(current_context); +        let vertex_buffer = GlBuffer::new(current_context); + +        vertex_buffer +            .store_mapped( +                current_context, +                mesh.vertices(), +                GlBufferUsage::Static, +                |vertex| RendererVertex { +                    pos: vertex.pos.into(), +                    texture_coords: vertex.texture_coords.into(), +                    normal: vertex.normal.into(), +                }, +            ) +            .map_err(Error::StoreVerticesFailed)?; + +        vertex_arr.bind_vertex_buffer(current_context, 0, &vertex_buffer, 0); + +        let mut offset = 0u32; + +        for attrib in RendererVertex::attrs() { +            vertex_arr.enable_attrib(current_context, attrib.index); + +            vertex_arr.set_attrib_format( +                current_context, +                attrib.index, +                match attrib.component_type { +                    VertexAttributeComponentType::Float => GlVertexArrayDataType::Float, +                }, +                false, +                offset, +            ); + +            vertex_arr.set_attrib_vertex_buf_binding(current_context, attrib.index, 0); + +            offset += attrib.component_size * attrib.component_cnt as u32; +        } + +        if let Some(indices) = mesh.indices() { +            let index_buffer = GlBuffer::new(current_context); + +            index_buffer +                .store(current_context, indices, GlBufferUsage::Static) +                .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 +                .vertices() +                .len() +                .try_into() +                .expect("Mesh vertex count does not fit into a 32-bit unsigned int"), +            vertex_arr, +        }) +    } +} + +#[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), +} diff --git a/engine/src/vertex.rs b/engine/src/renderer/opengl/vertex.rs index 897ee97..5a1593e 100644 --- a/engine/src/vertex.rs +++ b/engine/src/renderer/opengl/vertex.rs @@ -1,23 +1,18 @@ -use std::mem::size_of; +use safer_ffi::derive_ReprC; -use crate::util::builder; -use crate::vector::{Vec2, Vec3}; - -builder! { -#[builder(name = Builder, derives = (Debug, Default))] -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] +#[derive_ReprC]  #[repr(C)]  pub struct Vertex  { -    pos: Vec3<f32>, -    texture_coords: Vec2<f32>, -    normal: Vec3<f32>, -} +    pub pos: opengl_bindings::data_types::Vec3<f32>, +    pub texture_coords: opengl_bindings::data_types::Vec2<f32>, +    pub normal: opengl_bindings::data_types::Vec3<f32>,  }  impl Vertex  { -    pub(crate) fn attrs() -> &'static [Attribute] +    pub fn attrs() -> &'static [Attribute]      {          #[allow(clippy::cast_possible_truncation)]          &[ @@ -43,15 +38,17 @@ impl Vertex      }  } -pub(crate) struct Attribute +#[derive(Debug)] +pub struct Attribute  { -    pub(crate) index: u32, -    pub(crate) component_type: AttributeComponentType, -    pub(crate) component_cnt: AttributeComponentCnt, -    pub(crate) component_size: u32, +    pub index: u32, +    pub component_type: AttributeComponentType, +    pub component_cnt: AttributeComponentCnt, +    pub component_size: u32,  } -pub(crate) enum AttributeComponentType +#[derive(Debug)] +pub enum AttributeComponentType  {      Float,  } @@ -59,7 +56,7 @@ pub(crate) enum AttributeComponentType  #[derive(Debug, Clone, Copy)]  #[repr(u32)]  #[allow(dead_code)] -pub(crate) enum AttributeComponentCnt +pub enum AttributeComponentCnt  {      One = 1,      Two = 2, diff --git a/engine/src/shader.rs b/engine/src/shader.rs deleted file mode 100644 index 89f7b7c..0000000 --- a/engine/src/shader.rs +++ /dev/null @@ -1,186 +0,0 @@ -use std::collections::hash_map::DefaultHasher; -use std::fs::read_to_string; -use std::hash::{Hash, Hasher}; -use std::path::{Path, PathBuf}; - -use ecs::Component; - -use crate::shader_preprocessor::{Error as ShaderPreprocessorError, ShaderPreprocessor}; - -const VERTEX_SHADER_FILE: &str = "vertex.glsl"; -const FRAGMENT_SHADER_FILE: &str = "fragment.glsl"; - -const SHADER_DIR: &str = "engine"; - -/// Shader program -#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Component)] -pub struct Program -{ -    shaders: Vec<Shader>, -} - -impl Program -{ -    /// Creates a new shader program with the default shaders. -    /// -    /// # Errors -    /// Returns `Err` if: -    /// - Reading a default shader file Fails -    /// - Preprocessing a shader fails. -    pub fn new() -> Result<Self, Error> -    { -        let mut program = Self { shaders: Vec::new() }; - -        program.push_shader( -            Shader::read_shader_file( -                Kind::Vertex, -                &Path::new(SHADER_DIR).join(VERTEX_SHADER_FILE), -            )? -            .preprocess()?, -        ); - -        program.push_shader( -            Shader::read_shader_file( -                Kind::Fragment, -                &Path::new(SHADER_DIR).join(FRAGMENT_SHADER_FILE), -            )? -            .preprocess()?, -        ); - -        Ok(program) -    } - -    pub fn push_shader(&mut self, shader: Shader) -    { -        self.shaders.push(shader); -    } - -    pub fn append_shaders(&mut self, shaders: impl IntoIterator<Item = Shader>) -    { -        self.shaders.extend(shaders); -    } - -    #[must_use] -    pub fn shaders(&self) -> &[Shader] -    { -        &self.shaders -    } - -    pub(crate) fn u64_hash(&self) -> u64 -    { -        let mut hasher = DefaultHasher::new(); - -        self.hash(&mut hasher); - -        hasher.finish() -    } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct Shader -{ -    kind: Kind, -    source: String, -    file: PathBuf, -} - -impl Shader -{ -    /// Reads a shader from the specified source file. -    /// -    /// # Errors -    /// Will return `Err` if: -    /// - Reading the file fails -    /// - The shader source is not ASCII -    pub fn read_shader_file(kind: Kind, shader_file: &Path) -> Result<Self, Error> -    { -        let source = read_to_string(shader_file).map_err(|err| Error::ReadFailed { -            source: err, -            shader_file: shader_file.to_path_buf(), -        })?; - -        if !source.is_ascii() { -            return Err(Error::SourceNotAscii); -        } - -        Ok(Self { -            kind, -            source, -            file: shader_file.to_path_buf(), -        }) -    } - -    /// Preprocesses the shaders. -    /// -    /// # Errors -    /// Returns `Err` if preprocessing fails. -    pub fn preprocess(self) -> Result<Self, Error> -    { -        let shader_preprocessor = ShaderPreprocessor::new( -            self.file -                .parent() -                .ok_or(Error::SourcePathHasNoParent)? -                .to_path_buf(), -        ); - -        let source_preprocessed = shader_preprocessor -            .preprocess(self.source, &self.file) -            .map_err(|err| Error::PreprocessFailed { -                source: err, -                shader_file: self.file.clone(), -            })?; - -        Ok(Self { -            kind: self.kind, -            source: source_preprocessed, -            file: self.file.clone(), -        }) -    } - -    #[must_use] -    pub fn kind(&self) -> Kind -    { -        self.kind -    } - -    #[must_use] -    pub fn source(&self) -> &str -    { -        &self.source -    } -} - -/// Shader kind. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Kind -{ -    Vertex, -    Fragment, -} - -/// Shader error -#[derive(Debug, thiserror::Error)] -pub enum Error -{ -    #[error("Failed to read shader {}", shader_file.display())] -    ReadFailed -    { -        #[source] -        source: std::io::Error, -        shader_file: PathBuf, -    }, - -    #[error("Shader source is not ASCII")] -    SourceNotAscii, - -    #[error("Failed to preprocess shader {}", shader_file.display())] -    PreprocessFailed -    { -        #[source] -        source: ShaderPreprocessorError, -        shader_file: PathBuf, -    }, - -    #[error("Shader source path has no parent")] -    SourcePathHasNoParent, -} diff --git a/engine/src/texture.rs b/engine/src/texture.rs index f82b59d..d02b9ff 100644 --- a/engine/src/texture.rs +++ b/engine/src/texture.rs @@ -1,152 +1,37 @@ -use std::fmt::Display; -use std::path::Path; -use std::sync::atomic::{AtomicU32, Ordering}; - -use image::io::Reader as ImageReader; -use image::{DynamicImage, ImageError, Rgb, RgbImage}; - -use crate::color::Color; -use crate::data_types::dimens::Dimens; -use crate::opengl::texture::PixelDataFormat; - -static NEXT_ID: AtomicU32 = AtomicU32::new(0); - -mod reexports -{ -    pub use crate::opengl::texture::{Filtering, Wrapping}; -} - -pub use reexports::*; +use crate::asset::Handle as AssetHandle; +use crate::image::Image; +use crate::builder;  #[derive(Debug, Clone)] +#[non_exhaustive]  pub struct Texture  { -    id: Id, -    image: DynamicImage, -    pixel_data_format: PixelDataFormat, -    dimensions: Dimens<u32>, -    properties: Properties, +    pub asset_handle: AssetHandle<Image>, +    pub properties: Properties,  }  impl Texture  { -    /// Opens a texture image. -    /// -    /// # Errors -    /// Will return `Err` if: -    /// - Opening the image fails -    /// - The image data is not 8-bit/color RGB -    #[allow(clippy::new_without_default)] -    pub fn open(path: &Path) -> Result<Self, Error> -    { -        let image = ImageReader::open(path) -            .map_err(Error::OpenImageFailed)? -            .decode() -            .map_err(Error::DecodeImageFailed)?; - -        let pixel_data_format = match &image { -            DynamicImage::ImageRgb8(_) => PixelDataFormat::Rgb8, -            DynamicImage::ImageRgba8(_) => PixelDataFormat::Rgba8, -            _ => { -                return Err(Error::UnsupportedImageDataKind); -            } -        }; - -        let dimensions = Dimens { -            width: image.width(), -            height: image.height(), -        }; - -        let id = NEXT_ID.fetch_add(1, Ordering::Relaxed); - -        Ok(Self { -            id: Id::new(id), -            image, -            pixel_data_format, -            dimensions, -            properties: Properties::default(), -        }) -    } - -    #[must_use] -    pub fn new_from_color(dimensions: &Dimens<u32>, color: &Color<u8>) -> Self +    pub fn new(asset_handle: AssetHandle<Image>) -> Self      { -        let image = RgbImage::from_pixel( -            dimensions.width, -            dimensions.height, -            Rgb([color.red, color.green, color.blue]), -        ); - -        let id = NEXT_ID.fetch_add(1, Ordering::Relaxed); -          Self { -            id: Id::new(id), -            image: image.into(), -            pixel_data_format: PixelDataFormat::Rgb8, -            dimensions: *dimensions, +            asset_handle,              properties: Properties::default(),          }      } -    #[must_use] -    pub fn id(&self) -> Id -    { -        self.id -    } - -    #[must_use] -    pub fn properties(&self) -> &Properties -    { -        &self.properties -    } - -    pub fn properties_mut(&mut self) -> &mut Properties -    { -        &mut self.properties -    } - -    #[must_use] -    pub fn dimensions(&self) -> &Dimens<u32> -    { -        &self.dimensions -    } - -    #[must_use] -    pub fn pixel_data_format(&self) -> PixelDataFormat -    { -        self.pixel_data_format -    } - -    #[must_use] -    pub fn image(&self) -> &DynamicImage -    { -        &self.image -    } -} - -impl Drop for Texture -{ -    fn drop(&mut self) +    pub fn with_properties( +        asset_handle: AssetHandle<Image>, +        properties: Properties, +    ) -> Self      { -        NEXT_ID.fetch_sub(1, Ordering::Relaxed); +        Self { asset_handle, properties }      }  } -/// Texture error. -#[derive(Debug, thiserror::Error)] -pub enum Error -{ -    #[error("Failed to open texture image")] -    OpenImageFailed(#[source] std::io::Error), - -    #[error("Failed to decode texture image")] -    DecodeImageFailed(#[source] ImageError), - -    #[error("Unsupported image data kind")] -    UnsupportedImageDataKind, -} - +builder! {  /// Texture properties +#[builder(name = PropertiesBuilder, derives=(Debug, Clone))]  #[derive(Debug, Clone)]  #[non_exhaustive]  pub struct Properties @@ -155,6 +40,15 @@ pub struct Properties      pub magnifying_filter: Filtering,      pub minifying_filter: Filtering,  } +} + +impl Properties +{ +    pub fn builder() -> PropertiesBuilder +    { +        PropertiesBuilder::default() +    } +}  impl Default for Properties  { @@ -168,30 +62,29 @@ impl Default for Properties      }  } -/// Texture ID. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Id -{ -    id: u32, -} - -impl Id +impl Default for PropertiesBuilder  { -    fn new(id: u32) -> Self +    fn default() -> Self      { -        Self { id } +        Properties::default().into()      } +} -    pub(crate) fn into_inner(self) -> u32 -    { -        self.id -    } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum Filtering +{ +    Nearest, +    Linear,  } -impl Display for Id +/// Texture wrapping. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum Wrapping  { -    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result -    { -        self.id.fmt(formatter) -    } +    Repeat, +    MirroredRepeat, +    ClampToEdge, +    ClampToBorder,  } diff --git a/engine/src/transform.rs b/engine/src/transform.rs index 5e5e296..7c0c941 100644 --- a/engine/src/transform.rs +++ b/engine/src/transform.rs @@ -2,14 +2,14 @@ use ecs::Component;  use crate::vector::Vec3; -/// A position in 3D space. +/// 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 a505a38..9174734 100644 --- a/engine/src/util.rs +++ b/engine/src/util.rs @@ -1,3 +1,73 @@ +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() } +    } +} +  macro_rules! try_option {      ($expr: expr) => {          match $expr { @@ -9,9 +79,6 @@ macro_rules! try_option {      };  } -use std::mem::ManuallyDrop; -use std::ops::{Deref, DerefMut}; -  pub(crate) use try_option;  macro_rules! or { @@ -26,6 +93,18 @@ macro_rules! or {  pub(crate) use or; +#[macro_export] +macro_rules! expand_map_opt { +    ($in: tt, no_occurance=($($no_occurance: tt)*), occurance=($($occurance: tt)*)) => { +        $($occurance)* +    }; + +    (, no_occurance=($($no_occurance: tt)*), occurance=($($occurance: tt)*)) => { +        $($no_occurance)* +    }; +} + +#[macro_export]  macro_rules! builder {      (          $(#[doc = $doc: literal])* @@ -37,7 +116,8 @@ macro_rules! builder {          $visibility: vis struct $name: ident          {              $( -                $(#[$field_attr: meta])* +                $(#[doc = $field_doc: literal])* +                $(#[builder(skip_generate_fn$($field_skip_generate_fn: tt)?)])?                  $field_visibility: vis $field: ident: $field_type: ty,              )*          } @@ -47,7 +127,7 @@ macro_rules! builder {          $visibility struct $name          {              $( -                $(#[$field_attr])* +                $(#[doc = $field_doc])*                  $field_visibility $field: $field_type,              )*          } @@ -63,12 +143,18 @@ 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] @@ -83,6 +169,7 @@ macro_rules! builder {          impl From<$name> for $builder_name          { +            #[allow(unused_variables)]              fn from(built: $name) -> Self              {                  Self { @@ -94,39 +181,3 @@ macro_rules! builder {          }      };  } - -pub(crate) use builder; - -/// Wrapper that ensures the contained value will never be dropped. -#[derive(Debug)] -pub struct NeverDrop<Value> -{ -    value: ManuallyDrop<Value>, -} - -impl<Value> NeverDrop<Value> -{ -    #[must_use] -    pub fn new(value: Value) -> Self -    { -        Self { value: ManuallyDrop::new(value) } -    } -} - -impl<Value> Deref for NeverDrop<Value> -{ -    type Target = Value; - -    fn deref(&self) -> &Self::Target -    { -        &self.value -    } -} - -impl<Value> DerefMut for NeverDrop<Value> -{ -    fn deref_mut(&mut self) -> &mut Self::Target -    { -        &mut self.value -    } -} diff --git a/engine/src/window.rs b/engine/src/window.rs deleted file mode 100644 index ccc1b8d..0000000 --- a/engine/src/window.rs +++ /dev/null @@ -1,318 +0,0 @@ -use std::borrow::Cow; -use std::ffi::{CStr, CString}; - -use ecs::actions::Actions; -use ecs::extension::Collector as ExtensionCollector; -use ecs::sole::Single; -use ecs::Sole; -use glfw::WindowSize; - -use crate::data_types::dimens::Dimens; -use crate::event::{Conclude as ConcludeEvent, Start as StartEvent}; -use crate::vector::Vec2; - -mod reexports -{ -    pub use glfw::window::{ -        CursorMode, -        Hint as CreationHint, -        HintValue as CreationHintValue, -        InputMode, -        Key, -        KeyModifiers, -        KeyState, -    }; -} - -pub use reexports::*; - -#[derive(Debug, Sole)] -/// Has to be dropped last since it holds the OpenGL context. -#[sole(drop_last)] -pub struct Window -{ -    inner: glfw::Window, -} - -impl Window -{ -    /// Returns a new Window builder. -    #[must_use] -    pub fn builder() -> Builder -    { -        Builder::default() -    } - -    /// Sets the value of a input mode. -    /// -    /// # Errors -    /// Returns `Err` if the input mode is unsupported on the current system. -    pub fn set_input_mode( -        &self, -        input_mode: InputMode, -        enabled: bool, -    ) -> Result<(), Error> -    { -        Ok(self.inner.set_input_mode(input_mode, enabled)?) -    } - -    /// Sets the cursor mode. -    /// -    /// # Errors -    /// If a platform error occurs. -    pub fn set_cursor_mode(&self, cursor_mode: CursorMode) -> Result<(), Error> -    { -        Ok(self.inner.set_cursor_mode(cursor_mode)?) -    } - -    /// Returns whether or not the window should close. Will return true when the user has -    /// attempted to close the window. -    #[must_use] -    pub fn should_close(&self) -> bool -    { -        self.inner.should_close() -    } - -    /// Processes all pending events. -    /// -    /// # Errors -    /// If a platform error occurs. -    pub fn poll_events(&self) -> Result<(), Error> -    { -        Ok(self.inner.poll_events()?) -    } - -    /// Swaps the front and back buffers of the window. -    /// -    /// # Errors -    /// Will return `Err` if a platform error occurs or if no OpenGL window context -    /// is present. -    pub fn swap_buffers(&self) -> Result<(), Error> -    { -        Ok(self.inner.swap_buffers()?) -    } - -    /// Returns the size of the window. -    /// -    /// # Errors -    /// Will return `Err` if a platform error occurs. -    pub fn size(&self) -> Result<Dimens<u32>, Error> -    { -        let size = self.inner.size()?; - -        Ok(Dimens { -            width: size.width, -            height: size.height, -        }) -    } - -    /// Returns the address of the specified OpenGL function, if it is supported by the -    /// current OpenGL context. -    /// -    /// # Errors -    /// Will return `Err` if a platform error occurs or if no current context has -    /// been set. -    /// -    /// # Panics -    /// Will panic if the `proc_name` argument contains a nul byte. -    pub fn get_proc_address( -        &self, -        proc_name: &str, -    ) -> Result<unsafe extern "C" fn(), Error> -    { -        let proc_name_c: Cow<CStr> = CStr::from_bytes_with_nul(proc_name.as_bytes()) -            .map(Cow::Borrowed) -            .or_else(|_| CString::new(proc_name).map(Cow::Owned)) -            .expect("OpenGL function name contains a nul byte"); - -        Ok(self.inner.get_proc_address(&proc_name_c)?) -    } - -    /// Makes the OpenGL context of the window current for the calling thread. -    /// -    /// # Errors -    /// Will return `Err` if a platform error occurs or if no OpenGL context is -    /// present. -    pub fn make_context_current(&self) -> Result<(), Error> -    { -        Ok(self.inner.make_context_current()?) -    } - -    /// Sets the window's framebuffer size callback. -    pub fn set_framebuffer_size_callback(&self, callback: impl Fn(Dimens<u32>) + 'static) -    { -        self.inner.set_framebuffer_size_callback(move |size| { -            callback(Dimens { -                width: size.width, -                height: size.height, -            }); -        }); -    } - -    /// Sets the window's key size callback. -    pub fn set_key_callback( -        &self, -        callback: impl Fn(Key, i32, KeyState, KeyModifiers) + 'static, -    ) -    { -        self.inner.set_key_callback(callback); -    } - -    /// Sets the window's cursor position callback. -    pub fn set_cursor_pos_callback(&self, callback: impl Fn(Vec2<f64>) + 'static) -    { -        self.inner -            .set_cursor_pos_callback(move |pos| callback(Vec2 { x: pos.x, y: pos.y })); -    } - -    /// Sets the window's close callback. -    pub fn set_close_callback(&self, callback: impl Fn() + 'static) -    { -        self.inner.set_close_callback(callback); -    } - -    /// Sets the window's focus callback. The callback is called when the window loses or -    /// gains input focus. -    pub fn set_focus_callback(&self, callback: impl Fn(bool) + 'static) -    { -        self.inner.set_focus_callback(callback); -    } -} - -/// [`Window`] builder. -#[derive(Debug, Clone, Default)] -pub struct Builder -{ -    inner: glfw::WindowBuilder, -} - -impl Builder -{ -    #[must_use] -    pub fn creation_hint(mut self, hint: CreationHint, value: CreationHintValue) -> Self -    { -        self.inner = self.inner.hint(hint, value); - -        self -    } - -    /// Creates a new window. -    /// -    /// # Errors -    /// Will return `Err` if the title contains a internal nul byte or if a platform error -    /// occurs. -    pub fn create(&self, size: Dimens<u32>, title: &str) -> Result<Window, Error> -    { -        let builder = self.inner.clone().hint( -            CreationHint::OpenGLDebugContext, -            CreationHintValue::Bool(cfg!(feature = "debug")), -        ); - -        let window = builder.create( -            &WindowSize { -                width: size.width, -                height: size.height, -            }, -            title, -        )?; - -        Ok(Window { inner: window }) -    } -} - -#[derive(Debug)] -pub struct Extension -{ -    window_builder: Builder, -    window_size: Dimens<u32>, -    window_title: String, -} - -impl Extension -{ -    #[must_use] -    pub fn new(window_builder: Builder) -> Self -    { -        Self { window_builder, ..Default::default() } -    } - -    #[must_use] -    pub fn window_size(mut self, window_size: Dimens<u32>) -> Self -    { -        self.window_size = window_size; - -        self -    } - -    #[must_use] -    pub fn window_title(mut self, window_title: impl Into<String>) -> Self -    { -        self.window_title = window_title.into(); - -        self -    } -} - -impl ecs::extension::Extension for Extension -{ -    fn collect(self, mut collector: ExtensionCollector<'_>) -    { -        collector.add_system(StartEvent, initialize); -        collector.add_system(ConcludeEvent, update); - -        let window = self -            .window_builder -            .create(self.window_size, &self.window_title) -            .unwrap(); - -        window.set_cursor_mode(CursorMode::Normal).unwrap(); - -        collector.add_sole(window).ok(); -    } -} - -impl Default for Extension -{ -    fn default() -> Self -    { -        Self { -            window_builder: Builder::default(), -            window_size: Dimens { width: 1920, height: 1080 }, -            window_title: String::new(), -        } -    } -} - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct Error(glfw::Error); - -impl From<glfw::Error> for Error -{ -    fn from(err: glfw::Error) -> Self -    { -        Self(err) -    } -} - -fn initialize(window: Single<Window>, actions: Actions) -{ -    let actions_weak_ref = actions.to_weak_ref(); - -    window.set_close_callback(move || { -        let actions_weak_ref = actions_weak_ref.clone(); - -        let actions_ref = actions_weak_ref.access().expect("No world"); - -        actions_ref.to_actions().stop(); -    }); -} - -fn update(window: Single<Window>) -{ -    window -        .swap_buffers() -        .expect("Failed to swap window buffers"); - -    window.poll_events().expect("Failed to poll window events"); -} diff --git a/engine/src/windowing.rs b/engine/src/windowing.rs new file mode 100644 index 0000000..69adae9 --- /dev/null +++ b/engine/src/windowing.rs @@ -0,0 +1,669 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Weak}; +use std::thread::{spawn, JoinHandle as ThreadJoinHandle}; + +use crossbeam_channel::{ +    bounded as bounded_channel, +    Receiver as ChannelReceiver, +    Sender as ChannelSender, +    TrySendError, +}; +use ecs::actions::Actions; +use ecs::component::Component; +use ecs::entity::obtainer::Obtainer as EntityObtainer; +use ecs::event::component::{Added, Changed, 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::{declare_entity, Query, Sole}; +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, +    Motion as MouseMotion, +}; +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 = 128; + +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(MouseMotion::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.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_motion: Single<MouseMotion>, +    mut mouse_buttons: Single<MouseButtons>, +    mut actions: Actions, +    entity_obtainer: EntityObtainer, +) +{ +    keyboard.make_key_states_previous(); +    mouse_buttons.make_states_previous(); +    mouse_motion.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); + +                tracing::debug!("Added window component to window entity"); + +                windows.insert( +                    WindowId::from_inner(winit_window.id()), +                    (winit_window, window_ent_id), +                ); +            } +            MessageFromApp::WindowResized(window_id, new_window_size) => { +                let Some(window_ent_id) = +                    windows.get(&window_id).map(|(_, ent_id)| ent_id) +                else { +                    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::KeyboardKeyStateChanged(key, key_state) => { +                keyboard.set_key_state(key, key_state); +            } +            MessageFromApp::MouseMoved { position_delta } => { +                mouse_motion.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.id(); + +        let window = evt_match.get_changed_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_removed_comp(); + +        context.windows.remove(window.wid()); + +        actions.add_components(evt_match.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: 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}"); +                } +            }), +            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), +    KeyboardKeyStateChanged(Key, KeyState), +    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 +{ +    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; +                            } +                        }, +                    ); + +                    tracing::info!("Created window has title {}", winit_window.title()); + +                    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; +                } +            } +        } +    } + +    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) {} + +    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::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; +                } +            } +            _ => {} +        } +    } + +    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..1afe594 --- /dev/null +++ b/engine/src/windowing/mouse.rs @@ -0,0 +1,136 @@ +use std::collections::HashMap; + +use ecs::Sole; + +use crate::vector::Vec2; + +#[derive(Debug, Default, Clone, Sole)] +#[non_exhaustive] +pub struct Motion +{ +    pub position_delta: 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 +    } + +    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..79b2102 --- /dev/null +++ b/engine/src/windowing/window.rs @@ -0,0 +1,171 @@ +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 +{ +    wid: Id, +    pub title: Cow<'static, str>, +    pub cursor_visible: bool, +    pub cursor_grab_mode: CursorGrabMode, +    inner_size: Dimens<u32>, +} + +impl Window +{ +    pub fn wid(&self) -> Id +    { +        self.wid +    } + +    pub fn inner_size(&self) -> &Dimens<u32> +    { +        &self.inner_size +    } + +    pub(crate) fn new( +        winit_window: &winit::window::Window, +        creation_attrs: &CreationAttributes, +    ) -> Self +    { +        Self { +            wid: Id::from_inner(winit_window.id()), +            title: creation_attrs.title().to_string().into(), +            cursor_visible: true, +            cursor_grab_mode: CursorGrabMode::None, +            inner_size: winit_window.inner_size().into(), +        } +    } + +    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; +    } +} + +#[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..7226c7d --- /dev/null +++ b/engine/src/work_queue.rs @@ -0,0 +1,44 @@ +use std::marker::PhantomData; +use std::sync::mpsc::{channel as mpsc_channel, Sender as MpscSender}; +use std::thread::JoinHandle as ThreadHandle; + +pub struct Work<UserData: Send + Sync + 'static> +{ +    pub func: fn(UserData), +    pub user_data: UserData, +} + +#[derive(Debug)] +pub struct WorkQueue<UserData: Send + Sync + 'static> +{ +    work_sender: MpscSender<Work<UserData>>, +    _thread: ThreadHandle<()>, +    _pd: PhantomData<UserData>, +} + +impl<UserData: Send + Sync + 'static> WorkQueue<UserData> +{ +    pub fn new() -> Self +    { +        let (work_sender, work_receiver) = mpsc_channel::<Work<UserData>>(); + +        Self { +            work_sender, +            _thread: std::thread::spawn(move || { +                let work_receiver = work_receiver; + +                while let Ok(work) = work_receiver.recv() { +                    (work.func)(work.user_data); +                } +            }), +            _pd: PhantomData, +        } +    } + +    pub fn add_work(&self, work: Work<UserData>) +    { +        if self.work_sender.send(work).is_err() { +            tracing::error!("Cannot add work to work queue. Work queue thread is dead"); +        } +    } +}  | 
