summaryrefslogtreecommitdiff
path: root/engine/src
diff options
context:
space:
mode:
Diffstat (limited to 'engine/src')
-rw-r--r--engine/src/asset.rs921
-rw-r--r--engine/src/camera.rs38
-rw-r--r--engine/src/camera/fly.rs148
-rw-r--r--engine/src/collision.rs142
-rw-r--r--engine/src/data_types/color.rs196
-rw-r--r--engine/src/data_types/dimens.rs77
-rw-r--r--engine/src/data_types/matrix.rs492
-rw-r--r--engine/src/data_types/vector.rs364
-rw-r--r--engine/src/delta_time.rs32
-rw-r--r--engine/src/draw_flags.rs20
-rw-r--r--engine/src/event.rs27
-rw-r--r--engine/src/file_format/wavefront/common.rs12
-rw-r--r--engine/src/file_format/wavefront/mtl.rs215
-rw-r--r--engine/src/file_format/wavefront/obj.rs772
-rw-r--r--engine/src/image.rs363
-rw-r--r--engine/src/input.rs240
-rw-r--r--engine/src/input/keyboard.rs6
-rw-r--r--engine/src/input/mouse.rs6
-rw-r--r--engine/src/lib.rs108
-rw-r--r--engine/src/lighting.rs80
-rw-r--r--engine/src/material.rs165
-rw-r--r--engine/src/material/asset.rs82
-rw-r--r--engine/src/math.rs8
-rw-r--r--engine/src/mesh.rs554
-rw-r--r--engine/src/mesh/cube.rs702
-rw-r--r--engine/src/mesh/vertex_buffer.rs281
-rw-r--r--engine/src/model.rs112
-rw-r--r--engine/src/model/asset.rs120
-rw-r--r--engine/src/opengl/buffer.rs92
-rw-r--r--engine/src/opengl/debug.rs145
-rw-r--r--engine/src/opengl/mod.rs107
-rw-r--r--engine/src/opengl/shader.rs240
-rw-r--r--engine/src/opengl/texture.rs240
-rw-r--r--engine/src/opengl/util.rs30
-rw-r--r--engine/src/opengl/vertex_array.rs183
-rw-r--r--engine/src/performance.rs59
-rw-r--r--engine/src/projection.rs201
-rw-r--r--engine/src/reflection.rs2
-rw-r--r--engine/src/renderer.rs1
-rw-r--r--engine/src/renderer/opengl.rs719
-rw-r--r--engine/src/rendering.rs715
-rw-r--r--engine/src/rendering/backend.rs9
-rw-r--r--engine/src/rendering/backend/opengl.rs2060
-rw-r--r--engine/src/rendering/backend/opengl/glutin_compat.rs264
-rw-r--r--engine/src/rendering/backend/opengl/graphics_mesh.rs259
-rw-r--r--engine/src/rendering/blending.rs89
-rw-r--r--engine/src/rendering/main_render_pass.rs1006
-rw-r--r--engine/src/rendering/object.rs137
-rw-r--r--engine/src/rendering/shader.rs1404
-rw-r--r--engine/src/rendering/shader/cursor.rs372
-rw-r--r--engine/src/scene.rs8
-rw-r--r--engine/src/shader.rs186
-rw-r--r--engine/src/shader_preprocessor.rs607
-rw-r--r--engine/src/sky_box.rs11
-rw-r--r--engine/src/texture.rs247
-rw-r--r--engine/src/transform.rs48
-rw-r--r--engine/src/ui.rs2
-rw-r--r--engine/src/ui/dear_imgui.rs1081
-rw-r--r--engine/src/ui/view.rs2
-rw-r--r--engine/src/ui/view/transform_3d.rs128
-rw-r--r--engine/src/ui/view/world.rs2314
-rw-r--r--engine/src/util.rs344
-rw-r--r--engine/src/vertex.rs68
-rw-r--r--engine/src/window.rs318
-rw-r--r--engine/src/windowing.rs1169
-rw-r--r--engine/src/windowing/dpi.rs217
-rw-r--r--engine/src/windowing/keyboard.rs826
-rw-r--r--engine/src/windowing/monitor.rs50
-rw-r--r--engine/src/windowing/mouse.rs197
-rw-r--r--engine/src/windowing/window.rs291
-rw-r--r--engine/src/windowing/window/platform.rs12
-rw-r--r--engine/src/work_queue.rs77
72 files changed, 17596 insertions, 5224 deletions
diff --git a/engine/src/asset.rs b/engine/src/asset.rs
new file mode 100644
index 0000000..dd8aa37
--- /dev/null
+++ b/engine/src/asset.rs
@@ -0,0 +1,921 @@
+use std::any::{type_name, Any};
+use std::borrow::Cow;
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::convert::Infallible;
+use std::ffi::{OsStr, OsString};
+use std::fmt::{Debug, Display};
+use std::hash::{DefaultHasher, Hash, Hasher};
+use std::hint::cold_path;
+use std::marker::PhantomData;
+use std::path::{Path, PathBuf};
+use std::sync::mpsc::{
+ channel as mpsc_channel,
+ Receiver as MpscReceiver,
+ Sender as MpscSender,
+};
+use std::sync::Arc;
+
+use ecs::actions::Actions;
+
+use crate::ecs::pair::ChildOf;
+use crate::ecs::phase::{Phase, PRE_UPDATE as PRE_UPDATE_PHASE};
+use crate::ecs::sole::Single;
+use crate::ecs::{declare_entity, pair, Sole};
+use crate::work_queue::{Work, WorkQueue};
+
+declare_entity! {
+pub HANDLE_ASSETS_PHASE: (Phase, pair!(ChildOf, { *PRE_UPDATE_PHASE }));
+}
+
+/// Asset label.
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Label<'a>
+{
+ pub path: Cow<'a, Path>,
+ pub name: Option<Cow<'a, str>>,
+}
+
+impl Label<'_>
+{
+ pub fn to_owned(&self) -> LabelOwned
+ {
+ LabelOwned {
+ path: self.path.to_path_buf(),
+ name: self.name.as_ref().map(|name| name.to_string()),
+ }
+ }
+}
+
+impl<'a> From<&'a Path> for Label<'a>
+{
+ fn from(path: &'a Path) -> Self
+ {
+ Self { path: path.into(), name: None }
+ }
+}
+
+impl From<PathBuf> for Label<'_>
+{
+ fn from(path: PathBuf) -> Self
+ {
+ Self { path: path.into(), name: None }
+ }
+}
+
+impl<'a> From<&'a LabelOwned> for Label<'a>
+{
+ fn from(label: &'a LabelOwned) -> Self
+ {
+ Self {
+ path: (&label.path).into(),
+ name: label.name.as_ref().map(|name| Cow::Borrowed(name.as_str())),
+ }
+ }
+}
+
+impl Display for Label<'_>
+{
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
+ {
+ write!(formatter, "{}", self.path.display())?;
+
+ if let Some(name) = &self.name {
+ formatter.write_str("::")?;
+ formatter.write_str(&name)?;
+ }
+
+ Ok(())
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct LabelOwned
+{
+ pub path: PathBuf,
+ pub name: Option<String>,
+}
+
+impl LabelOwned
+{
+ pub fn to_label(&self) -> Label<'_>
+ {
+ Label {
+ path: (&self.path).into(),
+ name: self.name.as_ref().map(|name| Cow::Borrowed(name.as_str())),
+ }
+ }
+}
+
+impl Display for LabelOwned
+{
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
+ {
+ write!(formatter, "{}", self.path.display())?;
+
+ if let Some(name) = &self.name {
+ formatter.write_str("::")?;
+ formatter.write_str(&name)?;
+ }
+
+ Ok(())
+ }
+}
+
+#[derive(Debug, Sole)]
+pub struct Assets
+{
+ assets: Vec<StoredAsset>,
+ asset_lookup: RefCell<HashMap<LabelHash, LookupEntry>>,
+ importers: Vec<WrappedImporterFn>,
+ importer_lookup: HashMap<OsString, usize>,
+ import_work_queue: WorkQueue<ImportWorkUserData>,
+ import_work_msg_receiver: MpscReceiver<ImportWorkMessage>,
+ import_work_msg_sender: MpscSender<ImportWorkMessage>,
+ events: Events,
+}
+
+impl Assets
+{
+ pub fn with_capacity(capacity: usize) -> Self
+ {
+ let (import_work_msg_sender, import_work_msg_receiver) =
+ mpsc_channel::<ImportWorkMessage>();
+
+ Self {
+ assets: Vec::with_capacity(capacity),
+ asset_lookup: RefCell::new(HashMap::with_capacity(capacity)),
+ importers: Vec::new(),
+ importer_lookup: HashMap::new(),
+ import_work_queue: WorkQueue::new("asset_importing_work_queue"),
+ import_work_msg_receiver,
+ import_work_msg_sender,
+ events: Events::default(),
+ }
+ }
+
+ pub fn set_importer<'file_ext, AssetSettings, Err>(
+ &mut self,
+ file_extensions: impl IntoIterator<Item: Into<Cow<'file_ext, str>>>,
+ func: impl Fn(&mut Submitter<'_>, &Path, Option<&AssetSettings>) -> Result<(), Err>,
+ ) where
+ AssetSettings: 'static,
+ Err: std::error::Error + Send + Sync + 'static,
+ {
+ self.importers.push(WrappedImporterFn::new(func));
+
+ let importer_index = self.importers.len() - 1;
+
+ self.importer_lookup
+ .extend(file_extensions.into_iter().map(|file_ext| {
+ let file_ext: Cow<str> = file_ext.into();
+
+ (file_ext.into_owned().into(), importer_index)
+ }));
+ }
+
+ #[tracing::instrument(
+ skip_all,
+ fields(asset_label, asset_type = type_name::<Asset>())
+ )]
+ pub fn get<Asset: 'static + Send + Sync>(
+ &self,
+ handle: &Handle<Asset>,
+ ) -> Option<&Asset>
+ {
+ let asset_lookup = self.asset_lookup.borrow();
+
+ let LookupEntry::Occupied(asset_index, asset_label) =
+ asset_lookup.get(&handle.id.label_hash)?
+ else {
+ return None;
+ };
+
+ tracing::Span::current()
+ .record("asset_label", tracing::field::display(&asset_label));
+
+ let stored_asset = self.assets.get(*asset_index).expect("Not possible");
+
+ let Some(asset) = stored_asset.strong.downcast_ref::<Asset>() else {
+ tracing::error!("Wrong asset type");
+ return None;
+ };
+
+ Some(asset)
+ }
+
+ #[tracing::instrument(skip_all, fields(asset_type=type_name::<Asset>()))]
+ pub fn get_handle_to_loaded<'label, Asset: 'static + Send + Sync>(
+ &self,
+ label: impl Into<Label<'label>>,
+ ) -> Option<Handle<Asset>>
+ {
+ let label = label.into();
+
+ let label_hash = LabelHash::new(&label);
+
+ let asset_lookup = self.asset_lookup.borrow();
+
+ let LookupEntry::Occupied(asset_index, _) = asset_lookup.get(&label_hash)? else {
+ return None;
+ };
+
+ let stored_asset = self.assets.get(*asset_index).expect("Not possible");
+
+ if stored_asset.strong.downcast_ref::<Asset>().is_none() {
+ tracing::error!("Wrong asset type");
+ return None;
+ };
+
+ Some(Handle::new(label_hash))
+ }
+
+ pub fn is_loaded_and_has_type<Asset: 'static + Send + Sync>(
+ &self,
+ handle: &Handle<Asset>,
+ ) -> bool
+ {
+ let asset_lookup = self.asset_lookup.borrow();
+
+ let Some(LookupEntry::Occupied(asset_index, _)) =
+ asset_lookup.get(&handle.id.label_hash)
+ else {
+ return false;
+ };
+
+ let stored_asset = self.assets.get(*asset_index).expect("Not possible");
+
+ stored_asset.strong.downcast_ref::<Asset>().is_some()
+ }
+
+ pub fn get_label<Asset: 'static + Send + Sync>(
+ &self,
+ handle: &Handle<Asset>,
+ ) -> Option<LabelOwned>
+ {
+ let lookup_entry = self
+ .asset_lookup
+ .borrow()
+ .get(&handle.id.label_hash)?
+ .clone();
+
+ let LookupEntry::Occupied(_, label) = lookup_entry else {
+ return None;
+ };
+
+ Some(label)
+ }
+
+ pub fn get_label_by_id(&self, id: Id) -> Option<LabelOwned>
+ {
+ let lookup_entry = self.asset_lookup.borrow().get(&id.label_hash)?.clone();
+
+ let LookupEntry::Occupied(_, label) = lookup_entry else {
+ return None;
+ };
+
+ Some(label)
+ }
+
+ #[tracing::instrument(skip(self))]
+ pub fn load<'i, Asset: 'static + Send + Sync>(
+ &self,
+ label: impl Into<Label<'i>> + Debug,
+ ) -> Handle<Asset>
+ {
+ let label = label.into();
+
+ let label_hash = LabelHash::new(&label);
+
+ let mut asset_lookup = self.asset_lookup.borrow_mut();
+
+ if Self::is_pending(&asset_lookup, &label) {
+ return Handle::new(label_hash);
+ }
+
+ let Some(lookup_entry) = asset_lookup.get(&label_hash) else {
+ self.add_import_work::<Infallible>(
+ &label,
+ label_hash,
+ None,
+ &mut asset_lookup,
+ );
+
+ return Handle::new(label_hash);
+ };
+
+ match lookup_entry {
+ LookupEntry::Occupied(asset_index, _) => {
+ let stored_asset = self.assets.get(*asset_index).expect("Not possible");
+
+ if stored_asset.strong.downcast_ref::<Asset>().is_none() {
+ tracing::error!("Wrong asset type {}", type_name::<Asset>());
+ }
+ }
+ LookupEntry::Pending => {}
+ }
+
+ Handle::new(label_hash)
+ }
+
+ #[tracing::instrument(skip(self))]
+ pub fn load_with_settings<'i, Asset, AssetSettings>(
+ &self,
+ label: impl Into<Label<'i>> + Debug,
+ asset_settings: AssetSettings,
+ ) -> Handle<Asset>
+ where
+ Asset: Send + Sync + 'static,
+ AssetSettings: Send + Sync + Debug + 'static,
+ {
+ let label = label.into();
+
+ let label_hash = LabelHash::new(&label);
+
+ let mut asset_lookup = self.asset_lookup.borrow_mut();
+
+ if Self::is_pending(&asset_lookup, &label) {
+ return Handle::new(label_hash);
+ }
+
+ let Some(lookup_entry) = asset_lookup.get(&label_hash) else {
+ self.add_import_work::<AssetSettings>(
+ &label,
+ label_hash,
+ Some(asset_settings),
+ &mut asset_lookup,
+ );
+
+ return Handle::new(label_hash);
+ };
+
+ match lookup_entry {
+ LookupEntry::Occupied(asset_index, _) => {
+ let stored_asset = self.assets.get(*asset_index).expect("Not possible");
+
+ if stored_asset.strong.downcast_ref::<Asset>().is_none() {
+ tracing::error!(
+ "Wrong asset type {} for asset",
+ type_name::<Asset>()
+ );
+ }
+ }
+ LookupEntry::Pending => {}
+ }
+
+ Handle::new(label_hash)
+ }
+
+ pub fn store_with_name<'name, Asset: 'static + Send + Sync>(
+ &mut self,
+ name: impl Into<Cow<'name, str>>,
+ asset: Asset,
+ ) -> Handle<Asset>
+ {
+ self.store_with_label(
+ Label {
+ path: Path::new("").into(),
+ name: Some(name.into()),
+ },
+ asset,
+ )
+ }
+
+ pub fn store_with_name_with<'name, Asset: 'static + Send + Sync>(
+ &mut self,
+ name: impl Into<Cow<'name, str>>,
+ func: impl FnOnce(&mut Self) -> Asset,
+ ) -> Handle<Asset>
+ {
+ let asset = func(self);
+
+ self.store_with_label(
+ Label {
+ path: Path::new("").into(),
+ name: Some(name.into()),
+ },
+ asset,
+ )
+ }
+
+ #[tracing::instrument(skip(self, asset), fields(asset_type=type_name::<Asset>()))]
+ pub fn store_with_label<'i, Asset: 'static + Send + Sync>(
+ &mut self,
+ label: impl Into<Label<'i>> + Debug,
+ asset: Asset,
+ ) -> Handle<Asset>
+ {
+ let label = label.into();
+
+ let label_hash = LabelHash::new(&label);
+
+ if matches!(
+ self.asset_lookup.get_mut().get(&label_hash),
+ Some(LookupEntry::Occupied(_, _))
+ ) {
+ tracing::error!("Asset already exists");
+
+ return Handle::new(label_hash);
+ }
+
+ tracing::debug!("Storing asset");
+
+ self.assets.push(StoredAsset::new(asset));
+
+ let index = self.assets.len() - 1;
+
+ self.asset_lookup
+ .get_mut()
+ .insert(label_hash, LookupEntry::Occupied(index, label.to_owned()));
+
+ self.events
+ .curr_tick_events
+ .push(Event::Stored(Id { label_hash }, label.to_owned()));
+
+ Handle::new(label_hash)
+ }
+
+ pub fn events(&self) -> &Events
+ {
+ &self.events
+ }
+
+ fn is_pending(asset_lookup: &HashMap<LabelHash, LookupEntry>, label: &Label) -> bool
+ {
+ if label.name.is_some() {
+ if let Some(LookupEntry::Pending) =
+ asset_lookup.get(&LabelHash::new(&Label {
+ path: label.path.as_ref().into(),
+ name: None,
+ }))
+ {
+ return true;
+ }
+ }
+
+ if let Some(LookupEntry::Pending) = asset_lookup.get(&LabelHash::new(label)) {
+ return true;
+ };
+
+ false
+ }
+
+ fn add_import_work<AssetSettings>(
+ &self,
+ label: &Label<'_>,
+ label_hash: LabelHash,
+ asset_settings: Option<AssetSettings>,
+ asset_lookup: &mut HashMap<LabelHash, LookupEntry>,
+ ) where
+ AssetSettings: Any + Send + Sync,
+ {
+ let Some(file_ext) = label.path.extension() else {
+ tracing::error!("Asset file is missing a file extension");
+ return;
+ };
+
+ let Some(importer) = self.get_importer(file_ext) else {
+ tracing::error!(
+ "No importer exists for asset file extension {}",
+ file_ext.to_string_lossy()
+ );
+ return;
+ };
+
+ self.import_work_queue.add_work(Work {
+ func: |ImportWorkUserData {
+ import_work_msg_sender,
+ asset_path,
+ asset_settings,
+ importer,
+ }| {
+ if let Err(err) = importer.call(
+ import_work_msg_sender,
+ asset_path.as_path(),
+ asset_settings.as_deref(),
+ ) {
+ tracing::error!(
+ "Failed to import asset {}: {:#}",
+ asset_path.display(),
+ crate::Error::new(err)
+ );
+ }
+ },
+ user_data: ImportWorkUserData {
+ import_work_msg_sender: self.import_work_msg_sender.clone(),
+ asset_path: label.path.to_path_buf(),
+ asset_settings: asset_settings.map(|asset_settings| {
+ Box::new(asset_settings) as Box<dyn Any + Send + Sync>
+ }),
+ importer: importer.clone(),
+ },
+ });
+
+ asset_lookup.insert(label_hash, LookupEntry::Pending);
+
+ if label.name.is_some() {
+ asset_lookup.insert(
+ LabelHash::new(&Label {
+ path: label.path.as_ref().into(),
+ name: None,
+ }),
+ LookupEntry::Pending,
+ );
+ }
+ }
+
+ fn get_importer(&self, file_ext: &OsStr) -> Option<&WrappedImporterFn>
+ {
+ let index = *self.importer_lookup.get(file_ext)?;
+
+ Some(self.importers.get(index).expect("Not possible"))
+ }
+}
+
+impl Default for Assets
+{
+ fn default() -> Self
+ {
+ Self::with_capacity(0)
+ }
+}
+
+pub struct Submitter<'path>
+{
+ import_work_msg_sender: MpscSender<ImportWorkMessage>,
+ asset_path: &'path Path,
+}
+
+impl Submitter<'_>
+{
+ pub fn submit_load_other<'label, Asset: Send + Sync + 'static>(
+ &self,
+ label: impl Into<Label<'label>>,
+ ) -> Handle<Asset>
+ {
+ let label = label.into();
+
+ let _ = self.import_work_msg_sender.send(ImportWorkMessage::Load {
+ do_load: |assets, label, _asset_settings| {
+ let _ = assets.load::<Asset>(label);
+ },
+ label: label.to_owned(),
+ asset_settings: None,
+ });
+
+ Handle::new(LabelHash::new(&label))
+ }
+
+ pub fn submit_load_other_with_settings<'label, Asset, AssetSettings>(
+ &self,
+ label: impl Into<Label<'label>>,
+ asset_settings: AssetSettings,
+ ) -> Handle<Asset>
+ where
+ Asset: Send + Sync + 'static,
+ AssetSettings: Send + Sync + Debug + 'static,
+ {
+ let label = label.into();
+
+ let _ = self.import_work_msg_sender.send(ImportWorkMessage::Load {
+ do_load: |assets, label, asset_settings| {
+ let asset_settings = *asset_settings
+ .expect("Not possible")
+ .downcast::<AssetSettings>()
+ .expect("Not possible");
+
+ let _ = assets
+ .load_with_settings::<Asset, AssetSettings>(label, asset_settings);
+ },
+ label: label.to_owned(),
+ asset_settings: Some(Box::new(asset_settings)),
+ });
+
+ Handle::new(LabelHash::new(&label))
+ }
+
+ pub fn submit_store<Asset: Send + Sync + 'static>(
+ &self,
+ asset: Asset,
+ ) -> Handle<Asset>
+ {
+ let label = LabelOwned {
+ path: self.asset_path.into(),
+ name: None,
+ };
+
+ let label_hash = LabelHash::new(&label.to_label());
+
+ let _ = self.import_work_msg_sender.send(ImportWorkMessage::Store {
+ do_store: |assets, label, boxed_asset| {
+ let Ok(asset) = boxed_asset.downcast::<Asset>() else {
+ unreachable!();
+ };
+
+ assets.store_with_label::<Asset>(&label, *asset);
+ },
+ label,
+ asset: Box::new(asset),
+ });
+
+ Handle::new(label_hash)
+ }
+
+ pub fn submit_store_named<Asset: Send + Sync + 'static>(
+ &self,
+ name: impl AsRef<str>,
+ asset: Asset,
+ ) -> Handle<Asset>
+ {
+ let label = LabelOwned {
+ path: self.asset_path.into(),
+ name: Some(name.as_ref().into()),
+ };
+
+ let label_hash = LabelHash::new(&label.to_label());
+
+ let _ = self.import_work_msg_sender.send(ImportWorkMessage::Store {
+ do_store: |assets, label, boxed_asset| {
+ let Ok(asset) = boxed_asset.downcast::<Asset>() else {
+ unreachable!();
+ };
+
+ assets.store_with_label::<Asset>(&label, *asset);
+ },
+ label,
+ asset: Box::new(asset),
+ });
+
+ Handle::new(label_hash)
+ }
+}
+
+/// Asset handle.
+#[derive(Debug)]
+pub struct Handle<Asset: 'static>
+{
+ id: Id,
+ _pd: PhantomData<Asset>,
+}
+
+impl<Asset: 'static> Handle<Asset>
+{
+ pub fn from_id(id: Id) -> Self
+ {
+ Self { id, _pd: PhantomData }
+ }
+
+ pub fn id(&self) -> Id
+ {
+ self.id
+ }
+
+ fn new(label_hash: LabelHash) -> Self
+ {
+ Self {
+ id: Id { label_hash },
+ _pd: PhantomData,
+ }
+ }
+}
+
+impl<Asset: 'static> Clone for Handle<Asset>
+{
+ fn clone(&self) -> Self
+ {
+ Self { id: self.id, _pd: PhantomData }
+ }
+}
+
+/// Asset ID.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Id
+{
+ label_hash: LabelHash,
+}
+
+#[derive(Debug, Default)]
+pub struct Events
+{
+ curr_tick_events: Vec<Event>,
+ last_tick_events: Vec<Event>,
+}
+
+impl Events
+{
+ pub fn last_tick_events(&self) -> impl Iterator<Item = &Event>
+ {
+ self.last_tick_events.iter()
+ }
+}
+
+/// Asset event.
+#[derive(Debug)]
+pub enum Event
+{
+ /// Asset stored.
+ Stored(Id, LabelOwned),
+}
+
+#[derive(Debug, thiserror::Error)]
+enum ImporterError
+{
+ #[error("Settings has a incorrect type")]
+ IncorrectAssetSettingsType(PathBuf),
+
+ #[error(transparent)]
+ Other(Box<dyn std::error::Error + Send + Sync>),
+}
+
+#[derive(Debug, Clone)]
+struct WrappedImporterFn
+{
+ wrapper_func: fn(
+ MpscSender<ImportWorkMessage>,
+ &Path,
+ Option<&(dyn Any + Send + Sync)>,
+ ) -> Result<(), ImporterError>,
+}
+
+impl WrappedImporterFn
+{
+ fn new<InnerFunc, AssetSettings, Err>(inner_func_param: InnerFunc) -> Self
+ where
+ InnerFunc:
+ Fn(&mut Submitter<'_>, &Path, Option<&AssetSettings>) -> Result<(), Err>,
+ AssetSettings: 'static,
+ Err: std::error::Error + Send + Sync + 'static,
+ {
+ assert_eq!(size_of::<InnerFunc>(), 0);
+
+ let wrapper_func =
+ |import_work_msg_sender: MpscSender<ImportWorkMessage>,
+ asset_path: &Path,
+ asset_settings: Option<&(dyn Any + Send + Sync)>| {
+ let inner_func = unsafe { std::mem::zeroed::<InnerFunc>() };
+
+ let asset_settings = asset_settings
+ .map(|asset_settings| {
+ asset_settings
+ .downcast_ref::<AssetSettings>()
+ .ok_or_else(|| {
+ ImporterError::IncorrectAssetSettingsType(
+ asset_path.to_path_buf(),
+ )
+ })
+ })
+ .transpose()?;
+
+ inner_func(
+ &mut Submitter { import_work_msg_sender, asset_path },
+ asset_path,
+ asset_settings,
+ )
+ .map_err(|err| ImporterError::Other(Box::new(err)))?;
+
+ Ok(())
+ };
+
+ std::mem::forget(inner_func_param);
+
+ Self { wrapper_func }
+ }
+
+ fn call(
+ &self,
+ import_work_msg_sender: MpscSender<ImportWorkMessage>,
+ asset_path: &Path,
+ asset_settings: Option<&(dyn Any + Send + Sync)>,
+ ) -> Result<(), ImporterError>
+ {
+ (self.wrapper_func)(import_work_msg_sender, asset_path, asset_settings)
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+struct LabelHash(u64);
+
+impl LabelHash
+{
+ fn new(label: &Label<'_>) -> Self
+ {
+ let mut hasher = DefaultHasher::new();
+
+ label.hash(&mut hasher);
+
+ Self(hasher.finish())
+ }
+}
+
+#[derive(Debug, Default)]
+pub(crate) struct Extension
+{
+ pub assets: Assets,
+}
+
+impl crate::ecs::extension::Extension for Extension
+{
+ fn collect(self, mut collector: crate::ecs::extension::Collector<'_>)
+ {
+ let _ = collector.add_sole(self.assets);
+
+ collector.spawn_declared_entity(&HANDLE_ASSETS_PHASE);
+
+ collector.add_system(*HANDLE_ASSETS_PHASE, add_received_assets);
+ collector.add_system(*HANDLE_ASSETS_PHASE, check_import_wq_thread_not_panicked);
+ }
+}
+
+fn add_received_assets(mut assets: Single<Assets>)
+{
+ let Ok(assets) = assets.get_mut() else {
+ unreachable!();
+ };
+
+ let Events { curr_tick_events, last_tick_events } = &mut assets.events;
+
+ std::mem::swap(last_tick_events, curr_tick_events);
+
+ curr_tick_events.clear();
+
+ while let Some(import_work_msg) = assets.import_work_msg_receiver.try_recv().ok() {
+ match import_work_msg {
+ ImportWorkMessage::Store { do_store, label, asset } => {
+ do_store(assets, label, asset);
+ }
+ ImportWorkMessage::Load { do_load, label, asset_settings } => {
+ do_load(
+ &assets,
+ Label {
+ path: label.path.as_path().into(),
+ name: label.name.as_deref().map(|name| name.into()),
+ },
+ asset_settings,
+ );
+ }
+ }
+ }
+}
+
+fn check_import_wq_thread_not_panicked(assets: Single<Assets>, mut actions: Actions<'_>)
+{
+ let Ok(assets) = assets.get() else {
+ unreachable!();
+ };
+
+ if assets.import_work_queue.get_thread_panic().is_some() {
+ cold_path();
+
+ actions.stop();
+ }
+}
+
+#[derive(Debug)]
+struct ImportWorkUserData
+{
+ import_work_msg_sender: MpscSender<ImportWorkMessage>,
+ asset_path: PathBuf,
+ asset_settings: Option<Box<dyn Any + Send + Sync>>,
+ importer: WrappedImporterFn,
+}
+
+#[derive(Debug)]
+enum ImportWorkMessage
+{
+ Store
+ {
+ do_store: fn(&mut Assets, LabelOwned, Box<dyn Any + Send + Sync>),
+ label: LabelOwned,
+ asset: Box<dyn Any + Send + Sync>,
+ },
+
+ Load
+ {
+ do_load: fn(&Assets, Label<'_>, Option<Box<dyn Any + Send + Sync>>),
+ label: LabelOwned,
+ asset_settings: Option<Box<dyn Any + Send + Sync>>,
+ },
+}
+
+#[derive(Debug, Clone)]
+enum LookupEntry
+{
+ Occupied(usize, LabelOwned),
+ Pending,
+}
+
+#[derive(Debug)]
+struct StoredAsset
+{
+ strong: Arc<dyn Any + Send + Sync>,
+}
+
+impl StoredAsset
+{
+ fn new<Asset: Any + Send + Sync>(asset: Asset) -> Self
+ {
+ let strong = Arc::new(asset);
+
+ Self { strong }
+ }
+}
diff --git a/engine/src/camera.rs b/engine/src/camera.rs
index 66150af..51cd54c 100644
--- a/engine/src/camera.rs
+++ b/engine/src/camera.rs
@@ -1,11 +1,12 @@
-use ecs::Component;
-
+use crate::ecs::Component;
+use crate::matrix::Matrix;
use crate::projection::{Perspective, Projection};
+use crate::reflection::Reflection;
use crate::vector::Vec3;
pub mod fly;
-#[derive(Debug, Component)]
+#[derive(Debug, Clone, Component, Reflection)]
pub struct Camera
{
pub target: Vec3<f32>,
@@ -13,12 +14,24 @@ pub struct Camera
pub projection: Projection,
}
+impl Camera
+{
+ pub fn to_view_matrix(&self, camera_world_pos: Vec3<f32>) -> Matrix<f32, 4, 4>
+ {
+ let mut view = Matrix::new();
+
+ view.look_at(camera_world_pos, self.target, self.global_up);
+
+ view
+ }
+}
+
impl Default for Camera
{
fn default() -> Self
{
Self {
- target: Vec3::default(),
+ target: Vec3 { x: 0.0, y: 0.0, z: 0.0 },
global_up: Vec3::UP,
projection: Projection::Perspective(Perspective::default()),
}
@@ -26,5 +39,20 @@ impl Default for Camera
}
/// Marker component for cameras that are active.
-#[derive(Debug, Default, Clone, Copy, Component)]
+#[derive(Debug, Default, Clone, Copy, Component, Reflection)]
pub struct Active;
+
+/// Cameras that can be controlled have this component.
+#[derive(Debug, Clone, Copy, Component, Reflection)]
+pub struct Controllable
+{
+ pub control_enabled: bool,
+}
+
+impl Default for Controllable
+{
+ fn default() -> Self
+ {
+ Self { control_enabled: true }
+ }
+}
diff --git a/engine/src/camera/fly.rs b/engine/src/camera/fly.rs
index b6ba7aa..5f5ab4b 100644
--- a/engine/src/camera/fly.rs
+++ b/engine/src/camera/fly.rs
@@ -1,25 +1,28 @@
-use ecs::component::local::Local;
-use ecs::sole::Single;
-use ecs::system::{Into, System};
-use ecs::{Component, Query};
-use glfw::window::{Key, KeyState};
-
-use crate::camera::{Active as ActiveCamera, Camera};
-use crate::delta_time::DeltaTime;
-use crate::event::Update as UpdateEvent;
-use crate::input::{Cursor, CursorFlags, Keys};
-use crate::transform::Position;
-use crate::util::builder;
-use crate::vector::{Vec2, Vec3};
+use ecs::time::Time;
+
+use crate::builder;
+use crate::camera::{Active as ActiveCamera, Camera, Controllable as ControllableCamera};
+use crate::ecs::actions::Actions;
+use crate::ecs::component::local::Local;
+use crate::ecs::phase::UPDATE as UPDATE_PHASE;
+use crate::ecs::query::term::With;
+use crate::ecs::sole::Single;
+use crate::ecs::system::initializable::Initializable;
+use crate::ecs::system::Into;
+use crate::ecs::{Component, Query};
+use crate::input::keyboard::{Key, Keyboard};
+use crate::input::mouse::Mouse;
+use crate::reflection::Reflection;
+use crate::transform::Transform;
+use crate::vector::{Angles, Vec2, Vec3};
builder! {
/// A fly camera.
#[builder(name = Builder, derives = (Debug))]
-#[derive(Debug, Component)]
+#[derive(Debug, Component, Reflection)]
#[non_exhaustive]
pub struct Fly {
- pub current_pitch: f64,
- pub current_yaw: f64,
+ pub angles: Angles<f32>,
pub speed: f32,
}
}
@@ -46,8 +49,7 @@ impl Default for Builder
fn default() -> Self
{
Self {
- current_yaw: -90.0,
- current_pitch: 0.0,
+ angles: Vec3::BACK.into_deg_angles(),
speed: 3.0,
}
}
@@ -56,16 +58,11 @@ impl Default for Builder
/// Fly camera extension.
pub struct Extension(pub Options);
-impl ecs::extension::Extension for Extension
+impl crate::ecs::extension::Extension for Extension
{
- fn collect(self, mut collector: ecs::extension::Collector<'_>)
+ fn collect(self, mut collector: crate::ecs::extension::Collector<'_>)
{
- collector.add_system(
- UpdateEvent,
- update
- .into_system()
- .initialize((CursorState::default(), self.0)),
- );
+ collector.add_system(*UPDATE_PHASE, update.into_system().initialize((self.0,)));
}
}
@@ -76,82 +73,81 @@ pub struct Options
}
fn update(
- camera_query: Query<(Camera, Position, Fly, ActiveCamera)>,
- keys: Single<Keys>,
- cursor: Single<Cursor>,
- cursor_flags: Single<CursorFlags>,
- delta_time: Single<DeltaTime>,
- mut cursor_state: Local<CursorState>,
+ camera_query: Query<(
+ &mut Camera,
+ &mut Transform,
+ &mut Fly,
+ Option<&ControllableCamera>,
+ With<ActiveCamera>,
+ )>,
+ keyboard: Single<Keyboard>,
+ mouse: Single<Mouse>,
+ time: Single<Time>,
options: Local<Options>,
-)
+ mut actions: Actions,
+) -> Result<(), crate::Error>
{
- for (mut camera, mut camera_pos, mut fly_camera, _) in &camera_query {
- if cursor.has_moved && cursor_flags.is_first_move.flag {
- #[cfg(feature = "debug")]
- tracing::debug!("First cursor move");
+ let keyboard = keyboard.get()?;
+ let mouse = mouse.get()?;
- cursor_state.last_pos = cursor.position;
+ for (
+ camera_ent_id,
+ (mut camera, mut camera_transform, mut fly_camera, controllable_camera),
+ ) in camera_query.iter_with_euids()
+ {
+ if let Some(controllable_camera) = controllable_camera {
+ if !controllable_camera.control_enabled {
+ continue;
+ }
+ } else {
+ actions.add_components(camera_ent_id, (ControllableCamera::default(),));
}
- let delta_time = delta_time.duration;
-
- let mut x_offset = cursor.position.x - cursor_state.last_pos.x;
- let mut y_offset = cursor_state.last_pos.y - cursor.position.y;
-
- cursor_state.last_pos = cursor.position;
+ if mouse.curr_tick_position_delta != (Vec2 { x: 0.0, y: 0.0 }) {
+ let x_offset =
+ mouse.curr_tick_position_delta.x * f64::from(options.mouse_sensitivity);
- x_offset *= f64::from(options.mouse_sensitivity);
- y_offset *= f64::from(options.mouse_sensitivity);
+ let y_offset = (-mouse.curr_tick_position_delta.y)
+ * f64::from(options.mouse_sensitivity);
- fly_camera.current_yaw += x_offset;
- fly_camera.current_pitch += y_offset;
+ fly_camera.angles.yaw += x_offset as f32;
+ fly_camera.angles.pitch += y_offset as f32;
- fly_camera.current_pitch = fly_camera.current_pitch.clamp(-89.0, 89.0);
-
- // TODO: This casting to a f32 from a f64 is horrible. fix it
- #[allow(clippy::cast_possible_truncation)]
- let direction = Vec3 {
- x: (fly_camera.current_yaw.to_radians().cos()
- * fly_camera.current_pitch.to_radians().cos()) as f32,
- y: fly_camera.current_pitch.to_radians().sin() as f32,
- z: (fly_camera.current_yaw.to_radians().sin()
- * fly_camera.current_pitch.to_radians().cos()) as f32,
+ fly_camera.angles.pitch = fly_camera.angles.pitch.clamp(-89.0, 89.0);
}
- .normalize();
+
+ let direction = Vec3::direction_from_deg_angles(fly_camera.angles);
let cam_right = direction.cross(&Vec3::UP).normalize();
camera.global_up = cam_right.cross(&direction).normalize();
- if matches!(keys.get_key_state(Key::W), KeyState::Pressed) {
- camera_pos.position +=
- direction * fly_camera.speed * delta_time.as_secs_f32();
+ if keyboard.pressed(Key::W) {
+ camera_transform.position +=
+ direction * fly_camera.speed * time.delta_time.as_secs_f32();
}
- if matches!(keys.get_key_state(Key::S), KeyState::Pressed) {
- camera_pos.position -=
- direction * fly_camera.speed * delta_time.as_secs_f32();
+ if keyboard.pressed(Key::S) {
+ camera_transform.position -=
+ direction * fly_camera.speed * time.delta_time.as_secs_f32();
}
- if matches!(keys.get_key_state(Key::A), KeyState::Pressed) {
+ if keyboard.pressed(Key::A) {
let cam_left = -direction.cross(&Vec3::UP).normalize();
- camera_pos.position += cam_left * fly_camera.speed * delta_time.as_secs_f32();
+ camera_transform.position +=
+ cam_left * fly_camera.speed * time.delta_time.as_secs_f32();
}
- if matches!(keys.get_key_state(Key::D), KeyState::Pressed) {
+ if keyboard.pressed(Key::D) {
let cam_right = direction.cross(&Vec3::UP).normalize();
- camera_pos.position +=
- cam_right * fly_camera.speed * delta_time.as_secs_f32();
+ camera_transform.position +=
+ cam_right * fly_camera.speed * time.delta_time.as_secs_f32();
}
- camera.target = camera_pos.position + direction;
+ camera.target = camera_transform.position + direction;
}
-}
-#[derive(Debug, Default, Component)]
-struct CursorState
-{
- last_pos: Vec2<f64>,
+ Ok(())
}
diff --git a/engine/src/collision.rs b/engine/src/collision.rs
new file mode 100644
index 0000000..015ab58
--- /dev/null
+++ b/engine/src/collision.rs
@@ -0,0 +1,142 @@
+use crate::ecs::Component;
+use crate::mesh::Mesh;
+use crate::reflection::Reflection;
+use crate::vector::Vec3;
+
+pub trait Collider<Other>
+{
+ fn intersects(&self, other: &Other) -> bool;
+}
+
+#[derive(Debug, Default, Clone, Component, Reflection)]
+#[non_exhaustive]
+pub struct BoxCollider
+{
+ pub min: Vec3<f32>,
+ pub max: Vec3<f32>,
+}
+
+impl BoxCollider
+{
+ pub fn for_mesh(mesh: &Mesh) -> Self
+ {
+ let furthest_dir_points = mesh.find_furthest_vertex_positions();
+
+ Self {
+ min: Vec3 {
+ x: furthest_dir_points.left.x,
+ y: furthest_dir_points.down.y,
+ z: furthest_dir_points.back.z,
+ },
+ max: Vec3 {
+ x: furthest_dir_points.right.x,
+ y: furthest_dir_points.up.y,
+ z: furthest_dir_points.front.z,
+ },
+ }
+ }
+
+ pub fn offset(self, offset: Vec3<f32>) -> Self
+ {
+ Self {
+ min: self.min + offset,
+ max: self.max + offset,
+ }
+ }
+}
+
+impl Collider<BoxCollider> for BoxCollider
+{
+ fn intersects(&self, other: &BoxCollider) -> bool
+ {
+ self.min.x <= other.max.x
+ && self.max.x >= other.min.x
+ && self.min.y <= other.max.y
+ && self.max.y >= other.min.y
+ && self.min.z <= other.max.z
+ && self.max.z >= other.min.z
+ }
+}
+
+impl Collider<SphereCollider> for BoxCollider
+{
+ fn intersects(&self, other: &SphereCollider) -> bool
+ {
+ other.intersects(self)
+ }
+}
+
+impl Collider<Vec3<f32>> for BoxCollider
+{
+ fn intersects(&self, other: &Vec3<f32>) -> bool
+ {
+ other.x >= self.min.x
+ && other.y >= self.min.y
+ && other.z >= self.min.z
+ && other.x <= self.max.x
+ && other.y <= self.max.y
+ && other.z <= self.max.z
+ }
+}
+
+#[derive(Debug, Default, Clone, Component, Reflection)]
+pub struct SphereCollider
+{
+ pub center: Vec3<f32>,
+ pub radius: f32,
+}
+
+impl SphereCollider
+{
+ pub fn offset(self, offset: Vec3<f32>) -> Self
+ {
+ Self {
+ center: self.center + offset,
+ radius: self.radius,
+ }
+ }
+}
+
+impl Collider<SphereCollider> for SphereCollider
+{
+ fn intersects(&self, other: &SphereCollider) -> bool
+ {
+ (self.center - other.center).length() <= self.radius + other.radius
+ }
+}
+
+impl Collider<BoxCollider> for SphereCollider
+{
+ fn intersects(&self, other: &BoxCollider) -> bool
+ {
+ let mut min_distance = 0.0;
+
+ if self.center.x < other.min.x {
+ min_distance += (self.center.x - other.min.x).powf(2.0);
+ } else if self.center.x > other.max.x {
+ min_distance += (self.center.x - other.max.x).powf(2.0);
+ }
+
+ if self.center.y < other.min.y {
+ min_distance += (self.center.y - other.min.y).powf(2.0);
+ } else if self.center.y > other.max.y {
+ min_distance += (self.center.y - other.max.y).powf(2.0);
+ }
+
+ if self.center.z < other.min.z {
+ min_distance += (self.center.z - other.min.z).powf(2.0);
+ } else if self.center.z > other.max.z {
+ min_distance += (self.center.z - other.max.z).powf(2.0);
+ }
+
+ min_distance <= self.radius.powf(2.0)
+ }
+}
+
+impl Collider<Vec3<f32>> for SphereCollider
+{
+ fn intersects(&self, other: &Vec3<f32>) -> bool
+ {
+ (self.center - *other).length() <= self.radius
+ }
+}
diff --git a/engine/src/data_types/color.rs b/engine/src/data_types/color.rs
index cef3b92..d2fd217 100644
--- a/engine/src/data_types/color.rs
+++ b/engine/src/data_types/color.rs
@@ -1,59 +1,145 @@
-use std::ops::{Add, Div, Mul, Neg, Sub};
+use std::ops::{Add, Div, Mul, Sub};
-#[derive(Debug, Clone, Default)]
-#[repr(C)]
-pub struct Color<Value>
+use crate::reflection::Reflection;
+
+pub trait Pixel: sealed::Sealed
{
- pub red: Value,
- pub green: Value,
- pub blue: Value,
+ type Component: PixelComponent;
}
-impl Color<f32>
+pub trait PixelComponent: Clone + Copy + sealed::Sealed
{
- pub const WHITE_F32: Self = Self { red: 1.0, green: 1.0, blue: 1.0 };
+ const COLOR_MAX: Self;
+}
+
+macro_rules! gen_color {
+ ($ident: ident, components=($($component: ident),+)) => {
+ #[derive(Debug, Clone, Default, Reflection)]
+ #[reflection(impl_with_generics(<u8>, <u16>, <u32>, <f32>))]
+ pub struct $ident<Component>
+ {
+ $(pub $component: Component),+
+ }
+
+ impl<Component: PixelComponent> $ident<Component>
+ {
+ pub fn white() -> Self
+ {
+ Self {
+ $($component: Component::COLOR_MAX),*
+ }
+ }
+ }
+
+ impl<Component: PixelComponent> Pixel for $ident<Component>
+ {
+ type Component = Component;
+ }
+
+ impl<Component> sealed::Sealed for $ident<Component> {}
+ };
}
-impl Color<u8>
+gen_color!(Rgb, components = (r, g, b));
+gen_color!(Rgba, components = (r, g, b, a));
+
+#[derive(Debug, Clone, Reflection)]
+#[reflection(impl_with_generics(<u8>, <u16>, <u32>, <f32>))]
+pub enum Color<Component>
{
- pub const WHITE_U8: Self = Self { red: 255, green: 255, blue: 255 };
+ Rgb(Rgb<Component>),
+ Rgba(Rgba<Component>),
}
-impl<Value: Clone> From<Value> for Color<Value>
+impl<Component: PixelComponent> Color<Component>
{
- fn from(value: Value) -> Self
+ pub fn to_rgb_lossy(&self) -> Rgb<Component>
+ {
+ match self {
+ Self::Rgb(rgb) => rgb.clone(),
+ Self::Rgba(rgba) => Rgb { r: rgba.r, g: rgba.g, b: rgba.b },
+ }
+ }
+
+ pub fn to_rgba_lossy(&self) -> Rgba<Component>
{
- Self {
- red: value.clone(),
- green: value.clone(),
- blue: value,
+ match self {
+ Self::Rgb(rgb) => Rgba {
+ r: rgb.r,
+ g: rgb.g,
+ b: rgb.b,
+ a: Component::COLOR_MAX,
+ },
+ Self::Rgba(rgba) => rgba.clone(),
}
}
}
-macro_rules! impl_math_op {
- ($math_op_trait: ident, $function: ident) => {
- impl<Value> $math_op_trait for Color<Value>
+impl<Component> From<Rgb<Component>> for Color<Component>
+{
+ fn from(rgb: Rgb<Component>) -> Self
+ {
+ Self::Rgb(rgb)
+ }
+}
+
+impl<Component> From<Rgba<Component>> for Color<Component>
+{
+ fn from(rgba: Rgba<Component>) -> Self
+ {
+ Self::Rgba(rgba)
+ }
+}
+
+impl<Component: Default> Default for Color<Component>
+{
+ fn default() -> Self
+ {
+ Self::Rgb(Rgb::default())
+ }
+}
+
+macro_rules! gen_scalar_math_op_impl {
+ (
+ $ident: ident,
+ $math_op_trait: ident,
+ $function: ident,
+ components=($($component: ident),+)
+ ) => {
+ impl<Value> $math_op_trait<Value> for $ident<Value>
where
- Value: $math_op_trait<Output = Value>,
+ Value: $math_op_trait<Output = Value> + Clone,
{
type Output = Self;
- fn $function(self, rhs: Self) -> Self::Output
+ fn $function(self, rhs: Value) -> Self::Output
{
Self {
- red: self.red.$function(rhs.red),
- green: self.green.$function(rhs.green),
- blue: self.blue.$function(rhs.blue),
+ $($component: self.$component.$function(rhs.clone())),+
}
}
}
};
}
-macro_rules! impl_scalar_math_op {
- ($math_op_trait: ident, $function: ident) => {
- impl<Value> $math_op_trait<Value> for Color<Value>
+gen_scalar_math_op_impl!(Rgb, Add, add, components = (r, g, b));
+gen_scalar_math_op_impl!(Rgb, Sub, sub, components = (r, g, b));
+gen_scalar_math_op_impl!(Rgb, Mul, mul, components = (r, g, b));
+gen_scalar_math_op_impl!(Rgb, Div, div, components = (r, g, b));
+
+gen_scalar_math_op_impl!(Rgba, Add, add, components = (r, g, b, a));
+gen_scalar_math_op_impl!(Rgba, Sub, sub, components = (r, g, b, a));
+gen_scalar_math_op_impl!(Rgba, Mul, mul, components = (r, g, b, a));
+gen_scalar_math_op_impl!(Rgba, Div, div, components = (r, g, b, a));
+
+macro_rules! gen_enum_scalar_math_op_impl {
+ (
+ $ident: ident,
+ $math_op_trait: ident,
+ $function: ident,
+ variants=($($variant: ident),+)
+ ) => {
+ impl<Value> $math_op_trait<Value> for $ident<Value>
where
Value: $math_op_trait<Output = Value> + Clone,
{
@@ -61,38 +147,44 @@ macro_rules! impl_scalar_math_op {
fn $function(self, rhs: Value) -> Self::Output
{
- Self {
- red: self.red.$function(rhs.clone()),
- green: self.green.$function(rhs.clone()),
- blue: self.blue.$function(rhs),
+ match self {
+ $(
+ Self::$variant(inner) =>
+ Self::$variant(inner.$function(rhs.clone()))
+ ),+
}
}
}
};
}
-impl_math_op!(Add, add);
-impl_math_op!(Sub, sub);
-impl_math_op!(Mul, mul);
-impl_math_op!(Div, div);
+gen_enum_scalar_math_op_impl!(Color, Add, add, variants = (Rgb, Rgba));
+gen_enum_scalar_math_op_impl!(Color, Sub, sub, variants = (Rgb, Rgba));
+gen_enum_scalar_math_op_impl!(Color, Mul, mul, variants = (Rgb, Rgba));
+gen_enum_scalar_math_op_impl!(Color, Div, div, variants = (Rgb, Rgba));
-impl_scalar_math_op!(Add, add);
-impl_scalar_math_op!(Sub, sub);
-impl_scalar_math_op!(Mul, mul);
-impl_scalar_math_op!(Div, div);
+impl PixelComponent for u8
+{
+ const COLOR_MAX: Self = u8::MAX;
+}
-impl<Value> Neg for Color<Value>
-where
- Value: Neg<Output = Value>,
+impl sealed::Sealed for u8 {}
+
+impl PixelComponent for u16
{
- type Output = Self;
+ const COLOR_MAX: Self = u16::MAX;
+}
- fn neg(self) -> Self::Output
- {
- Self {
- red: -self.red,
- green: -self.green,
- blue: -self.blue,
- }
- }
+impl sealed::Sealed for u16 {}
+
+impl PixelComponent for f32
+{
+ const COLOR_MAX: Self = 1.0;
+}
+
+impl sealed::Sealed for f32 {}
+
+mod sealed
+{
+ pub trait Sealed {}
}
diff --git a/engine/src/data_types/dimens.rs b/engine/src/data_types/dimens.rs
index b395627..0621b75 100644
--- a/engine/src/data_types/dimens.rs
+++ b/engine/src/data_types/dimens.rs
@@ -1,7 +1,80 @@
-/// Dimensions.
-#[derive(Debug, Clone, Copy)]
+use std::num::NonZeroU32;
+use std::ops::Div;
+
+use crate::reflection::Reflection;
+
+/// 2D dimensions.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Reflection)]
+#[reflection(impl_with_generics(<f32>, <f64>, <u32>, <u16>))]
pub struct Dimens<Value>
{
pub width: Value,
pub height: Value,
}
+
+impl<Value: Clone> From<Value> for Dimens<Value>
+{
+ fn from(value: Value) -> Self
+ {
+ Self { width: value.clone(), height: value }
+ }
+}
+
+impl<Value> From<(Value, Value)> for Dimens<Value>
+{
+ fn from(value: (Value, Value)) -> Self
+ {
+ Self { width: value.0, height: value.1 }
+ }
+}
+
+impl<Value: Div<Output = Value> + Clone> Div<Value> for Dimens<Value>
+{
+ type Output = Self;
+
+ fn div(self, rhs: Value) -> Self::Output
+ {
+ Self {
+ width: self.width / rhs.clone(),
+ height: self.height / rhs,
+ }
+ }
+}
+
+impl Dimens<u32>
+{
+ #[must_use]
+ pub fn try_into_nonzero(self) -> Option<Dimens<NonZeroU32>>
+ {
+ Some(Dimens {
+ width: NonZeroU32::new(self.width)?,
+ height: NonZeroU32::new(self.height)?,
+ })
+ }
+}
+
+/// 3D dimensions.
+#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Reflection)]
+#[reflection(impl_with_generics(<f32>, <f64>, <u32>, <u16>))]
+pub struct Dimens3<Value>
+{
+ pub width: Value,
+ pub height: Value,
+ pub depth: Value,
+}
+
+impl<Value> From<[Value; 3]> for Dimens3<Value>
+{
+ fn from([width, height, depth]: [Value; 3]) -> Self
+ {
+ Self { width, height, depth }
+ }
+}
+
+impl<Value> From<Dimens3<Value>> for [Value; 3]
+{
+ fn from(Dimens3 { width, height, depth }: Dimens3<Value>) -> Self
+ {
+ [width, height, depth]
+ }
+}
diff --git a/engine/src/data_types/matrix.rs b/engine/src/data_types/matrix.rs
index 3a29ae2..9ab5bd4 100644
--- a/engine/src/data_types/matrix.rs
+++ b/engine/src/data_types/matrix.rs
@@ -1,10 +1,12 @@
-use crate::vector::Vec3;
+use std::ops::{Add, Index, IndexMut, Mul};
+
+use crate::data_types::dimens::Dimens3;
+use crate::vector::{Vec3, Vec4};
#[derive(Debug, Clone)]
pub struct Matrix<Value, const ROWS: usize, const COLUMNS: usize>
{
- /// Items must be layed out this way for it to work with OpenGL shaders.
- items: [[Value; ROWS]; COLUMNS],
+ items: [[Value; COLUMNS]; ROWS],
}
impl<Value, const ROWS: usize, const COLUMNS: usize> Matrix<Value, ROWS, COLUMNS>
@@ -20,17 +22,61 @@ impl<Value, const ROWS: usize, const COLUMNS: usize> Matrix<Value, ROWS, COLUMNS
}
}
- /// Sets the value at the specified cell.
- pub fn set_cell(&mut self, row: usize, column: usize, value: Value)
+ pub fn from_columns<Column>(columns: [Column; COLUMNS]) -> Self
+ where
+ Column: Into<[Value; ROWS]>,
+ Value: Default,
{
- self.items[column][row] = value;
+ let mut items =
+ std::array::from_fn(|_| std::array::from_fn(|_| Value::default()));
+
+ for (column_index, column) in columns.into_iter().enumerate() {
+ for (row_index, item) in column.into().into_iter().enumerate() {
+ items[row_index][column_index] = item;
+ }
+ }
+
+ Self { items }
}
- /// Returns the internal 2D array as a pointer.
- #[must_use]
- pub fn as_ptr(&self) -> *const Value
+ pub fn from_rows<Row>(rows: [Row; ROWS]) -> Self
+ where
+ Row: Into<[Value; COLUMNS]>,
{
- self.items[0].as_ptr()
+ let items = rows.map(|row| row.into());
+
+ Self { items }
+ }
+
+ pub fn get_column_copied(&self, column_index: usize) -> [Value; ROWS]
+ where
+ Value: Default + Copy,
+ {
+ std::array::from_fn::<Value, ROWS, _>(|row_index| {
+ self[CellPos { row: row_index, col: column_index }]
+ })
+ }
+
+ pub fn items(&self) -> &[Value]
+ {
+ &self.items.as_flattened()
+ }
+
+ pub fn to_column_major(&self) -> [[Value; ROWS]; COLUMNS]
+ where
+ Value: Default + Clone,
+ {
+ let mut transposed = std::array::from_fn::<_, COLUMNS, _>(|_| {
+ std::array::from_fn::<_, ROWS, _>(|_| Value::default())
+ });
+
+ for (row_index, row) in self.items.iter().enumerate() {
+ for (column_index, item) in row.iter().enumerate() {
+ transposed[column_index][row_index] = item.clone();
+ }
+ }
+
+ transposed
}
}
@@ -51,19 +97,13 @@ impl<const ROWS_COLS: usize> Matrix<f32, ROWS_COLS, ROWS_COLS>
#[must_use]
pub fn new_identity() -> Self
{
- let mut index = 0;
-
- let items = [(); ROWS_COLS].map(|()| {
- let mut columns = [0.0; ROWS_COLS];
-
- columns[index] = 1.0;
-
- index += 1;
+ let mut identity = Self::new();
- columns
- });
+ for index in 0..ROWS_COLS {
+ identity[CellPos { row: index, col: index }] = 1.0;
+ }
- Self { items }
+ identity
}
}
@@ -71,21 +111,21 @@ impl Matrix<f32, 4, 4>
{
pub fn translate(&mut self, translation: &Vec3<f32>)
{
- self.set_cell(0, 3, translation.x);
- self.set_cell(1, 3, translation.y);
- self.set_cell(2, 3, translation.z);
- self.set_cell(3, 3, 1.0);
+ self[CellPos { row: 0, col: 3 }] = translation.x;
+ self[CellPos { row: 1, col: 3 }] = translation.y;
+ self[CellPos { row: 2, col: 3 }] = translation.z;
+ self[CellPos { row: 3, col: 3 }] = 1.0;
}
- pub fn scale(&mut self, scaling: &Vec3<f32>)
+ pub fn scale(&mut self, scaling: &Dimens3<f32>)
{
- self.set_cell(0, 0, scaling.x);
- self.set_cell(1, 1, scaling.y);
- self.set_cell(2, 2, scaling.z);
- self.set_cell(3, 3, 1.0);
+ self[CellPos { row: 0, col: 0 }] = scaling.width;
+ self[CellPos { row: 1, col: 1 }] = scaling.height;
+ self[CellPos { row: 2, col: 2 }] = scaling.depth;
+ self[CellPos { row: 3, col: 3 }] = 1.0;
}
- pub fn look_at(&mut self, eye: &Vec3<f32>, target: &Vec3<f32>, up: &Vec3<f32>)
+ pub fn look_at(&mut self, eye: Vec3<f32>, target: Vec3<f32>, up: Vec3<f32>)
{
let rev_target_direction = (eye - target).normalize();
@@ -93,30 +133,388 @@ impl Matrix<f32, 4, 4>
let camera_up = rev_target_direction.cross(&camera_right);
- self.set_cell(0, 0, camera_right.x);
- self.set_cell(0, 1, camera_right.y);
- self.set_cell(0, 2, camera_right.z);
+ self[CellPos { row: 0, col: 0 }] = camera_right.x;
+ self[CellPos { row: 0, col: 1 }] = camera_right.y;
+ self[CellPos { row: 0, col: 2 }] = camera_right.z;
- self.set_cell(1, 0, camera_up.x);
- self.set_cell(1, 1, camera_up.y);
- self.set_cell(1, 2, camera_up.z);
+ self[CellPos { row: 1, col: 0 }] = camera_up.x;
+ self[CellPos { row: 1, col: 1 }] = camera_up.y;
+ self[CellPos { row: 1, col: 2 }] = camera_up.z;
- self.set_cell(2, 0, rev_target_direction.x);
- self.set_cell(2, 1, rev_target_direction.y);
- self.set_cell(2, 2, rev_target_direction.z);
+ self[CellPos { row: 2, col: 0 }] = rev_target_direction.x;
+ self[CellPos { row: 2, col: 1 }] = rev_target_direction.y;
+ self[CellPos { row: 2, col: 2 }] = rev_target_direction.z;
// The vector is negated since we want the world to be translated in the opposite
// direction of where we want the camera to move.
let camera_pos = -Vec3 {
- x: camera_right.dot(eye),
- y: camera_up.dot(eye),
- z: rev_target_direction.dot(eye),
+ x: camera_right.dot(&eye),
+ y: camera_up.dot(&eye),
+ z: rev_target_direction.dot(&eye),
};
- self.set_cell(0, 3, camera_pos.x);
- self.set_cell(1, 3, camera_pos.y);
- self.set_cell(2, 3, camera_pos.z);
+ self[CellPos { row: 0, col: 3 }] = camera_pos.x;
+ self[CellPos { row: 1, col: 3 }] = camera_pos.y;
+ self[CellPos { row: 2, col: 3 }] = camera_pos.z;
- self.set_cell(3, 3, 1.0);
+ self[CellPos { row: 3, col: 3 }] = 1.0;
}
+
+ pub fn inverse(&self) -> Self
+ {
+ let coef_00 = self[CellPos { row: 2, col: 2 }] * self[CellPos { row: 3, col: 3 }]
+ - self[CellPos { row: 2, col: 3 }] * self[CellPos { row: 3, col: 2 }];
+ let coef_02 = self[CellPos { row: 2, col: 1 }] * self[CellPos { row: 3, col: 3 }]
+ - self[CellPos { row: 2, col: 3 }] * self[CellPos { row: 3, col: 1 }];
+ let coef_03 = self[CellPos { row: 2, col: 1 }] * self[CellPos { row: 3, col: 2 }]
+ - self[CellPos { row: 2, col: 2 }] * self[CellPos { row: 3, col: 1 }];
+
+ let coef_04 = self[CellPos { row: 1, col: 2 }] * self[CellPos { row: 3, col: 3 }]
+ - self[CellPos { row: 1, col: 3 }] * self[CellPos { row: 3, col: 2 }];
+ let coef_06 = self[CellPos { row: 1, col: 1 }] * self[CellPos { row: 3, col: 3 }]
+ - self[CellPos { row: 1, col: 3 }] * self[CellPos { row: 3, col: 1 }];
+ let coef_07 = self[CellPos { row: 1, col: 1 }] * self[CellPos { row: 3, col: 2 }]
+ - self[CellPos { row: 1, col: 2 }] * self[CellPos { row: 3, col: 1 }];
+
+ let coef_08 = self[CellPos { row: 1, col: 2 }] * self[CellPos { row: 2, col: 3 }]
+ - self[CellPos { row: 1, col: 3 }] * self[CellPos { row: 2, col: 2 }];
+ let coef_10 = self[CellPos { row: 1, col: 1 }] * self[CellPos { row: 2, col: 3 }]
+ - self[CellPos { row: 1, col: 3 }] * self[CellPos { row: 2, col: 1 }];
+ let coef_11 = self[CellPos { row: 1, col: 1 }] * self[CellPos { row: 2, col: 2 }]
+ - self[CellPos { row: 1, col: 2 }] * self[CellPos { row: 2, col: 1 }];
+
+ let coef_12 = self[CellPos { row: 0, col: 2 }] * self[CellPos { row: 3, col: 3 }]
+ - self[CellPos { row: 0, col: 3 }] * self[CellPos { row: 3, col: 2 }];
+ let coef_14 = self[CellPos { row: 0, col: 1 }] * self[CellPos { row: 3, col: 3 }]
+ - self[CellPos { row: 0, col: 3 }] * self[CellPos { row: 3, col: 1 }];
+ let coef_15 = self[CellPos { row: 0, col: 1 }] * self[CellPos { row: 3, col: 2 }]
+ - self[CellPos { row: 0, col: 2 }] * self[CellPos { row: 3, col: 1 }];
+
+ let coef_16 = self[CellPos { row: 0, col: 2 }] * self[CellPos { row: 2, col: 3 }]
+ - self[CellPos { row: 0, col: 3 }] * self[CellPos { row: 2, col: 2 }];
+ let coef_18 = self[CellPos { row: 0, col: 1 }] * self[CellPos { row: 2, col: 3 }]
+ - self[CellPos { row: 0, col: 3 }] * self[CellPos { row: 2, col: 1 }];
+ let coef_19 = self[CellPos { row: 0, col: 1 }] * self[CellPos { row: 2, col: 2 }]
+ - self[CellPos { row: 0, col: 2 }] * self[CellPos { row: 2, col: 1 }];
+
+ let coef_20 = self[CellPos { row: 0, col: 2 }] * self[CellPos { row: 1, col: 3 }]
+ - self[CellPos { row: 0, col: 3 }] * self[CellPos { row: 1, col: 2 }];
+ let coef_22 = self[CellPos { row: 0, col: 1 }] * self[CellPos { row: 1, col: 3 }]
+ - self[CellPos { row: 0, col: 3 }] * self[CellPos { row: 1, col: 1 }];
+ let coef_23 = self[CellPos { row: 0, col: 1 }] * self[CellPos { row: 1, col: 2 }]
+ - self[CellPos { row: 0, col: 2 }] * self[CellPos { row: 1, col: 1 }];
+
+ let fac_0 = Vec4 {
+ x: coef_00,
+ y: coef_00,
+ z: coef_02,
+ w: coef_03,
+ };
+ let fac_1 = Vec4 {
+ x: coef_04,
+ y: coef_04,
+ z: coef_06,
+ w: coef_07,
+ };
+ let fac_2 = Vec4 {
+ x: coef_08,
+ y: coef_08,
+ z: coef_10,
+ w: coef_11,
+ };
+ let fac_3 = Vec4 {
+ x: coef_12,
+ y: coef_12,
+ z: coef_14,
+ w: coef_15,
+ };
+ let fac_4 = Vec4 {
+ x: coef_16,
+ y: coef_16,
+ z: coef_18,
+ w: coef_19,
+ };
+ let fac_5 = Vec4 {
+ x: coef_20,
+ y: coef_20,
+ z: coef_22,
+ w: coef_23,
+ };
+
+ let vec_0 = Vec4 {
+ x: self[CellPos { row: 0, col: 1 }],
+ y: self[CellPos { row: 0, col: 0 }],
+ z: self[CellPos { row: 0, col: 0 }],
+ w: self[CellPos { row: 0, col: 0 }],
+ };
+ let vec_1 = Vec4 {
+ x: self[CellPos { row: 1, col: 1 }],
+ y: self[CellPos { row: 1, col: 0 }],
+ z: self[CellPos { row: 1, col: 0 }],
+ w: self[CellPos { row: 1, col: 0 }],
+ };
+ let vec_2 = Vec4 {
+ x: self[CellPos { row: 2, col: 1 }],
+ y: self[CellPos { row: 2, col: 0 }],
+ z: self[CellPos { row: 2, col: 0 }],
+ w: self[CellPos { row: 2, col: 0 }],
+ };
+ let vec_3 = Vec4 {
+ x: self[CellPos { row: 3, col: 1 }],
+ y: self[CellPos { row: 3, col: 0 }],
+ z: self[CellPos { row: 3, col: 0 }],
+ w: self[CellPos { row: 3, col: 0 }],
+ };
+
+ let inv_0 = vec_1 * fac_0 - vec_2 * fac_1 + vec_3 * fac_2;
+ let inv_1 = vec_0 * fac_0 - vec_2 * fac_3 + vec_3 * fac_4;
+ let inv_2 = vec_0 * fac_1 - vec_1 * fac_3 + vec_3 * fac_5;
+ let inv_3 = vec_0 * fac_2 - vec_1 * fac_4 + vec_2 * fac_5;
+
+ let sign_a = Vec4 { x: 1.0, y: -1.0, z: 1.0, w: -1.0 };
+ let sign_b = Vec4 { x: -1.0, y: 1.0, z: -1.0, w: 1.0 };
+
+ let inverse = Self::from_columns([
+ inv_0 * sign_a,
+ inv_1 * sign_b,
+ inv_2 * sign_a,
+ inv_3 * sign_b,
+ ]);
+
+ let row_0 = Vec4 {
+ x: inverse[CellPos { row: 0, col: 0 }],
+ y: inverse[CellPos { row: 0, col: 1 }],
+ z: inverse[CellPos { row: 0, col: 2 }],
+ w: inverse[CellPos { row: 0, col: 3 }],
+ };
+
+ let dot_0 = Vec4::<f32>::from(self.get_column_copied(0)) * row_0;
+
+ let dot_1 = (dot_0.x + dot_0.y) + (dot_0.z + dot_0.w);
+
+ let one_over_determinant = 1.0 / dot_1;
+
+ inverse * one_over_determinant
+ }
+}
+
+impl Mul<f32> for Matrix<f32, 4, 4>
+{
+ type Output = Self;
+
+ fn mul(self, scalar: f32) -> Self::Output
+ {
+ Self {
+ items: self.items.map(|row| (Vec4::from(row) * scalar).into()),
+ }
+ }
+}
+
+impl<Value, const ROWS: usize, const COLUMNS: usize, const RHS_COLUMNS: usize>
+ Mul<Matrix<Value, ROWS, RHS_COLUMNS>> for Matrix<Value, ROWS, COLUMNS>
+where
+ Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy,
+{
+ type Output = Matrix<Value, ROWS, RHS_COLUMNS>;
+
+ fn mul(self, rhs: Matrix<Value, ROWS, RHS_COLUMNS>) -> Self::Output
+ {
+ &self * &rhs
+ }
+}
+
+impl<Value, const ROWS: usize, const COLUMNS: usize, const RHS_COLUMNS: usize>
+ Mul<&Matrix<Value, ROWS, RHS_COLUMNS>> for Matrix<Value, ROWS, COLUMNS>
+where
+ Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy,
+{
+ type Output = Matrix<Value, ROWS, RHS_COLUMNS>;
+
+ fn mul(self, rhs: &Matrix<Value, ROWS, RHS_COLUMNS>) -> Self::Output
+ {
+ &self * rhs
+ }
+}
+
+impl<Value, const ROWS: usize, const COLUMNS: usize, const RHS_COLUMNS: usize>
+ Mul<Matrix<Value, ROWS, RHS_COLUMNS>> for &Matrix<Value, ROWS, COLUMNS>
+where
+ Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy,
+{
+ type Output = Matrix<Value, ROWS, RHS_COLUMNS>;
+
+ fn mul(self, rhs: Matrix<Value, ROWS, RHS_COLUMNS>) -> Self::Output
+ {
+ self * &rhs
+ }
+}
+
+impl<Value, const ROWS: usize, const COLUMNS: usize, const RHS_COLUMNS: usize>
+ Mul<&Matrix<Value, ROWS, RHS_COLUMNS>> for &Matrix<Value, ROWS, COLUMNS>
+where
+ Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy,
+{
+ type Output = Matrix<Value, ROWS, RHS_COLUMNS>;
+
+ fn mul(self, rhs: &Matrix<Value, ROWS, RHS_COLUMNS>) -> Self::Output
+ {
+ // https://en.wikipedia.org/wiki/Matrix_multiplication
+
+ let mut out = Self::Output::new();
+
+ for (row_index, row) in self.items.iter().enumerate() {
+ for (column, item) in row.iter().enumerate() {
+ let rhs_items = &rhs.items[column];
+
+ for (rhs_item_index, rhs_item) in rhs_items.iter().enumerate() {
+ let prev = out[CellPos { row: row_index, col: rhs_item_index }];
+
+ out[CellPos { row: row_index, col: rhs_item_index }] =
+ prev + (*item * *rhs_item);
+ }
+ }
+ }
+
+ out
+ }
+}
+
+impl<Value, const COLUMNS: usize> Mul<Vec3<Value>> for Matrix<Value, 3, COLUMNS>
+where
+ Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy,
+{
+ type Output = Vec3<Value>;
+
+ fn mul(self, rhs: Vec3<Value>) -> Self::Output
+ {
+ &self * &rhs
+ }
+}
+
+impl<Value, const COLUMNS: usize> Mul<&Vec3<Value>> for Matrix<Value, 3, COLUMNS>
+where
+ Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy,
+{
+ type Output = Vec3<Value>;
+
+ fn mul(self, rhs: &Vec3<Value>) -> Self::Output
+ {
+ &self * rhs
+ }
+}
+
+impl<Value, const COLUMNS: usize> Mul<Vec3<Value>> for &Matrix<Value, 3, COLUMNS>
+where
+ Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy,
+{
+ type Output = Vec3<Value>;
+
+ fn mul(self, rhs: Vec3<Value>) -> Self::Output
+ {
+ self * &rhs
+ }
+}
+
+impl<Value, const COLUMNS: usize> Mul<&Vec3<Value>> for &Matrix<Value, 3, COLUMNS>
+where
+ Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy,
+{
+ type Output = Vec3<Value>;
+
+ fn mul(self, rhs: &Vec3<Value>) -> Self::Output
+ {
+ let rhs_mat = Matrix::<Value, 3, 1> { items: [[rhs.x], [rhs.y], [rhs.z]] };
+
+ let Matrix { items: [[out_x], [out_y], [out_z]] } = self * &rhs_mat;
+
+ Vec3 { x: out_x, y: out_y, z: out_z }
+ }
+}
+
+impl<Value, const COLUMNS: usize> Mul<Vec4<Value>> for Matrix<Value, 4, COLUMNS>
+where
+ Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy,
+{
+ type Output = Vec4<Value>;
+
+ fn mul(self, rhs: Vec4<Value>) -> Self::Output
+ {
+ &self * &rhs
+ }
+}
+
+impl<Value, const COLUMNS: usize> Mul<&Vec4<Value>> for Matrix<Value, 4, COLUMNS>
+where
+ Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy,
+{
+ type Output = Vec4<Value>;
+
+ fn mul(self, rhs: &Vec4<Value>) -> Self::Output
+ {
+ &self * rhs
+ }
+}
+
+impl<Value, const COLUMNS: usize> Mul<Vec4<Value>> for &Matrix<Value, 4, COLUMNS>
+where
+ Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy,
+{
+ type Output = Vec4<Value>;
+
+ fn mul(self, rhs: Vec4<Value>) -> Self::Output
+ {
+ self * &rhs
+ }
+}
+
+impl<Value, const COLUMNS: usize> Mul<&Vec4<Value>> for &Matrix<Value, 4, COLUMNS>
+where
+ Value: Mul<Output = Value> + Add<Output = Value> + Default + Copy,
+{
+ type Output = Vec4<Value>;
+
+ fn mul(self, rhs: &Vec4<Value>) -> Self::Output
+ {
+ let rhs_mat = Matrix::<Value, 4, 1> {
+ items: [[rhs.x], [rhs.y], [rhs.z], [rhs.w]],
+ };
+
+ let Matrix {
+ items: [[out_x], [out_y], [out_z], [out_w]],
+ } = self * &rhs_mat;
+
+ Vec4 {
+ x: out_x,
+ y: out_y,
+ z: out_z,
+ w: out_w,
+ }
+ }
+}
+
+impl<Value, const ROWS: usize, const COLUMNS: usize> Index<CellPos>
+ for Matrix<Value, ROWS, COLUMNS>
+{
+ type Output = Value;
+
+ fn index(&self, cell_pos: CellPos) -> &Self::Output
+ {
+ &self.items[cell_pos.row][cell_pos.col]
+ }
+}
+
+impl<Value, const ROWS: usize, const COLUMNS: usize> IndexMut<CellPos>
+ for Matrix<Value, ROWS, COLUMNS>
+{
+ fn index_mut(&mut self, cell_pos: CellPos) -> &mut Self::Output
+ {
+ &mut self.items[cell_pos.row][cell_pos.col]
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct CellPos
+{
+ pub row: usize,
+ pub col: usize,
}
diff --git a/engine/src/data_types/vector.rs b/engine/src/data_types/vector.rs
index 17953f4..915599a 100644
--- a/engine/src/data_types/vector.rs
+++ b/engine/src/data_types/vector.rs
@@ -1,82 +1,125 @@
-use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
-
-use crate::color::Color;
+use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
+
+use crate::reflection::Reflection;
+
+macro_rules! impl_math_op_traits {
+ (impl $op_trait: ident for $vector: ident, ($($vec_field: ident),+)) => {
+ paste::paste! {
+ impl<Value> $op_trait for $vector<Value>
+ where
+ Value: $op_trait<Output = Value>,
+ {
+ type Output = Self;
+
+ fn [<$op_trait:lower>](self, rhs: Self) -> Self::Output
+ {
+ Self::Output {$(
+ $vec_field: self.$vec_field.[<$op_trait:lower>](rhs.$vec_field)
+ ),+}
+ }
+ }
+
+ impl<Value> [<$op_trait Assign>] for $vector<Value>
+ where
+ Value: [<$op_trait Assign>],
+ {
+ fn [<$op_trait:lower _assign>](&mut self, rhs: Self)
+ {
+ $(
+ self.$vec_field.[<$op_trait:lower _assign>](rhs.$vec_field);
+ )+
+ }
+ }
+ }
+ };
+
+ (impl $op_trait: ident <Value> for $vector: ident, ($($vec_field: ident),+)) => {
+ paste::paste! {
+ impl<Value> $op_trait<Value> for $vector<Value>
+ where
+ Value: $op_trait<Output = Value> + Clone,
+ {
+ type Output = Self;
+
+ fn [<$op_trait:lower>](self, rhs: Value) -> Self::Output
+ {
+ Self {$(
+ $vec_field: self.$vec_field.[<$op_trait:lower>](rhs.clone())
+ ),+}
+ }
+ }
+
+ impl<Value> [<$op_trait Assign>]<Value> for $vector<Value>
+ where
+ Value: [<$op_trait Assign>] + Clone,
+ {
+ fn [<$op_trait:lower _assign>](&mut self, rhs: Value)
+ {
+ $(
+ self.$vec_field.[<$op_trait:lower _assign>](rhs.clone());
+ )+
+ }
+ }
+ }
+ };
+}
-#[derive(Debug, Default, Clone, Copy)]
-pub struct Vec2<Value>
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Reflection)]
+#[reflection(impl_with_generics(<f32>, <f64>, <u32>, <u16>))]
+pub struct Vec2<Value: 'static>
{
pub x: Value,
pub y: Value,
}
+impl<Value> Vec2<Value>
+{
+ pub fn into_array(self) -> [Value; 2]
+ {
+ self.into()
+ }
+}
+
impl Vec2<u32>
{
pub const ZERO: Self = Self { x: 0, y: 0 };
}
-impl<Value> Add<Value> for Vec2<Value>
-where
- Value: Add<Output = Value> + Clone,
-{
- type Output = Self;
+impl_math_op_traits!(impl Add for Vec2, (x, y));
+impl_math_op_traits!(impl Sub for Vec2, (x, y));
- fn add(self, rhs: Value) -> Self::Output
- {
- Self {
- x: self.x + rhs.clone(),
- y: self.y + rhs,
- }
- }
-}
+impl_math_op_traits!(impl Add<Value> for Vec2, (x, y));
+impl_math_op_traits!(impl Sub<Value> for Vec2, (x, y));
+impl_math_op_traits!(impl Mul<Value> for Vec2, (x, y));
+impl_math_op_traits!(impl Div<Value> for Vec2, (x, y));
-impl<Value> Sub<Value> for Vec2<Value>
-where
- Value: Sub<Output = Value> + Clone,
+impl<Value> From<[Value; 2]> for Vec2<Value>
{
- type Output = Self;
-
- fn sub(self, rhs: Value) -> Self::Output
+ fn from([x, y]: [Value; 2]) -> Self
{
- Self {
- x: self.x - rhs.clone(),
- y: self.y - rhs,
- }
+ Self { x, y }
}
}
-impl<Value> Mul<Value> for Vec2<Value>
-where
- Value: Mul<Output = Value> + Clone,
+impl<Value> From<(Value, Value)> for Vec2<Value>
{
- type Output = Self;
-
- fn mul(self, rhs: Value) -> Self::Output
+ fn from((x, y): (Value, Value)) -> Self
{
- Self {
- x: self.x * rhs.clone(),
- y: self.y * rhs,
- }
+ Self { x, y }
}
}
-impl<Value> Div<Value> for Vec2<Value>
-where
- Value: Div<Output = Value> + Clone,
+impl<Value> From<Vec2<Value>> for [Value; 2]
{
- type Output = Self;
-
- fn div(self, rhs: Value) -> Self::Output
+ fn from(vec: Vec2<Value>) -> Self
{
- Self {
- x: self.x / rhs.clone(),
- y: self.y / rhs,
- }
+ [vec.x, vec.y]
}
}
-#[derive(Debug, Default, Clone, Copy)]
-#[repr(C)]
-pub struct Vec3<Value>
+#[derive(Debug, Default, Clone, Copy, PartialEq, Reflection)]
+#[reflection(impl_with_generics(<f32>, <f64>, <u32>, <u16>))]
+pub struct Vec3<Value: 'static>
{
pub x: Value,
pub y: Value,
@@ -85,6 +128,11 @@ pub struct Vec3<Value>
impl Vec3<f32>
{
+ pub const BACK: Self = Self { x: 0.0, y: 0.0, z: -1.0 };
+ pub const DOWN: Self = Self { x: 0.0, y: -1.0, z: 0.0 };
+ pub const FRONT: Self = Self { x: 0.0, y: 0.0, z: 1.0 };
+ pub const LEFT: Self = Self { x: -1.0, y: 0.0, z: 0.0 };
+ pub const RIGHT: Self = Self { x: 1.0, y: 0.0, z: 0.0 };
pub const UP: Self = Self { x: 0.0, y: 1.0, z: 0.0 };
/// Returns the length of the vector.
@@ -95,16 +143,18 @@ impl Vec3<f32>
}
/// Normalizes the vector, returning a unit vector.
+ ///
+ /// // Panics
+ /// When debug assertions are enabled, if the length is zero or not finite,
+ /// this functions panics.
#[must_use]
pub fn normalize(&self) -> Self
{
- let length = self.length();
+ let length_recip = 1.0 / self.length();
- Self {
- x: self.x / length,
- y: self.y / length,
- z: self.z / length,
- }
+ debug_assert!(length_recip.is_finite());
+
+ *self * length_recip
}
/// Returns the cross product of this and another vector.
@@ -125,89 +175,54 @@ impl Vec3<f32>
(self.x * rhs.x) + (self.y * rhs.y) + (self.z * rhs.z)
}
- /// Returns a direction vector from the specified angle (in degrees).
+ /// Returns a normalized direction vector from the specified angle (in degrees).
#[must_use]
- pub fn direction_from_angle(pitch_degs: f32, yaw_degs: f32) -> Self
+ pub fn direction_from_deg_angles(angles: Angles<f32>) -> Self
{
Self {
- x: yaw_degs.to_radians().cos() * pitch_degs.to_radians().cos(),
- y: pitch_degs.to_radians().sin(),
- z: yaw_degs.to_radians().sin() * pitch_degs.to_radians().cos(),
+ x: angles.yaw.to_radians().cos() * angles.pitch.to_radians().cos(),
+ y: angles.pitch.to_radians().sin(),
+ z: angles.yaw.to_radians().sin() * angles.pitch.to_radians().cos(),
}
+ .normalize()
}
-}
-impl<Value> Vec3<Value>
-{
- pub fn as_ptr(&self) -> *const Value
+ pub fn into_deg_angles(self) -> Angles<f32>
{
- &self.x
- }
-}
-
-impl<Value> Sub for Vec3<Value>
-where
- Value: Sub<Value, Output = Value>,
-{
- type Output = Self;
+ let Self { x, y, z } = self.normalize();
- fn sub(self, rhs: Self) -> Self::Output
- {
- Self::Output {
- x: self.x - rhs.x,
- y: self.y - rhs.y,
- z: self.z - rhs.z,
+ Angles {
+ yaw: z.atan2(x).to_degrees(),
+ pitch: y.asin().to_degrees(),
+ roll: 0.0,
}
}
}
-impl<Value> Sub for &Vec3<Value>
-where
- for<'a, 'b> &'a Value: Sub<&'b Value, Output = Value>,
+impl<Value> Vec3<Value>
{
- type Output = Vec3<Value>;
-
- fn sub(self, rhs: Self) -> Self::Output
+ pub fn into_array(self) -> [Value; 3]
{
- Self::Output {
- x: &self.x - &rhs.x,
- y: &self.y - &rhs.y,
- z: &self.z - &rhs.z,
- }
+ self.into()
}
-}
-impl<Value> Add for Vec3<Value>
-where
- Value: Add<Value, Output = Value>,
-{
- type Output = Self;
-
- fn add(self, rhs: Self) -> Self::Output
+ pub fn from_vec4(source: Vec4<Value>) -> Self
{
- Self::Output {
- x: self.x + rhs.x,
- y: self.y + rhs.y,
- z: self.z + rhs.z,
+ Self {
+ x: source.x,
+ y: source.y,
+ z: source.z,
}
}
}
-impl<Value> Add for &Vec3<Value>
-where
- for<'a, 'b> &'a Value: Add<&'b Value, Output = Value>,
-{
- type Output = Vec3<Value>;
+impl_math_op_traits!(impl Add for Vec3, (x, y, z));
+impl_math_op_traits!(impl Sub for Vec3, (x, y, z));
- fn add(self, rhs: Self) -> Self::Output
- {
- Self::Output {
- x: &self.x + &rhs.x,
- y: &self.y + &rhs.y,
- z: &self.z + &rhs.z,
- }
- }
-}
+impl_math_op_traits!(impl Add<Value> for Vec3, (x, y, z));
+impl_math_op_traits!(impl Sub<Value> for Vec3, (x, y, z));
+impl_math_op_traits!(impl Mul<Value> for Vec3, (x, y, z));
+impl_math_op_traits!(impl Div<Value> for Vec3, (x, y, z));
impl<Value> Neg for Vec3<Value>
where
@@ -225,100 +240,85 @@ where
}
}
-impl<Value> Add<Value> for Vec3<Value>
-where
- Value: Add<Value, Output = Value> + Clone,
+impl From<f32> for Vec3<f32>
{
- type Output = Self;
-
- fn add(mut self, rhs: Value) -> Self::Output
+ fn from(value: f32) -> Self
{
- self.x = self.x + rhs.clone();
- self.y = self.y + rhs.clone();
- self.z = self.z + rhs.clone();
-
- self
+ Self { x: value, y: value, z: value }
}
}
-impl<Value> Sub<Value> for Vec3<Value>
-where
- Value: Sub<Value, Output = Value> + Clone,
+impl<Value> From<[Value; 3]> for Vec3<Value>
{
- type Output = Self;
-
- fn sub(mut self, rhs: Value) -> Self::Output
+ fn from([x, y, z]: [Value; 3]) -> Self
{
- self.x = self.x - rhs.clone();
- self.y = self.y - rhs.clone();
- self.z = self.z - rhs.clone();
-
- self
+ Self { x, y, z }
}
}
-impl<Value> Mul<Value> for Vec3<Value>
-where
- Value: Mul<Value, Output = Value> + Clone,
+impl<Value> From<Vec3<Value>> for [Value; 3]
{
- type Output = Self;
-
- fn mul(mut self, rhs: Value) -> Self::Output
+ fn from(vec: Vec3<Value>) -> Self
{
- self.x = self.x * rhs.clone();
- self.y = self.y * rhs.clone();
- self.z = self.z * rhs.clone();
-
- self
+ [vec.x, vec.y, vec.z]
}
}
-impl<Value> AddAssign for Vec3<Value>
-where
- Value: AddAssign<Value>,
+#[derive(Debug, Default, Clone, Copy, PartialEq, Reflection)]
+#[reflection(impl_with_generics(<f32>, <f64>, <u32>, <u16>))]
+pub struct Vec4<Value: 'static>
{
- fn add_assign(&mut self, rhs: Self)
- {
- self.x += rhs.x;
- self.y += rhs.y;
- self.z += rhs.z;
- }
+ pub x: Value,
+ pub y: Value,
+ pub z: Value,
+ pub w: Value,
}
-impl<Value> SubAssign for Vec3<Value>
-where
- Value: SubAssign<Value>,
-{
- fn sub_assign(&mut self, rhs: Self)
- {
- self.x -= rhs.x;
- self.y -= rhs.y;
- self.z -= rhs.z;
- }
-}
+impl_math_op_traits!(impl Add for Vec4, (x, y, z, w));
+impl_math_op_traits!(impl Sub for Vec4, (x, y, z, w));
+impl_math_op_traits!(impl Mul for Vec4, (x, y, z, w));
-impl<Value> From<Value> for Vec3<Value>
-where
- Value: Clone,
+impl_math_op_traits!(impl Add<Value> for Vec4, (x, y, z, w));
+impl_math_op_traits!(impl Sub<Value> for Vec4, (x, y, z, w));
+impl_math_op_traits!(impl Mul<Value> for Vec4, (x, y, z, w));
+impl_math_op_traits!(impl Div<Value> for Vec4, (x, y, z, w));
+
+impl<Value: Clone> From<Value> for Vec4<Value>
{
fn from(value: Value) -> Self
{
Self {
x: value.clone(),
y: value.clone(),
- z: value,
+ z: value.clone(),
+ w: value,
}
}
}
-impl<Value> From<Color<Value>> for Vec3<Value>
+impl<Value> From<[Value; 4]> for Vec4<Value>
{
- fn from(color: Color<Value>) -> Self
+ fn from(values: [Value; 4]) -> Self
{
- Self {
- x: color.red,
- y: color.green,
- z: color.blue,
- }
+ let [x, y, z, w] = values;
+
+ Self { x, y, z, w }
+ }
+}
+
+impl<Value> From<Vec4<Value>> for [Value; 4]
+{
+ fn from(vec: Vec4<Value>) -> Self
+ {
+ [vec.x, vec.y, vec.z, vec.w]
}
}
+
+#[derive(Debug, Clone, Copy, Reflection)]
+#[reflection(impl_with_generics(<f32>, <f64>))]
+pub struct Angles<Value>
+{
+ pub yaw: Value,
+ pub pitch: Value,
+ pub roll: Value,
+}
diff --git a/engine/src/delta_time.rs b/engine/src/delta_time.rs
deleted file mode 100644
index 33a2fc8..0000000
--- a/engine/src/delta_time.rs
+++ /dev/null
@@ -1,32 +0,0 @@
-use std::time::{Duration, Instant};
-
-use ecs::component::local::Local;
-use ecs::sole::Single;
-use ecs::{Component, Sole};
-
-#[derive(Debug, Clone, Default, Sole)]
-pub struct DeltaTime
-{
- pub duration: Duration,
-}
-
-#[derive(Debug, Clone, Default, Component)]
-pub struct LastUpdate
-{
- pub time: Option<Instant>,
-}
-
-/// Updates the current delta time.
-///
-/// # Panics
-/// Will panic if no delta time component exists.
-pub fn update(mut delta_time: Single<DeltaTime>, mut last_update: Local<LastUpdate>)
-{
- let current_time = Instant::now();
-
- if let Some(last_update_time) = last_update.time {
- delta_time.duration = current_time.duration_since(last_update_time);
- }
-
- last_update.time = Some(current_time);
-}
diff --git a/engine/src/draw_flags.rs b/engine/src/draw_flags.rs
index df5eed1..b32bb14 100644
--- a/engine/src/draw_flags.rs
+++ b/engine/src/draw_flags.rs
@@ -1,11 +1,11 @@
-use ecs::Component;
-
-use crate::util::builder;
+use crate::builder;
+use crate::ecs::Component;
+use crate::reflection::Reflection;
builder! {
/// Flags for how a object should be drawn.
#[builder(name = Builder, derives = (Debug, Default, Clone))]
-#[derive(Debug, Default, Clone, Component)]
+#[derive(Debug, Default, Clone, Component, Reflection)]
#[non_exhaustive]
pub struct DrawFlags
{
@@ -22,14 +22,16 @@ impl DrawFlags
}
}
-#[derive(Debug, Default, Clone)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Reflection)]
pub struct PolygonModeConfig
{
pub face: PolygonModeFace,
pub mode: PolygonMode,
}
-#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(
+ Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Reflection,
+)]
pub enum PolygonMode
{
Point,
@@ -39,7 +41,9 @@ pub enum PolygonMode
Fill,
}
-#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(
+ Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Reflection,
+)]
pub enum PolygonModeFace
{
Front,
@@ -50,5 +54,5 @@ pub enum PolygonModeFace
}
/// Component that makes a object not be drawn.
-#[derive(Debug, Clone, Copy, Component)]
+#[derive(Debug, Default, Clone, Copy, Component, Reflection)]
pub struct NoDraw;
diff --git a/engine/src/event.rs b/engine/src/event.rs
deleted file mode 100644
index e5ae486..0000000
--- a/engine/src/event.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-pub use ecs::event::start::Start;
-use ecs::event::Event;
-
-#[derive(Debug)]
-pub struct Update;
-
-impl Event for Update {}
-
-#[derive(Debug)]
-pub struct PreUpdate;
-
-impl Event for PreUpdate {}
-
-#[derive(Debug)]
-pub struct Present;
-
-impl Event for Present {}
-
-#[derive(Debug)]
-pub struct PostPresent;
-
-impl Event for PostPresent {}
-
-#[derive(Debug)]
-pub struct Conclude;
-
-impl Event for Conclude {}
diff --git a/engine/src/file_format/wavefront/common.rs b/engine/src/file_format/wavefront/common.rs
index 8cdfb34..83fd8af 100644
--- a/engine/src/file_format/wavefront/common.rs
+++ b/engine/src/file_format/wavefront/common.rs
@@ -1,5 +1,7 @@
use std::num::{ParseFloatError, ParseIntError};
+use engine_ecs::util::array_vec::ArrayVec;
+
macro_rules! keyword {
(
$(#[$attr: meta])*
@@ -68,9 +70,11 @@ where
let mut parts = line.split(' ');
let keyword = KeywordT::from_str(parts.next().unwrap(), line_no)?;
- let arguments = parts
- .map(|part| Value::parse::<KeywordT>(part, line_no))
- .collect::<Result<Vec<_>, _>>()?;
+ let mut arguments = ArrayVec::<Value, 16>::default();
+
+ for part in parts {
+ arguments.push(Value::parse::<KeywordT>(part, line_no)?);
+ }
Ok(Some(Statement { keyword, arguments }))
}
@@ -265,7 +269,7 @@ impl Value
pub struct Statement<KeywordT: Keyword>
{
pub keyword: KeywordT,
- pub arguments: Vec<Value>,
+ pub arguments: ArrayVec<Value, 16>,
}
impl<KeywordT: Keyword> Statement<KeywordT>
diff --git a/engine/src/file_format/wavefront/mtl.rs b/engine/src/file_format/wavefront/mtl.rs
index ef6e894..24387b9 100644
--- a/engine/src/file_format/wavefront/mtl.rs
+++ b/engine/src/file_format/wavefront/mtl.rs
@@ -2,17 +2,15 @@
//!
//! File format documentation: <https://paulbourke.net/dataformats/mtl>
-use std::path::Path;
+use std::path::{Path, PathBuf};
-use crate::color::Color;
+use crate::color::{Color, Rgb};
use crate::file_format::wavefront::common::{
keyword,
parse_statement_line,
ParsingError,
Statement,
};
-use crate::material::{Builder as MaterialBuilder, Material};
-use crate::texture::{Error as TextureError, Texture};
/// Parses the content of a Wavefront `.mtl`.
///
@@ -44,25 +42,47 @@ pub fn parse(obj_content: &str) -> Result<Vec<NamedMaterial>, Error>
.filter(|(_, statement)| matches!(statement.keyword, Keyword::Newmtl))
.count();
- #[cfg(feature = "debug")]
tracing::debug!("Material count: {material_cnt}");
statements_to_materials(statements, material_cnt)
}
#[derive(Debug, Clone)]
+#[non_exhaustive]
pub struct NamedMaterial
{
pub name: String,
- pub material: Material,
+ pub ambient: Color<f32>,
+ pub diffuse: Color<f32>,
+ pub specular: Color<f32>,
+ pub ambient_map: Option<TextureMap>,
+ pub diffuse_map: Option<TextureMap>,
+ pub specular_map: Option<TextureMap>,
+ pub shininess: f32,
+}
+
+impl Default for NamedMaterial
+{
+ fn default() -> Self
+ {
+ Self {
+ name: String::new(),
+ ambient: Color::Rgb(Rgb::<f32>::white()),
+ diffuse: Color::Rgb(Rgb::<f32>::white()),
+ specular: Color::Rgb(Rgb::<f32>::white()),
+ ambient_map: None,
+ diffuse_map: None,
+ specular_map: None,
+ shininess: 0.0,
+ }
+ }
}
#[derive(Debug, Clone)]
-pub struct UnfinishedNamedMaterial
+#[non_exhaustive]
+pub struct TextureMap
{
- name: String,
- material_builder: MaterialBuilder,
- ready: bool,
+ pub path: PathBuf,
}
#[derive(Debug, thiserror::Error)]
@@ -71,8 +91,14 @@ pub enum Error
#[error(transparent)]
ParsingError(#[from] ParsingError),
- #[error("Failed to open texture")]
- TextureError(#[from] TextureError),
+ #[error(
+ "A material start statement (newmtl) is expected before statement at line {}",
+ line_no
+ )]
+ ExpectedMaterialStartStmtBeforeStmt
+ {
+ line_no: usize
+ },
#[error(
"Unsupported number of arguments ({arg_count}) to {keyword} at line {line_no}"
@@ -93,7 +119,7 @@ pub enum Error
},
}
-#[cfg_attr(feature = "debug", tracing::instrument(skip_all))]
+#[tracing::instrument(skip_all)]
fn statements_to_materials(
statements: impl IntoIterator<Item = (usize, Statement<Keyword>)>,
material_cnt: usize,
@@ -101,63 +127,52 @@ fn statements_to_materials(
{
let mut materials = Vec::<NamedMaterial>::with_capacity(material_cnt);
- let mut curr_material = UnfinishedNamedMaterial {
- name: String::new(),
- material_builder: MaterialBuilder::new(),
- ready: false,
- };
-
for (line_no, statement) in statements {
if statement.keyword == Keyword::Newmtl {
- if curr_material.ready {
- #[cfg(feature = "debug")]
- tracing::debug!("Building material");
-
- let material = curr_material.material_builder.clone().build();
-
- materials.push(NamedMaterial { name: curr_material.name, material });
- }
-
let name = statement.get_text_arg(0, line_no)?;
- curr_material.name = name.to_string();
- curr_material.ready = true;
+ materials.push(NamedMaterial {
+ name: name.to_string(),
+ ..Default::default()
+ });
continue;
}
- if !curr_material.ready {
- // Discard statements not belonging to a material
- continue;
+ let Some(curr_material) = materials.last_mut() else {
+ return Err(Error::ExpectedMaterialStartStmtBeforeStmt { line_no });
};
match statement.keyword {
Keyword::Ka => {
let color = get_color_from_statement(&statement, line_no)?;
- #[cfg(feature = "debug")]
- tracing::debug!("Adding ambient color");
+ tracing::debug!(
+ "Adding ambient color {color:?} to material {}",
+ curr_material.name
+ );
- curr_material.material_builder =
- curr_material.material_builder.ambient(color);
+ curr_material.ambient = color;
}
Keyword::Kd => {
let color = get_color_from_statement(&statement, line_no)?;
- #[cfg(feature = "debug")]
- tracing::debug!("Adding diffuse color");
+ tracing::debug!(
+ "Adding diffuse color {color:?} to material {}",
+ curr_material.name
+ );
- curr_material.material_builder =
- curr_material.material_builder.diffuse(color);
+ curr_material.diffuse = color;
}
Keyword::Ks => {
let color = get_color_from_statement(&statement, line_no)?;
- #[cfg(feature = "debug")]
- tracing::debug!("Adding specular color");
+ tracing::debug!(
+ "Adding specular color {color:?} to material {}",
+ curr_material.name
+ );
- curr_material.material_builder =
- curr_material.material_builder.specular(color);
+ curr_material.specular = color;
}
Keyword::MapKa => {
if statement.arguments.len() > 1 {
@@ -170,55 +185,75 @@ fn statements_to_materials(
let texture_file_path = statement.get_text_arg(0, line_no)?;
- let texture = Texture::open(Path::new(texture_file_path))?;
-
- #[cfg(feature = "debug")]
- tracing::debug!("Adding ambient map");
+ tracing::debug!(
+ "Adding ambient map {texture_file_path} to material {}",
+ curr_material.name
+ );
- let texture_id = texture.id();
-
- curr_material.material_builder = curr_material
- .material_builder
- .texture(texture)
- .ambient_map(texture_id);
+ curr_material.ambient_map = Some(TextureMap {
+ path: Path::new(texture_file_path).to_path_buf(),
+ });
}
Keyword::MapKd => {
- let texture = get_map_from_texture(&statement, line_no)?;
+ if statement.arguments.len() > 1 {
+ return Err(Error::UnsupportedArgumentCount {
+ keyword: statement.keyword.to_string(),
+ arg_count: statement.arguments.len(),
+ line_no,
+ });
+ }
- #[cfg(feature = "debug")]
- tracing::debug!("Adding diffuse map");
+ let texture_file_path = statement.get_text_arg(0, line_no)?;
- let texture_id = texture.id();
+ tracing::debug!(
+ "Adding diffuse map {texture_file_path} to material {}",
+ curr_material.name
+ );
- curr_material.material_builder = curr_material
- .material_builder
- .texture(texture)
- .diffuse_map(texture_id);
+ curr_material.diffuse_map = Some(TextureMap {
+ path: Path::new(texture_file_path).to_path_buf(),
+ });
}
Keyword::MapKs => {
- let texture = get_map_from_texture(&statement, line_no)?;
+ if statement.arguments.len() > 1 {
+ return Err(Error::UnsupportedArgumentCount {
+ keyword: statement.keyword.to_string(),
+ arg_count: statement.arguments.len(),
+ line_no,
+ });
+ }
- #[cfg(feature = "debug")]
- tracing::debug!("Adding specular map");
+ let texture_file_path = statement.get_text_arg(0, line_no)?;
- let texture_id = texture.id();
+ tracing::debug!(
+ "Adding specular map {texture_file_path} to material {}",
+ curr_material.name
+ );
- curr_material.material_builder = curr_material
- .material_builder
- .texture(texture)
- .specular_map(texture_id);
+ curr_material.specular_map = Some(TextureMap {
+ path: Path::new(texture_file_path).to_path_buf(),
+ });
}
- Keyword::Newmtl => {}
- }
- }
+ Keyword::Ns => {
+ if statement.arguments.len() != 1 {
+ return Err(Error::UnsupportedArgumentCount {
+ keyword: statement.keyword.to_string(),
+ arg_count: statement.arguments.len(),
+ line_no,
+ });
+ }
- if curr_material.ready {
- #[cfg(feature = "debug")]
- tracing::debug!("Building last material");
+ let shininess = statement.get_float_arg(0, line_no)?;
- let material = curr_material.material_builder.build();
+ tracing::debug!(
+ "Adding shininess {shininess} to material {}",
+ curr_material.name
+ );
- materials.push(NamedMaterial { name: curr_material.name, material });
+ curr_material.shininess = shininess;
+ }
+ Keyword::Newmtl => {}
+ }
}
Ok(materials)
@@ -241,25 +276,7 @@ fn get_color_from_statement(
let green = statement.get_float_arg(1, line_no)?;
let blue = statement.get_float_arg(2, line_no)?;
- Ok(Color { red, green, blue })
-}
-
-fn get_map_from_texture(
- statement: &Statement<Keyword>,
- line_no: usize,
-) -> Result<Texture, Error>
-{
- if statement.arguments.len() > 1 {
- return Err(Error::UnsupportedArgumentCount {
- keyword: statement.keyword.to_string(),
- arg_count: statement.arguments.len(),
- line_no,
- });
- }
-
- let texture_file_path = statement.get_text_arg(0, line_no)?;
-
- Ok(Texture::open(Path::new(texture_file_path))?)
+ Ok(Color::Rgb(Rgb { r: red, g: green, b: blue }))
}
keyword! {
@@ -280,5 +297,7 @@ keyword! {
#[keyword(rename = "map_Ks")]
MapKs,
+
+ Ns,
}
}
diff --git a/engine/src/file_format/wavefront/obj.rs b/engine/src/file_format/wavefront/obj.rs
index 88e6580..dbcee33 100644
--- a/engine/src/file_format/wavefront/obj.rs
+++ b/engine/src/file_format/wavefront/obj.rs
@@ -2,20 +2,28 @@
//!
//! File format documentation: <https://paulbourke.net/dataformats/obj>
+use std::collections::HashMap;
use std::fs::read_to_string;
use std::path::PathBuf;
+use engine_ecs::util::Either;
+
use crate::file_format::wavefront::common::{
keyword,
parse_statement_line,
+ Keyword as _,
ParsingError,
Statement,
Triplet,
};
-use crate::mesh::Mesh;
-use crate::util::try_option;
+use crate::mesh::vertex_buffer::{
+ NamedVertexAttr,
+ VertexAttrInfo,
+ VertexBuffer as MeshVertexBuffer,
+ VertexLabel,
+};
+use crate::mesh::{Mesh, VertexAttrType};
use crate::vector::{Vec2, Vec3};
-use crate::vertex::{Builder as VertexBuilder, Vertex};
/// Parses the content of a Wavefront `.obj`.
///
@@ -28,34 +36,37 @@ pub fn parse(obj_content: &str) -> Result<Obj, Error>
.enumerate()
.map(|(line_index, line)| (line_index + 1, line));
- let statements = lines
- .map(|(line_no, line)| (line_no, parse_statement_line::<Keyword>(line, line_no)))
- .filter_map(|(line_no, result)| {
- let opt_statement = match result {
- Ok(opt_statement) => opt_statement,
- Err(err) => {
- return Some(Err(err));
- }
- };
-
- Some(Ok((line_no, opt_statement?)))
- })
- .collect::<Result<Vec<_>, _>>()?;
-
- let vertex_positions = get_vertex_positions_from_statements(&statements)?;
- let texture_positions = get_texture_positions_from_statements(&statements)?;
- let vertex_normals = get_vertex_normals_from_statements(&statements)?;
- let material_specs = get_material_specs_from_statements(&statements)?;
- let faces = get_faces_from_statements(&statements, &material_specs)?;
- let mtl_libs = get_mtl_libs_from_statements(&statements)?;
-
- Ok(Obj {
- vertex_positions,
- vertex_normals,
- texture_positions,
- faces,
- mtl_libs,
- })
+ let mut item_counts = ItemCounts::default();
+
+ for (line_index, line) in obj_content.lines().enumerate() {
+ if line.is_empty() || line.starts_with('#') {
+ continue;
+ }
+
+ let Some((keyword, _)) = line.split_once(" ") else {
+ continue;
+ };
+
+ let Ok(keyword) = Keyword::from_str(keyword, line_index + 1) else {
+ continue;
+ };
+
+ item_counts.increment_item_cnt_by_keyword(keyword);
+ }
+
+ let mut obj = Obj::with_capacities_from_item_cnts(item_counts);
+
+ let mut parsing_state = ParsingState::default();
+
+ for (line_no, line) in lines {
+ let Some(statement) = parse_statement_line::<Keyword>(line, line_no)? else {
+ continue;
+ };
+
+ obj.handle_statement(line_no, statement, &mut parsing_state)?;
+ }
+
+ Ok(obj)
}
/// The data of a Wavefront object file.
@@ -67,7 +78,8 @@ pub struct Obj
pub vertex_normals: Vec<Vec3<f32>>,
pub texture_positions: Vec<Vec2<f32>>,
pub faces: Vec<Face>,
- pub mtl_libs: Vec<Vec<PathBuf>>,
+ pub mtl_libs: Vec<PathBuf>,
+ pub unique_used_material_names: Vec<Box<str>>,
}
impl Obj
@@ -80,28 +92,124 @@ impl Obj
/// - A face's texture position cannot be found
/// - A face's vertex normal cannot be found
/// - A face index does not fit in a [`u32`]
- pub fn to_mesh(&self) -> Result<Mesh, Error>
+ pub fn to_mesh(&self, options: ToMeshOptions) -> Result<Mesh, Error>
{
- let vertices = self
- .faces
- .iter()
- .flat_map(|face| face.vertices.clone())
- .map(|face_vertex| face_vertex.to_vertex(self))
- .collect::<Result<Vec<_>, Error>>()?;
-
- Ok(Mesh::new(
- vertices,
- Some(
- self.faces
- .iter()
- .flat_map(|face| face.vertices.clone())
- .enumerate()
- .map(|(index, _)| {
- u32::try_from(index).map_err(|_| Error::FaceIndexTooBig(index))
- })
- .collect::<Result<Vec<_>, _>>()?,
- ),
- ))
+ let mut vertex_buf = MeshVertexBuffer::with_capacity(
+ &[
+ VertexAttrInfo {
+ label: VertexLabel::Position,
+ ty: VertexAttrType::Float32Array { length: 3 },
+ },
+ VertexAttrInfo {
+ label: VertexLabel::UvFromTopLeft,
+ ty: VertexAttrType::Float32Array { length: 2 },
+ },
+ VertexAttrInfo {
+ label: VertexLabel::Normal,
+ ty: VertexAttrType::Float32Array { length: 3 },
+ },
+ ],
+ self.faces.len() * 3,
+ );
+
+ let mut indices = Vec::<u32>::with_capacity(self.faces.len() * 3);
+
+ let mut added_face_vertices =
+ HashMap::<FaceVertex, u32>::with_capacity(self.faces.len() * 3);
+
+ for face in &self.faces {
+ for face_vertex in &face.vertices {
+ if let Some(index) = added_face_vertices.get(&face_vertex) {
+ indices.push(*index);
+
+ continue;
+ }
+
+ let pos = self
+ .vertex_positions
+ .get(face_vertex.position as usize - 1)
+ .ok_or(Error::FaceVertexPositionNotFound {
+ vertex_pos_index: face_vertex.position,
+ })?
+ .clone();
+
+ let texture_pos = face_vertex.texture.map_or_else(
+ || {
+ if !self.texture_positions.is_empty() {
+ tracing::warn!(concat!(
+ "Wavefront OBJ has texture coordinates ",
+ "but face vertex does not specify one"
+ ));
+ }
+
+ Ok(Vec2::default())
+ },
+ |face_vertex_texture| {
+ self.texture_positions
+ .get(face_vertex_texture as usize - 1)
+ .ok_or(Error::FaceTexturePositionNotFound {
+ texture_pos_index: face_vertex_texture,
+ })
+ .cloned()
+ },
+ )?;
+
+ let texture_pos = options
+ .y_flip_uvs
+ .then(|| Vec2 { x: texture_pos.x, y: -texture_pos.y })
+ .unwrap_or(texture_pos);
+
+ let normal = face_vertex.normal.map_or_else(
+ || {
+ if !self.vertex_normals.is_empty() {
+ tracing::warn!(concat!(
+ "Wavefront OBJ has normals ",
+ "but face vertex does not specify one"
+ ));
+ }
+
+ Ok(Vec3::default())
+ },
+ |face_vertex_normal| {
+ self.vertex_normals
+ .get(face_vertex_normal as usize - 1)
+ .ok_or(Error::FaceVertexNormalNotFound {
+ vertex_normal_index: face_vertex_normal,
+ })
+ .cloned()
+ },
+ )?;
+
+ vertex_buf.push((
+ NamedVertexAttr {
+ label: VertexLabel::Position,
+ value: pos.into_array(),
+ },
+ NamedVertexAttr {
+ label: VertexLabel::UvFromTopLeft,
+ value: texture_pos.into_array(),
+ },
+ NamedVertexAttr {
+ label: VertexLabel::Normal,
+ value: normal.into_array(),
+ },
+ ));
+
+ let vertex_index = vertex_buf.len() - 1;
+
+ let vertex_index = u32::try_from(vertex_index)
+ .map_err(|_| Error::FaceIndexTooBig(vertex_index))?;
+
+ indices.push(vertex_index);
+
+ added_face_vertices.insert(face_vertex.clone(), vertex_index);
+ }
+ }
+
+ Ok(Mesh::builder()
+ .vertices(vertex_buf)
+ .indices(indices)
+ .build())
}
/// Reads and parses the material libraries of this `Obj`.
@@ -117,7 +225,6 @@ impl Obj
{
self.mtl_libs
.iter()
- .flatten()
.map(|mtl_lib| {
Ok(parse_material_lib(&read_to_string(mtl_lib).map_err(
|err| MaterialLibsError::ReadingMaterialLibFailed {
@@ -132,69 +239,257 @@ impl Obj
})
.collect::<Result<Vec<_>, _>>()
}
-}
-#[derive(Debug)]
-#[non_exhaustive]
-pub struct Face
-{
- pub vertices: [FaceVertex; 3],
- pub material_name: Option<String>,
-}
+ fn with_capacities_from_item_cnts(item_counts: ItemCounts) -> Self
+ {
+ Self {
+ vertex_positions: Vec::with_capacity(item_counts.pos_cnt),
+ vertex_normals: Vec::with_capacity(item_counts.normal_cnt),
+ texture_positions: Vec::with_capacity(item_counts.uv_cnt),
+ faces: Vec::with_capacity(item_counts.face_cnt),
+ mtl_libs: Vec::with_capacity(item_counts.mtl_lib_cnt),
+ unique_used_material_names: Vec::with_capacity(
+ item_counts.material_usage_cnt,
+ ),
+ }
+ }
-#[derive(Debug, Clone)]
-pub struct FaceVertex
-{
- pub position: u32,
- pub texture: Option<u32>,
- pub normal: Option<u32>,
-}
+ fn handle_statement(
+ &mut self,
+ line_no: usize,
+ statement: Statement<Keyword>,
+ parsing_state: &mut ParsingState,
+ ) -> Result<(), Error>
+ {
+ match statement.keyword {
+ Keyword::V => self.handle_position_statement(line_no, statement),
+ Keyword::Vn => self.handle_normal_statement(line_no, statement),
+ Keyword::Vt => self.handle_uv_statement(line_no, statement),
+ Keyword::F => self.handle_face_statement(line_no, statement, parsing_state),
+ Keyword::Mtllib => self.handle_material_lib_statement(line_no, statement),
+ Keyword::Usemtl => {
+ self.handle_material_usage_statement(line_no, statement, parsing_state)
+ }
+ Keyword::O | Keyword::S => Ok(()),
+ }
+ }
-impl FaceVertex
-{
- /// Tries to convert this face vertex into a [`Vertex`].
- ///
- /// # Errors
- /// Returns `Err` if:
- /// - The face's vertex position cannot be found in the given [`Obj`]
- /// - The face's texture position cannot be found in the given [`Obj`]
- /// - The face's vertex normal cannot be found in the given [`Obj`]
- pub fn to_vertex(&self, obj: &Obj) -> Result<Vertex, Error>
+ fn handle_position_statement(
+ &mut self,
+ line_no: usize,
+ statement: Statement<Keyword>,
+ ) -> Result<(), Error>
{
- let mut vertex_builder = VertexBuilder::default();
+ debug_assert_eq!(statement.keyword, Keyword::V);
+
+ if statement.arguments.len() == 4 {
+ return Err(Error::UnsupportedArgumentCount {
+ keyword: statement.keyword.to_string(),
+ arg_count: statement.arguments.len(),
+ line_no: line_no,
+ });
+ }
- let vertex_pos = *obj.vertex_positions.get(self.position as usize - 1).ok_or(
- Error::FaceVertexPositionNotFound { vertex_pos_index: self.position },
- )?;
+ if statement.arguments.len() > 4 {
+ return Err(Error::InvalidArgumentCount {
+ keyword: statement.keyword.to_string(),
+ arg_count: statement.arguments.len(),
+ line_no: line_no,
+ });
+ }
+
+ let x = statement.get_float_arg(0, line_no)?;
+ let y = statement.get_float_arg(1, line_no)?;
+ let z = statement.get_float_arg(2, line_no)?;
- vertex_builder = vertex_builder.pos(vertex_pos);
+ self.vertex_positions.push(Vec3 { x, y, z });
+
+ Ok(())
+ }
- if let Some(face_vertex_texture) = self.texture {
- let texture_pos = obj
- .texture_positions
- .get(face_vertex_texture as usize - 1)
- .ok_or(Error::FaceTexturePositionNotFound {
- texture_pos_index: face_vertex_texture,
- })?;
+ fn handle_uv_statement(
+ &mut self,
+ line_no: usize,
+ statement: Statement<Keyword>,
+ ) -> Result<(), Error>
+ {
+ debug_assert_eq!(statement.keyword, Keyword::Vt);
+
+ if statement.arguments.len() == 3 {
+ return Err(Error::UnsupportedArgumentCount {
+ keyword: statement.keyword.to_string(),
+ arg_count: statement.arguments.len(),
+ line_no: line_no,
+ });
+ }
- vertex_builder = vertex_builder.texture_coords(*texture_pos);
+ if statement.arguments.len() > 3 {
+ return Err(Error::InvalidArgumentCount {
+ keyword: statement.keyword.to_string(),
+ arg_count: statement.arguments.len(),
+ line_no: line_no,
+ });
}
- if let Some(face_vertex_normal) = self.normal {
- let vertex_normal = *obj
- .vertex_normals
- .get(face_vertex_normal as usize - 1)
- .ok_or(Error::FaceVertexNormalNotFound {
- vertex_normal_index: face_vertex_normal,
- })?;
+ let u = statement.get_float_arg(0, line_no)?;
+ let v = statement.get_float_arg(1, line_no)?;
- vertex_builder = vertex_builder.normal(vertex_normal);
+ self.texture_positions.push(Vec2 { x: u, y: v });
+
+ Ok(())
+ }
+
+ fn handle_normal_statement(
+ &mut self,
+ line_no: usize,
+ statement: Statement<Keyword>,
+ ) -> Result<(), Error>
+ {
+ debug_assert_eq!(statement.keyword, Keyword::Vn);
+
+ if statement.arguments.len() > 3 {
+ return Err(Error::InvalidArgumentCount {
+ keyword: statement.keyword.to_string(),
+ arg_count: statement.arguments.len(),
+ line_no: line_no,
+ });
}
- Ok(vertex_builder.build())
+ let i = statement.get_float_arg(0, line_no)?;
+ let j = statement.get_float_arg(1, line_no)?;
+ let k = statement.get_float_arg(2, line_no)?;
+
+ self.vertex_normals.push(Vec3 { x: i, y: j, z: k });
+
+ Ok(())
+ }
+
+ fn handle_material_usage_statement(
+ &mut self,
+ line_no: usize,
+ statement: Statement<Keyword>,
+ parsing_state: &mut ParsingState,
+ ) -> Result<(), Error>
+ {
+ debug_assert_eq!(statement.keyword, Keyword::Usemtl);
+
+ if statement.arguments.len() > 1 {
+ return Err(Error::InvalidArgumentCount {
+ keyword: statement.keyword.to_string(),
+ arg_count: statement.arguments.len(),
+ line_no: line_no,
+ });
+ }
+
+ let material_name = statement
+ .get_text_arg(0, line_no)?
+ .to_owned()
+ .into_boxed_str();
+
+ if !self.unique_used_material_names.contains(&material_name) {
+ self.unique_used_material_names.push(material_name.clone());
+ }
+
+ parsing_state.current_material_name = Some(material_name);
+
+ Ok(())
+ }
+
+ fn handle_face_statement(
+ &mut self,
+ line_no: usize,
+ statement: Statement<Keyword>,
+ parsing_state: &mut ParsingState,
+ ) -> Result<(), Error>
+ {
+ debug_assert_eq!(statement.keyword, Keyword::F);
+
+ if statement.arguments.len() > 3 {
+ return Err(Error::UnsupportedArgumentCount {
+ keyword: statement.keyword.to_string(),
+ arg_count: statement.arguments.len(),
+ line_no: line_no,
+ });
+ }
+
+ let vertex_a = statement.get_triplet_arg(0, line_no)?;
+ let vertex_b = statement.get_triplet_arg(1, line_no)?;
+ let vertex_c = statement.get_triplet_arg(2, line_no)?;
+
+ self.faces.push(Face {
+ vertices: [vertex_a.into(), vertex_b.into(), vertex_c.into()],
+ material_name: parsing_state.current_material_name.clone(),
+ });
+
+ Ok(())
+ }
+
+ fn handle_material_lib_statement(
+ &mut self,
+ line_no: usize,
+ statement: Statement<Keyword>,
+ ) -> Result<(), Error>
+ {
+ debug_assert_eq!(statement.keyword, Keyword::Mtllib);
+
+ for (index, value) in statement.arguments.iter().enumerate() {
+ let mtl_lib = PathBuf::from(value.to_text(index, line_no)?);
+
+ self.mtl_libs.push(mtl_lib);
+ }
+
+ Ok(())
}
}
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct ToMeshOptions
+{
+ /// Whether all UVs should be flipped on the Y axis. This is necessary if the UVs
+ /// have their origin at the bottom left corner of the image
+ ///
+ /// The default is `true`.
+ pub y_flip_uvs: bool,
+}
+
+impl ToMeshOptions
+{
+ /// Whether all UVs should be flipped on the Y axis. This is necessary if the UVs have
+ /// their origin at the bottom left corner of the image
+ ///
+ /// The default is `true`.
+ pub fn y_flip_uvs(mut self, y_flip_uvs: bool) -> Self
+ {
+ self.y_flip_uvs = y_flip_uvs;
+ self
+ }
+}
+
+impl Default for ToMeshOptions
+{
+ fn default() -> Self
+ {
+ Self { y_flip_uvs: true }
+ }
+}
+
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct Face
+{
+ pub vertices: [FaceVertex; 3],
+ pub material_name: Option<Box<str>>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct FaceVertex
+{
+ pub position: u32,
+ pub texture: Option<u32>,
+ pub normal: Option<u32>,
+}
+
impl From<Triplet> for FaceVertex
{
fn from(triplet: Triplet) -> Self
@@ -274,6 +569,39 @@ pub enum MaterialLibsError<MaterialLibParseError>
},
}
+#[derive(Debug, Default)]
+struct ParsingState
+{
+ current_material_name: Option<Box<str>>,
+}
+
+#[derive(Debug, Default)]
+struct ItemCounts
+{
+ pos_cnt: usize,
+ uv_cnt: usize,
+ normal_cnt: usize,
+ face_cnt: usize,
+ mtl_lib_cnt: usize,
+ material_usage_cnt: usize,
+}
+
+impl ItemCounts
+{
+ fn increment_item_cnt_by_keyword(&mut self, keyword: Keyword)
+ {
+ match keyword {
+ Keyword::V => self.pos_cnt += 1,
+ Keyword::Vn => self.normal_cnt += 1,
+ Keyword::Vt => self.uv_cnt += 1,
+ Keyword::F => self.face_cnt += 1,
+ Keyword::Mtllib => self.mtl_lib_cnt += 1,
+ Keyword::Usemtl => self.material_usage_cnt += 1,
+ Keyword::O | Keyword::S => {}
+ }
+ }
+}
+
keyword! {
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum Keyword {
@@ -303,233 +631,6 @@ keyword! {
}
}
-#[derive(Debug)]
-struct MaterialSpecifier
-{
- material_name: String,
- line_no: usize,
-}
-
-fn find_material_specifier_for_line_no(
- material_specifiers: &[MaterialSpecifier],
- line_no: usize,
-) -> Option<&MaterialSpecifier>
-{
- material_specifiers
- .iter()
- .rev()
- .find(|material_specifier| material_specifier.line_no < line_no)
-}
-
-enum Either<ValA, ValB>
-{
- A(ValA),
- B(ValB),
-}
-
-impl<ValA, ValB, Item> Iterator for Either<ValA, ValB>
-where
- ValA: Iterator<Item = Item>,
- ValB: Iterator<Item = Item>,
-{
- type Item = Item;
-
- fn next(&mut self) -> Option<Self::Item>
- {
- match self {
- Self::A(iter_a) => iter_a.next(),
- Self::B(iter_b) => iter_b.next(),
- }
- }
-}
-
-fn get_vertex_positions_from_statements(
- statements: &[(usize, Statement<Keyword>)],
-) -> Result<Vec<Vec3<f32>>, Error>
-{
- statements
- .iter()
- .filter_map(|(line_no, statement)| {
- if statement.keyword != Keyword::V {
- return None;
- }
-
- if statement.arguments.len() == 4 {
- return Some(Err(Error::UnsupportedArgumentCount {
- keyword: statement.keyword.to_string(),
- arg_count: statement.arguments.len(),
- line_no: *line_no,
- }));
- }
-
- if statement.arguments.len() > 4 {
- return Some(Err(Error::InvalidArgumentCount {
- keyword: statement.keyword.to_string(),
- arg_count: statement.arguments.len(),
- line_no: *line_no,
- }));
- }
-
- let x = try_option!(statement.get_float_arg(0, *line_no));
- let y = try_option!(statement.get_float_arg(1, *line_no));
- let z = try_option!(statement.get_float_arg(2, *line_no));
-
- Some(Ok(Vec3 { x, y, z }))
- })
- .collect::<Result<Vec<_>, Error>>()
-}
-
-fn get_texture_positions_from_statements(
- statements: &[(usize, Statement<Keyword>)],
-) -> Result<Vec<Vec2<f32>>, Error>
-{
- statements
- .iter()
- .filter_map(|(line_no, statement)| {
- if statement.keyword != Keyword::Vt {
- return None;
- }
-
- if statement.arguments.len() == 3 {
- return Some(Err(Error::UnsupportedArgumentCount {
- keyword: statement.keyword.to_string(),
- arg_count: statement.arguments.len(),
- line_no: *line_no,
- }));
- }
-
- if statement.arguments.len() > 3 {
- return Some(Err(Error::InvalidArgumentCount {
- keyword: statement.keyword.to_string(),
- arg_count: statement.arguments.len(),
- line_no: *line_no,
- }));
- }
-
- let u = try_option!(statement.get_float_arg(0, *line_no));
- let v = try_option!(statement.get_float_arg(1, *line_no));
-
- Some(Ok(Vec2 { x: u, y: v }))
- })
- .collect::<Result<Vec<_>, Error>>()
-}
-
-fn get_vertex_normals_from_statements(
- statements: &[(usize, Statement<Keyword>)],
-) -> Result<Vec<Vec3<f32>>, Error>
-{
- statements
- .iter()
- .filter_map(|(line_no, statement)| {
- if statement.keyword != Keyword::Vn {
- return None;
- }
-
- if statement.arguments.len() > 3 {
- return Some(Err(Error::InvalidArgumentCount {
- keyword: statement.keyword.to_string(),
- arg_count: statement.arguments.len(),
- line_no: *line_no,
- }));
- }
-
- let i = try_option!(statement.get_float_arg(0, *line_no));
- let j = try_option!(statement.get_float_arg(1, *line_no));
- let k = try_option!(statement.get_float_arg(2, *line_no));
-
- Some(Ok(Vec3 { x: i, y: j, z: k }))
- })
- .collect::<Result<Vec<_>, Error>>()
-}
-
-fn get_material_specs_from_statements(
- statements: &[(usize, Statement<Keyword>)],
-) -> Result<Vec<MaterialSpecifier>, Error>
-{
- statements
- .iter()
- .filter_map(|(line_no, statement)| {
- if statement.keyword != Keyword::Usemtl {
- return None;
- }
-
- if statement.arguments.len() > 1 {
- return Some(Err(Error::InvalidArgumentCount {
- keyword: statement.keyword.to_string(),
- arg_count: statement.arguments.len(),
- line_no: *line_no,
- }));
- }
-
- let material_name = try_option!(statement.get_text_arg(0, *line_no));
-
- Some(Ok(MaterialSpecifier {
- material_name: material_name.to_string(),
- line_no: *line_no,
- }))
- })
- .collect::<Result<Vec<_>, Error>>()
-}
-
-fn get_faces_from_statements(
- statements: &[(usize, Statement<Keyword>)],
- material_specifiers: &[MaterialSpecifier],
-) -> Result<Vec<Face>, Error>
-{
- statements
- .iter()
- .filter_map(|(line_no, statement)| {
- if statement.keyword != Keyword::F {
- return None;
- }
-
- if statement.arguments.len() > 3 {
- return Some(Err(Error::UnsupportedArgumentCount {
- keyword: statement.keyword.to_string(),
- arg_count: statement.arguments.len(),
- line_no: *line_no,
- }));
- }
-
- let vertex_a = try_option!(statement.get_triplet_arg(0, *line_no)).into();
- let vertex_b = try_option!(statement.get_triplet_arg(1, *line_no)).into();
- let vertex_c = try_option!(statement.get_triplet_arg(2, *line_no)).into();
-
- let material_name =
- find_material_specifier_for_line_no(material_specifiers, *line_no)
- .map(|material_specifier| material_specifier.material_name.clone());
-
- Some(Ok(Face {
- vertices: [vertex_a, vertex_b, vertex_c],
- material_name,
- }))
- })
- .collect::<Result<Vec<Face>, Error>>()
-}
-
-fn get_mtl_libs_from_statements(
- statements: &[(usize, Statement<Keyword>)],
-) -> Result<Vec<Vec<PathBuf>>, Error>
-{
- statements
- .iter()
- .filter_map(|(line_no, statement)| {
- if statement.keyword != Keyword::Mtllib {
- return None;
- }
-
- let mtl_lib_paths = try_option!(statement
- .arguments
- .iter()
- .enumerate()
- .map(|(index, value)| Ok(PathBuf::from(value.to_text(index, *line_no)?)))
- .collect::<Result<Vec<_>, ParsingError>>());
-
- Some(Ok(mtl_lib_paths))
- })
- .collect::<Result<Vec<_>, Error>>()
-}
-
#[cfg(test)]
mod tests
{
@@ -550,17 +651,26 @@ mod tests
assert_eq!(obj.faces.len(), 3);
assert_eq!(
- obj.faces[0].material_name.as_ref().expect("Expected Some"),
+ obj.faces[0]
+ .material_name
+ .as_deref()
+ .expect("Expected Some"),
"dark-green"
);
assert_eq!(
- obj.faces[1].material_name.as_ref().expect("Expected Some"),
+ obj.faces[1]
+ .material_name
+ .as_deref()
+ .expect("Expected Some"),
"dark-green"
);
assert_eq!(
- obj.faces[2].material_name.as_ref().expect("Expected Some"),
+ obj.faces[2]
+ .material_name
+ .as_deref()
+ .expect("Expected Some"),
"light-pink"
);
}
diff --git a/engine/src/image.rs b/engine/src/image.rs
new file mode 100644
index 0000000..b01ea53
--- /dev/null
+++ b/engine/src/image.rs
@@ -0,0 +1,363 @@
+use std::fs::File;
+use std::io::{BufRead, BufReader, Seek};
+use std::marker::PhantomData;
+use std::ops::Deref;
+use std::path::Path;
+
+use image_rs::GenericImageView as _;
+
+use crate::color::{Pixel as ColorPixel, Rgb, Rgba};
+use crate::data_types::dimens::Dimens;
+use crate::vector::Vec2;
+
+#[derive(Debug, Clone)]
+pub struct Image
+{
+ inner: image_rs::DynamicImage,
+}
+
+impl Image
+{
+ pub fn open(path: impl AsRef<Path>) -> Result<Self, Error>
+ {
+ let buffered_reader = BufReader::new(File::open(&path).map_err(Error::Io)?);
+
+ Self::from_reader(
+ buffered_reader,
+ Format::from_image_rs(
+ image_rs::ImageFormat::from_path(path)
+ .map_err(|_| Error::UnsupportedFormat)?,
+ )
+ .ok_or(Error::UnsupportedFormat)?,
+ )
+ }
+
+ pub fn from_reader(reader: impl BufRead + Seek, format: Format)
+ -> Result<Self, Error>
+ {
+ let image_reader =
+ image_rs::ImageReader::with_format(reader, format.into_image_rs());
+
+ let image = match image_reader
+ .decode()
+ .map_err(|err| Error::DecodeFailed(DecodeError(err)))?
+ {
+ image @ (image_rs::DynamicImage::ImageLuma8(_)
+ | image_rs::DynamicImage::ImageLumaA8(_)
+ | image_rs::DynamicImage::ImageLuma16(_)
+ | image_rs::DynamicImage::ImageLumaA16(_)) => image.into_rgba32f().into(),
+ image => image,
+ };
+
+ Ok(Self { inner: image })
+ }
+
+ pub fn from_pixels<PixelT, Buf>(
+ pixels: PixelBuffer<PixelT, Buf>,
+ ) -> Result<Self, PixelBufferLenIncorrectForSize>
+ where
+ PixelT: ColorPixel,
+ Buf: Deref<Target = [PixelT::Component]>,
+ Self: TryFrom<PixelBuffer<PixelT, Buf>, Error = PixelBufferLenIncorrectForSize>,
+ {
+ Self::try_from(pixels)
+ }
+
+ pub fn from_color<PixelT: ColorPixel>(color: PixelT, size: Dimens<u32>) -> Self
+ where
+ Self: From<(PixelT, Dimens<u32>)>,
+ {
+ Self::from((color, size))
+ }
+
+ pub fn dimensions(&self) -> Dimens<u32>
+ {
+ self.inner.dimensions().into()
+ }
+
+ pub fn color_type(&self) -> ColorType
+ {
+ self.inner.color().into()
+ }
+
+ pub fn color_space_is_srgb(&self) -> bool
+ {
+ match self.inner.color_space().primaries {
+ image_rs::metadata::CicpColorPrimaries::SRgb => true,
+ _ => false,
+ }
+ }
+
+ /// Returns a immutable view of a subsection of this image.
+ /// The `offset` argument sets the position of the top left corner of the view.
+ ///
+ /// # Panics
+ /// Panics if the view would be out of bounds
+ pub fn sub_view(&self, offset: Vec2<u32>, size: Dimens<u32>) -> SubView<'_>
+ {
+ assert!(offset.x + size.width <= self.dimensions().width);
+ assert!(offset.y + size.height <= self.dimensions().height);
+
+ SubView { image: &self, offset, size }
+ }
+
+ pub fn to_rgba8(&self) -> Self
+ {
+ Self { inner: self.inner.to_rgba8().into() }
+ }
+
+ /// Consumes the image and returns a RGBA8 image. If it already is RGBA8, the image is
+ /// returned as is. If not, the image is converted to RGBA8.
+ pub fn into_rgba8(self) -> Self
+ {
+ Self {
+ inner: self.inner.into_rgba8().into(),
+ }
+ }
+
+ pub fn as_bytes(&self) -> &[u8]
+ {
+ self.inner.as_bytes()
+ }
+
+ pub fn into_bytes(self) -> Vec<u8>
+ {
+ self.inner.into_bytes()
+ }
+}
+
+impl From<(Rgb<u8>, Dimens<u32>)> for Image
+{
+ fn from((color, size): (Rgb<u8>, Dimens<u32>)) -> Self
+ {
+ Self {
+ inner: image_rs::RgbImage::from_pixel(
+ size.width,
+ size.height,
+ image_rs::Rgb([color.r, color.g, color.b]),
+ )
+ .into(),
+ }
+ }
+}
+
+impl From<(Rgba<u8>, Dimens<u32>)> for Image
+{
+ fn from((color, size): (Rgba<u8>, Dimens<u32>)) -> Self
+ {
+ Self {
+ inner: image_rs::RgbaImage::from_pixel(
+ size.width,
+ size.height,
+ image_rs::Rgba([color.r, color.g, color.b, color.a]),
+ )
+ .into(),
+ }
+ }
+}
+
+macro_rules! gen_try_from_pixel_buffer_impl {
+ ($pixel: ident<$component: ty>) => {
+ impl<Buf> TryFrom<PixelBuffer<$pixel<$component>, Buf>> for Image
+ where
+ Buf: Deref<Target = [$component]>,
+ {
+ type Error = PixelBufferLenIncorrectForSize;
+
+ fn try_from(
+ pixels: PixelBuffer<$pixel<$component>, Buf>,
+ ) -> Result<Self, Self::Error>
+ {
+ Ok(
+ Image {
+ inner: image_rs::ImageBuffer::<
+ image_rs::$pixel<$component>,
+ Vec<_>,
+ >::from_raw(
+ pixels.size.width,
+ pixels.size.height,
+ pixels.buf.deref().to_vec(),
+ )
+ .ok_or(PixelBufferLenIncorrectForSize)?
+ .into(),
+ },
+ )
+ }
+ }
+ };
+}
+
+gen_try_from_pixel_buffer_impl!(Rgb<u8>);
+gen_try_from_pixel_buffer_impl!(Rgb<u16>);
+gen_try_from_pixel_buffer_impl!(Rgb<f32>);
+
+gen_try_from_pixel_buffer_impl!(Rgba<u8>);
+gen_try_from_pixel_buffer_impl!(Rgba<u16>);
+gen_try_from_pixel_buffer_impl!(Rgba<f32>);
+
+#[derive(Debug)]
+pub struct PixelBuffer<PixelT, Buf>
+where
+ PixelT: ColorPixel,
+ Buf: Deref<Target = [PixelT::Component]>,
+{
+ buf: Buf,
+ size: Dimens<u32>,
+ _pd: PhantomData<PixelT>,
+}
+
+impl<PixelT, Buf> PixelBuffer<PixelT, Buf>
+where
+ PixelT: ColorPixel,
+ Buf: Deref<Target = [PixelT::Component]>,
+{
+ pub fn new(buf: Buf, size: Dimens<u32>) -> Self
+ {
+ Self { buf, size, _pd: PhantomData }
+ }
+}
+
+/// An enumeration over supported color types and bit depths
+#[derive(Copy, PartialEq, Eq, Debug, Clone, Hash)]
+#[non_exhaustive]
+pub enum ColorType
+{
+ /// Pixel contains 8-bit R, G and B channels
+ Rgb8,
+
+ /// Pixel is 8-bit RGB with an alpha channel
+ Rgba8,
+
+ /// Pixel is 16-bit RGB
+ Rgb16,
+
+ /// Pixel is 16-bit RGBA
+ Rgba16,
+
+ /// Pixel is 32-bit float RGB
+ Rgb32F,
+
+ /// Pixel is 32-bit float RGBA
+ Rgba32F,
+}
+
+impl From<image_rs::ColorType> for ColorType
+{
+ fn from(color_type: image_rs::ColorType) -> Self
+ {
+ match color_type {
+ image_rs::ColorType::Rgb8 => Self::Rgb8,
+ image_rs::ColorType::Rgba8 => Self::Rgba8,
+ image_rs::ColorType::Rgb16 => Self::Rgb16,
+ image_rs::ColorType::Rgba16 => Self::Rgba16,
+ image_rs::ColorType::Rgb32F => Self::Rgb32F,
+ image_rs::ColorType::Rgba32F => Self::Rgba32F,
+ image_rs::ColorType::L8
+ | image_rs::ColorType::La8
+ | image_rs::ColorType::L16
+ | image_rs::ColorType::La16 => unimplemented!(),
+ _ => {
+ panic!("Unrecognized image_rs::ColorType variant");
+ }
+ }
+ }
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
+#[non_exhaustive]
+pub enum Format
+{
+ Png,
+ Jpeg,
+ Ico,
+}
+
+impl Format
+{
+ fn into_image_rs(self) -> image_rs::ImageFormat
+ {
+ match self {
+ Self::Png => image_rs::ImageFormat::Png,
+ Self::Jpeg => image_rs::ImageFormat::Jpeg,
+ Self::Ico => image_rs::ImageFormat::Ico,
+ }
+ }
+
+ fn from_image_rs(format: image_rs::ImageFormat) -> Option<Self>
+ {
+ match format {
+ image_rs::ImageFormat::Png => Some(Self::Png),
+ image_rs::ImageFormat::Jpeg => Some(Self::Jpeg),
+ image_rs::ImageFormat::Ico => Some(Self::Ico),
+ _ => None,
+ }
+ }
+}
+
+pub struct SubView<'image>
+{
+ image: &'image Image,
+ offset: Vec2<u32>,
+ size: Dimens<u32>,
+}
+
+impl SubView<'_>
+{
+ pub fn to_image(&self) -> Image
+ {
+ fn create_sub_image<Image>(
+ image: Image,
+ sub_view: &SubView<'_>,
+ ) -> image_rs::SubImage<Image>
+ {
+ let SubView { image: _, offset, size } = sub_view;
+
+ image_rs::SubImage::new(image, offset.x, offset.y, size.width, size.height)
+ }
+
+ Image {
+ inner: match &self.image.inner {
+ image_rs::DynamicImage::ImageRgb8(image) => {
+ create_sub_image(image, self).to_image().into()
+ }
+ image_rs::DynamicImage::ImageRgba8(image) => {
+ create_sub_image(image, self).to_image().into()
+ }
+ image_rs::DynamicImage::ImageRgb16(image) => {
+ create_sub_image(image, self).to_image().into()
+ }
+ image_rs::DynamicImage::ImageRgba16(image) => {
+ create_sub_image(image, self).to_image().into()
+ }
+ image_rs::DynamicImage::ImageRgb32F(image) => {
+ create_sub_image(image, self).to_image().into()
+ }
+ image_rs::DynamicImage::ImageRgba32F(image) => {
+ create_sub_image(image, self).to_image().into()
+ }
+ _ => unimplemented!(),
+ },
+ }
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error
+{
+ #[error("I/O error")]
+ Io(#[source] std::io::Error),
+
+ #[error("Failed to decode image")]
+ DecodeFailed(DecodeError),
+
+ #[error("Unsupported image format")]
+ UnsupportedFormat,
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error(transparent)]
+pub struct DecodeError(image_rs::ImageError);
+
+#[derive(Debug, thiserror::Error)]
+#[error("Length of pixel buffer is incorrect for the given image size")]
+pub struct PixelBufferLenIncorrectForSize;
diff --git a/engine/src/input.rs b/engine/src/input.rs
index f4166f6..c5e6d09 100644
--- a/engine/src/input.rs
+++ b/engine/src/input.rs
@@ -1,242 +1,26 @@
-use std::collections::HashMap;
+use crate::ecs::extension::Collector as ExtensionCollector;
+use crate::ecs::pair::ChildOf;
+use crate::ecs::phase::Phase;
+use crate::ecs::{declare_entity, pair};
+use crate::windowing::PHASE as WINDOWING_PHASE;
-use ecs::extension::Collector as ExtensionCollector;
-use ecs::sole::Single;
-use ecs::Sole;
+pub mod keyboard;
+pub mod mouse;
-use crate::event::{
- PostPresent as PostPresentEvent,
- PreUpdate as PreUpdateEvent,
- Start as StartEvent,
-};
-use crate::vector::Vec2;
-use crate::window::Window;
-
-mod reexports
-{
- pub use crate::window::{Key, KeyState};
-}
-
-pub use reexports::*;
-
-#[derive(Debug, Sole)]
-pub struct Keys
-{
- map: HashMap<Key, KeyData>,
-}
-
-impl Keys
-{
- #[must_use]
- pub fn new() -> Self
- {
- Self {
- map: Key::KEYS
- .iter()
- .map(|key| {
- (
- *key,
- KeyData {
- state: KeyState::Released,
- prev_tick_state: KeyState::Released,
- },
- )
- })
- .collect(),
- }
- }
-
- #[must_use]
- pub fn get_key_state(&self, key: Key) -> KeyState
- {
- let Some(key_data) = self.map.get(&key) else {
- unreachable!();
- };
-
- key_data.state
- }
-
- #[must_use]
- pub fn get_prev_key_state(&self, key: Key) -> KeyState
- {
- let Some(key_data) = self.map.get(&key) else {
- unreachable!();
- };
-
- key_data.prev_tick_state
- }
-
- pub fn set_key_state(&mut self, key: Key, new_key_state: KeyState)
- {
- if matches!(new_key_state, KeyState::Repeat) {
- return;
- }
-
- let Some(key_data) = self.map.get_mut(&key) else {
- unreachable!();
- };
-
- key_data.state = new_key_state;
- }
-
- #[must_use]
- pub fn is_anything_pressed(&self) -> bool
- {
- self.map
- .values()
- .any(|key_data| matches!(key_data.state, KeyState::Pressed))
- }
-}
-
-impl Default for Keys
-{
- fn default() -> Self
- {
- Self::new()
- }
-}
-
-#[derive(Debug, Default, Clone, Sole)]
-pub struct Cursor
-{
- pub position: Vec2<f64>,
- pub has_moved: bool,
-}
-
-#[derive(Debug, Clone, Sole)]
-pub struct CursorFlags
-{
- /// This flag is set in two situations:
- /// A: The window has just started
- /// B: The window has gained focus again after losing focus.
- ///
- /// This flag only lasts a single tick then it is cleared (at the beginning of the
- /// next tick).
- pub is_first_move: CursorFlag,
-}
-
-impl Default for CursorFlags
-{
- fn default() -> Self
- {
- Self {
- is_first_move: CursorFlag { flag: true, ..Default::default() },
- }
- }
-}
-
-#[derive(Debug, Default, Clone)]
-pub struct CursorFlag
-{
- pub flag: bool,
- pub pending_clear: bool,
-}
-
-impl CursorFlag
-{
- pub fn clear(&mut self)
- {
- self.flag = false;
- self.pending_clear = false;
- }
+declare_entity! {
+pub PHASE: (Phase, pair!(ChildOf, { *WINDOWING_PHASE }));
}
/// Input extension.
#[derive(Debug, Default)]
pub struct Extension {}
-impl ecs::extension::Extension for Extension
+impl crate::ecs::extension::Extension for Extension
{
fn collect(self, mut collector: ExtensionCollector<'_>)
{
- collector.add_system(StartEvent, initialize);
- collector.add_system(PreUpdateEvent, maybe_clear_cursor_is_first_move);
- collector.add_system(PostPresentEvent, set_keys_prev_tick_state);
+ collector.spawn_declared_entity(&PHASE);
- collector.add_sole(Keys::default()).ok();
- collector.add_sole(Cursor::default()).ok();
- collector.add_sole(CursorFlags::default()).ok();
+ // TODO: Add input mapping
}
}
-
-fn initialize(
- keys: Single<Keys>,
- cursor: Single<Cursor>,
- cursor_flags: Single<CursorFlags>,
- window: Single<Window>,
-)
-{
- let keys_weak_ref = keys.to_weak_ref();
-
- window.set_key_callback(move |key, _scancode, key_state, _modifiers| {
- let keys_ref = keys_weak_ref.access().expect("No world");
-
- let mut keys = keys_ref.to_single();
-
- keys.set_key_state(key, key_state);
- });
-
- let cursor_weak_ref = cursor.to_weak_ref();
-
- window.set_cursor_pos_callback(move |cursor_position| {
- let cursor_ref = cursor_weak_ref.access().expect("No world");
-
- let mut cursor = cursor_ref.to_single();
-
- cursor.position = Vec2 {
- x: cursor_position.x,
- y: cursor_position.y,
- };
-
- cursor.has_moved = true;
- });
-
- let cursor_flags_weak_ref = cursor_flags.to_weak_ref();
-
- window.set_focus_callback(move |is_focused| {
- #[cfg(feature = "debug")]
- tracing::trace!("Window is focused: {is_focused}");
-
- let cursor_flags_ref = cursor_flags_weak_ref.access().expect("No world");
-
- cursor_flags_ref.to_single().is_first_move.flag = is_focused;
- });
-}
-
-fn maybe_clear_cursor_is_first_move(
- cursor: Single<Cursor>,
- mut cursor_flags: Single<CursorFlags>,
-)
-{
- if cursor_flags.is_first_move.pending_clear {
- #[cfg(feature = "debug")]
- tracing::trace!("Clearing is_first_move");
-
- // This flag was set for the whole previous tick so it can be cleared now
- cursor_flags.is_first_move.clear();
-
- return;
- }
-
- if cursor.has_moved && cursor_flags.is_first_move.flag {
- #[cfg(feature = "debug")]
- tracing::trace!("Setting flag to clear is_first_move next tick");
-
- // Make this system clear is_first_move the next time it runs
- cursor_flags.is_first_move.pending_clear = true;
- }
-}
-
-fn set_keys_prev_tick_state(mut keys: Single<Keys>)
-{
- for key_data in keys.map.values_mut() {
- key_data.prev_tick_state = key_data.state;
- }
-}
-
-#[derive(Debug)]
-struct KeyData
-{
- state: KeyState,
- prev_tick_state: KeyState,
-}
diff --git a/engine/src/input/keyboard.rs b/engine/src/input/keyboard.rs
new file mode 100644
index 0000000..d226df0
--- /dev/null
+++ b/engine/src/input/keyboard.rs
@@ -0,0 +1,6 @@
+mod reexports
+{
+ pub use crate::windowing::keyboard::{Key, KeyState, Keyboard};
+}
+
+pub use reexports::*;
diff --git a/engine/src/input/mouse.rs b/engine/src/input/mouse.rs
new file mode 100644
index 0000000..c10aaf2
--- /dev/null
+++ b/engine/src/input/mouse.rs
@@ -0,0 +1,6 @@
+mod reexports
+{
+ pub use crate::windowing::mouse::{Button, ButtonState, Buttons, Mouse};
+}
+
+pub use reexports::*;
diff --git a/engine/src/lib.rs b/engine/src/lib.rs
index abf26f5..36affff 100644
--- a/engine/src/lib.rs
+++ b/engine/src/lib.rs
@@ -1,61 +1,45 @@
#![deny(clippy::all, clippy::pedantic)]
#![allow(clippy::needless_pass_by_value)]
-use ecs::component::Sequence as ComponentSequence;
-use ecs::event::component::TypeTransformComponentsToAddedEvents;
-use ecs::event::{Event, Sequence as EventSequence};
-use ecs::extension::Extension;
-use ecs::sole::Sole;
-use ecs::system::{Into, System};
-use ecs::tuple::Reduce as TupleReduce;
-use ecs::uid::Uid;
-use ecs::{SoleAlreadyExistsError, World};
-
-use crate::delta_time::{update as update_delta_time, DeltaTime, LastUpdate};
-use crate::event::{
- Conclude as ConcludeEvent,
- PostPresent as PostPresentEvent,
- PreUpdate as PreUpdateEvent,
- Present as PresentEvent,
- Update as UpdateEvent,
-};
-
-mod opengl;
-mod shader_preprocessor;
+use crate::asset::{Assets, Extension as AssetExtension};
+use crate::ecs::error::HandlerFn as ErrorHandlerFn;
+use crate::ecs::extension::Extension;
+use crate::ecs::World;
+
mod util;
+mod work_queue;
+pub mod asset;
pub mod camera;
+pub mod collision;
pub mod data_types;
-pub mod delta_time;
pub mod draw_flags;
-pub mod event;
pub mod file_format;
+pub mod image;
pub mod input;
pub mod lighting;
pub mod material;
pub mod math;
pub mod mesh;
-pub mod performance;
+pub mod model;
pub mod projection;
-pub mod renderer;
-pub mod shader;
+pub mod reflection;
+pub mod rendering;
+pub mod scene;
+pub mod sky_box;
pub mod texture;
pub mod transform;
-pub mod vertex;
-pub mod window;
+pub mod ui;
+pub mod windowing;
+
+pub extern crate engine_ecs as ecs;
-pub extern crate ecs;
+pub use ecs::error::Error;
pub(crate) use crate::data_types::matrix;
pub use crate::data_types::{color, vector};
-type EventOrder = (
- PreUpdateEvent,
- UpdateEvent,
- PresentEvent,
- PostPresentEvent,
- ConcludeEvent,
-);
+const INITIAL_ASSET_CAPACITY: usize = 128;
#[derive(Debug)]
pub struct Engine
@@ -71,53 +55,34 @@ impl Engine
{
let mut world = World::new();
- world.add_sole(DeltaTime::default()).ok();
+ let mut assets = Assets::with_capacity(INITIAL_ASSET_CAPACITY);
- world.register_system(
- PreUpdateEvent,
- update_delta_time
- .into_system()
- .initialize((LastUpdate::default(),)),
- );
+ crate::model::asset::add_importers(&mut assets);
+ crate::material::asset::add_importers(&mut assets);
- Self { world }
- }
+ crate::texture::initialize(&mut assets);
- pub fn spawn<Comps>(&mut self, components: Comps) -> Uid
- where
- Comps: ComponentSequence + TupleReduce<TypeTransformComponentsToAddedEvents>,
- Comps::Out: EventSequence,
- {
- self.world.create_entity(components)
- }
+ world.add_extension(AssetExtension { assets });
- pub fn register_system<'this, SystemImpl>(
- &'this mut self,
- event: impl Event,
- system: impl System<'this, SystemImpl>,
- )
- {
- self.world.register_system(event, system);
+ Self { world }
}
- /// Adds a globally shared singleton value.
- ///
- /// # Errors
- /// Returns `Err` if this [`Sole`] has already been added.
- pub fn add_sole(&mut self, sole: impl Sole) -> Result<(), SoleAlreadyExistsError>
+ pub fn with_error_handler(mut self, error_handler: ErrorHandlerFn) -> Self
{
- self.world.add_sole(sole)
+ self.world.set_err_handler(error_handler);
+ self
}
- pub fn add_extension(&mut self, extension: impl Extension)
+ pub fn with_extension(mut self, extension: impl Extension) -> Self
{
self.world.add_extension(extension);
+ self
}
/// Runs the event loop.
- pub fn start(&self)
+ pub fn start(&mut self)
{
- self.world.event_loop::<EventOrder>();
+ self.world.start_loop();
}
}
@@ -128,3 +93,10 @@ impl Default for Engine
Self::new()
}
}
+
+#[macro_export]
+macro_rules! error {
+ ($($tt: tt)*) => {
+ $crate::ecs::error!($($tt)*)
+ };
+}
diff --git a/engine/src/lighting.rs b/engine/src/lighting.rs
index 48adb0e..5314e58 100644
--- a/engine/src/lighting.rs
+++ b/engine/src/lighting.rs
@@ -1,16 +1,17 @@
-use ecs::{Component, Sole};
-
-use crate::color::Color;
+use crate::builder;
+use crate::color::{Color, Rgb};
use crate::data_types::vector::Vec3;
-use crate::util::builder;
+use crate::ecs::Component;
+use crate::reflection::Reflection;
builder! {
#[builder(name = PointLightBuilder, derives = (Debug, Clone))]
-#[derive(Debug, Clone, Component)]
+#[derive(Debug, Clone, Component, Reflection)]
#[non_exhaustive]
pub struct PointLight
{
- pub position: Vec3<f32>,
+ /// Position in local space.
+ pub local_position: Vec3<f32>,
pub diffuse: Color<f32>,
pub specular: Color<f32>,
pub attenuation_params: AttenuationParams,
@@ -31,9 +32,9 @@ impl Default for PointLight
fn default() -> Self
{
Self {
- position: Vec3::default(),
- diffuse: Color { red: 0.5, green: 0.5, blue: 0.5 },
- specular: Color { red: 1.0, green: 1.0, blue: 1.0 },
+ local_position: Vec3::default(),
+ diffuse: Color::Rgb(Rgb { r: 0.5, g: 0.5, b: 0.5 }),
+ specular: Color::Rgb(Rgb::<f32>::white()),
attenuation_params: AttenuationParams::default(),
}
}
@@ -48,7 +49,7 @@ impl Default for PointLightBuilder
}
/// Parameters for light [attenuation](https://en.wikipedia.org/wiki/Attenuation).
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Reflection)]
pub struct AttenuationParams
{
pub constant: f32,
@@ -58,7 +59,6 @@ pub struct AttenuationParams
impl Default for AttenuationParams
{
- #[must_use]
fn default() -> Self
{
Self {
@@ -70,8 +70,8 @@ impl Default for AttenuationParams
}
builder! {
-#[builder(name = DirectionalLightBuilder, derives = (Debug, Default, Clone))]
-#[derive(Debug, Default, Clone, Component)]
+#[builder(name = DirectionalLightBuilder, derives = (Debug, Clone))]
+#[derive(Debug, Clone, Component, Reflection)]
#[non_exhaustive]
pub struct DirectionalLight
{
@@ -90,22 +90,58 @@ impl DirectionalLight
}
}
+impl Default for DirectionalLight
+{
+ fn default() -> Self
+ {
+ Self::builder().build()
+ }
+}
+
+impl Default for DirectionalLightBuilder
+{
+ fn default() -> Self
+ {
+ Self {
+ diffuse: Color::Rgb(Rgb { r: 0.5, g: 0.5, b: 0.5 }),
+ specular: Color::Rgb(Rgb::<f32>::white()),
+ direction: Vec3::LEFT,
+ }
+ }
+}
+
builder! {
-/// Global light properties.
-#[builder(name = GlobalLightBuilder, derives = (Debug, Clone, Default))]
-#[derive(Debug, Clone, Default, Sole)]
+#[builder(name = EnvironmentalBuilder, derives = (Debug, Clone))]
+#[derive(Debug, Clone, Component, Reflection)]
#[non_exhaustive]
-pub struct GlobalLight
+pub struct Environmental
{
- pub ambient: Color<f32>,
+ pub ambient_color: Color<f32>,
}
}
-impl GlobalLight
+impl Environmental
{
- #[must_use]
- pub fn builder() -> GlobalLightBuilder
+ pub fn builder() -> EnvironmentalBuilder
+ {
+ EnvironmentalBuilder::default()
+ }
+}
+
+impl Default for Environmental
+{
+ fn default() -> Self
{
- GlobalLightBuilder::default()
+ Self::builder().build()
+ }
+}
+
+impl Default for EnvironmentalBuilder
+{
+ fn default() -> Self
+ {
+ Self {
+ ambient_color: Color::Rgb(Rgb { r: 0.2, g: 0.2, b: 0.2 }),
+ }
}
}
diff --git a/engine/src/material.rs b/engine/src/material.rs
index aae6003..af982f4 100644
--- a/engine/src/material.rs
+++ b/engine/src/material.rs
@@ -1,35 +1,58 @@
-use ecs::Component;
+use crate::asset::Handle as AssetHandle;
+use crate::builder;
+use crate::color::{Color, Rgb};
+use crate::ecs::Component;
+use crate::reflection::Reflection;
+use crate::texture::Texture;
-use crate::color::Color;
-use crate::data_types::dimens::Dimens;
-use crate::texture::{Id as TextureId, Texture};
-use crate::util::builder;
+pub mod asset;
-#[derive(Debug, Clone, Component)]
+#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Material
{
pub ambient: Color<f32>,
pub diffuse: Color<f32>,
pub specular: Color<f32>,
- pub ambient_map: TextureId,
- pub diffuse_map: TextureId,
- pub specular_map: TextureId,
- pub textures: Vec<Texture>,
+ pub ambient_map: Option<AssetHandle<Texture>>,
+ pub diffuse_map: Option<AssetHandle<Texture>>,
+ pub specular_map: Option<AssetHandle<Texture>>,
pub shininess: f32,
}
+impl Material
+{
+ pub fn builder() -> Builder
+ {
+ Builder::new()
+ }
+
+ pub fn textures(&self) -> impl Iterator<Item = &AssetHandle<Texture>>
+ {
+ [&self.ambient_map, &self.diffuse_map, &self.specular_map]
+ .into_iter()
+ .flatten()
+ }
+}
+
+impl Default for Material
+{
+ fn default() -> Self
+ {
+ Self::builder().build()
+ }
+}
+
/// [`Material`] builder.
#[derive(Debug, Clone)]
pub struct Builder
{
- ambient: Option<Color<f32>>,
- diffuse: Option<Color<f32>>,
- specular: Option<Color<f32>>,
- ambient_map: Option<TextureId>,
- diffuse_map: Option<TextureId>,
- specular_map: Option<TextureId>,
- textures: Vec<Texture>,
+ ambient: Color<f32>,
+ diffuse: Color<f32>,
+ specular: Color<f32>,
+ ambient_map: Option<AssetHandle<Texture>>,
+ diffuse_map: Option<AssetHandle<Texture>>,
+ specular_map: Option<AssetHandle<Texture>>,
shininess: f32,
}
@@ -39,13 +62,12 @@ impl Builder
pub fn new() -> Self
{
Self {
- ambient: None,
- diffuse: None,
- specular: None,
+ ambient: Color::Rgb(Rgb::<f32>::white()),
+ diffuse: Color::Rgb(Rgb::<f32>::white()),
+ specular: Color::Rgb(Rgb::<f32>::white()),
ambient_map: None,
diffuse_map: None,
specular_map: None,
- textures: Vec::new(),
shininess: 32.0,
}
}
@@ -53,7 +75,7 @@ impl Builder
#[must_use]
pub fn ambient(mut self, ambient: Color<f32>) -> Self
{
- self.ambient = Some(ambient);
+ self.ambient = ambient;
self
}
@@ -61,7 +83,7 @@ impl Builder
#[must_use]
pub fn diffuse(mut self, diffuse: Color<f32>) -> Self
{
- self.diffuse = Some(diffuse);
+ self.diffuse = diffuse;
self
}
@@ -69,13 +91,13 @@ impl Builder
#[must_use]
pub fn specular(mut self, specular: Color<f32>) -> Self
{
- self.specular = Some(specular);
+ self.specular = specular;
self
}
#[must_use]
- pub fn ambient_map(mut self, ambient_map: TextureId) -> Self
+ pub fn ambient_map(mut self, ambient_map: AssetHandle<Texture>) -> Self
{
self.ambient_map = Some(ambient_map);
@@ -83,7 +105,7 @@ impl Builder
}
#[must_use]
- pub fn diffuse_map(mut self, diffuse_map: TextureId) -> Self
+ pub fn diffuse_map(mut self, diffuse_map: AssetHandle<Texture>) -> Self
{
self.diffuse_map = Some(diffuse_map);
@@ -91,7 +113,7 @@ impl Builder
}
#[must_use]
- pub fn specular_map(mut self, specular_map: TextureId) -> Self
+ pub fn specular_map(mut self, specular_map: AssetHandle<Texture>) -> Self
{
self.specular_map = Some(specular_map);
@@ -99,22 +121,6 @@ impl Builder
}
#[must_use]
- pub fn textures(mut self, textures: impl IntoIterator<Item = Texture>) -> Self
- {
- self.textures = textures.into_iter().collect();
-
- self
- }
-
- #[must_use]
- pub fn texture(mut self, texture: Texture) -> Self
- {
- self.textures.push(texture);
-
- self
- }
-
- #[must_use]
pub fn shininess(mut self, shininess: f32) -> Self
{
self.shininess = shininess;
@@ -127,43 +133,15 @@ impl Builder
/// # Panics
/// Will panic if no ambient map, diffuse map or specular map is set.
#[must_use]
- pub fn build(mut self) -> Material
+ pub const fn build(self) -> Material
{
- let ambient_map = self.ambient_map.unwrap_or_else(|| {
- let texture = create_1x1_white_texture();
- let texture_id = texture.id();
-
- self.textures.push(texture);
-
- texture_id
- });
-
- let diffuse_map = self.diffuse_map.unwrap_or_else(|| {
- let texture = create_1x1_white_texture();
- let texture_id = texture.id();
-
- self.textures.push(texture);
-
- texture_id
- });
-
- let specular_map = self.specular_map.unwrap_or_else(|| {
- let texture = create_1x1_white_texture();
- let texture_id = texture.id();
-
- self.textures.push(texture);
-
- texture_id
- });
-
Material {
- ambient: self.ambient.unwrap_or(Color::WHITE_F32),
- diffuse: self.diffuse.unwrap_or(Color::WHITE_F32),
- specular: self.specular.unwrap_or(Color::WHITE_F32),
- ambient_map,
- diffuse_map,
- specular_map,
- textures: self.textures,
+ ambient: self.ambient,
+ diffuse: self.diffuse,
+ specular: self.specular,
+ ambient_map: self.ambient_map,
+ diffuse_map: self.diffuse_map,
+ specular_map: self.specular_map,
shininess: self.shininess,
}
}
@@ -179,8 +157,8 @@ impl Default for Builder
builder! {
/// Material flags.
-#[builder(name = FlagsBuilder, derives = (Debug, Default, Clone))]
-#[derive(Debug, Default, Clone, Component)]
+#[builder(name = FlagsBuilder, derives = (Debug, Clone))]
+#[derive(Debug, Clone, Component, Reflection)]
#[non_exhaustive]
pub struct Flags
{
@@ -193,13 +171,32 @@ pub struct Flags
impl Flags
{
#[must_use]
- pub fn builder() -> FlagsBuilder
+ pub const fn builder() -> FlagsBuilder
{
- FlagsBuilder::default()
+ FlagsBuilder::new()
}
}
-fn create_1x1_white_texture() -> Texture
+impl Default for Flags
{
- Texture::new_from_color(&Dimens { width: 1, height: 1 }, &Color::WHITE_U8)
+ fn default() -> Self
+ {
+ Self::builder().build()
+ }
+}
+
+impl FlagsBuilder
+{
+ pub const fn new() -> Self
+ {
+ Self { use_ambient_color: false }
+ }
+}
+
+impl Default for FlagsBuilder
+{
+ fn default() -> Self
+ {
+ Self::new()
+ }
}
diff --git a/engine/src/material/asset.rs b/engine/src/material/asset.rs
new file mode 100644
index 0000000..f87b768
--- /dev/null
+++ b/engine/src/material/asset.rs
@@ -0,0 +1,82 @@
+use std::borrow::Cow;
+use std::collections::HashMap;
+use std::fs::read_to_string;
+use std::path::{Path, PathBuf};
+
+use crate::asset::{Assets, Handle as AssetHandle, Submitter as AssetSubmitter};
+use crate::material::Material;
+
+#[derive(Debug, Clone)]
+pub struct Map
+{
+ pub assets: HashMap<Cow<'static, str>, AssetHandle<Material>>,
+}
+
+/// Material asset import settings.
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct Settings {}
+
+pub fn add_importers(assets: &mut Assets)
+{
+ assets.set_importer(["mtl"], import_wavefront_mtl_asset);
+}
+
+fn import_wavefront_mtl_asset(
+ asset_submitter: &mut AssetSubmitter<'_>,
+ path: &Path,
+ _settings: Option<&'_ Settings>,
+) -> Result<(), Error>
+{
+ let parent_path = path
+ .parent()
+ .ok_or_else(|| Error::InvalidPath(path.to_path_buf()))?;
+
+ let named_materials = crate::file_format::wavefront::mtl::parse(
+ &read_to_string(path)
+ .map_err(|err| Error::ReadFailed(err, path.to_path_buf()))?,
+ )?;
+
+ for material in named_materials {
+ let mut material_builder = Material::builder()
+ .ambient(material.ambient)
+ .diffuse(material.diffuse)
+ .specular(material.specular)
+ .shininess(material.shininess);
+
+ if let Some(ambient_map) = material.ambient_map {
+ material_builder = material_builder.ambient_map(
+ asset_submitter.submit_load_other(parent_path.join(&ambient_map.path)),
+ );
+ }
+
+ if let Some(diffuse_map) = material.diffuse_map {
+ material_builder = material_builder.diffuse_map(
+ asset_submitter.submit_load_other(parent_path.join(&diffuse_map.path)),
+ );
+ }
+
+ if let Some(specular_map) = material.specular_map {
+ material_builder = material_builder.specular_map(
+ asset_submitter.submit_load_other(parent_path.join(&specular_map.path)),
+ );
+ }
+
+ asset_submitter.submit_store_named(material.name, material_builder.build());
+ }
+
+ Ok(())
+}
+
+#[derive(Debug, thiserror::Error)]
+enum Error
+{
+ #[error("Invalid path '{}'", .0.display())]
+ InvalidPath(PathBuf),
+
+ #[error("Failed to read file {}", .1.display())]
+ ReadFailed(#[source] std::io::Error, PathBuf),
+
+ #[error(transparent)]
+ Other(#[from] crate::file_format::wavefront::mtl::Error),
+}
diff --git a/engine/src/math.rs b/engine/src/math.rs
index b86e760..eeed61e 100644
--- a/engine/src/math.rs
+++ b/engine/src/math.rs
@@ -5,13 +5,13 @@ use crate::vector::Vec3;
/// Calculates the surface normal of a triangle.
#[must_use]
pub fn calc_triangle_surface_normal(
- egde_a: &Vec3<f32>,
- edge_b: &Vec3<f32>,
- edge_c: &Vec3<f32>,
+ egde_a: Vec3<f32>,
+ edge_b: Vec3<f32>,
+ edge_c: Vec3<f32>,
) -> Vec3<f32>
{
let v1 = edge_b - egde_a;
let v2 = edge_c - egde_a;
- v1.cross(&v2)
+ v1.cross(&v2).normalize()
}
diff --git a/engine/src/mesh.rs b/engine/src/mesh.rs
index de0af70..c1c0f77 100644
--- a/engine/src/mesh.rs
+++ b/engine/src/mesh.rs
@@ -1,28 +1,253 @@
-use ecs::Component;
+use std::alloc::Layout;
-use crate::vertex::Vertex;
+use zerocopy::IntoBytes;
-pub mod cube;
+use crate::data_types::dimens::Dimens3;
+use crate::mesh::vertex_buffer::{VertexAttrProperties, VertexLabel};
+use crate::vector::{Vec2, Vec3};
-#[derive(Debug, Clone, Component)]
+pub mod vertex_buffer;
+
+#[derive(Debug, Clone)]
pub struct Mesh
{
- vertices: Vec<Vertex>,
+ vertex_buf: vertex_buffer::VertexBuffer,
indices: Option<Vec<u32>>,
}
impl Mesh
{
- #[must_use]
- pub fn new(vertices: Vec<Vertex>, indices: Option<Vec<u32>>) -> Self
+ pub fn cube(size: Dimens3<f32>) -> Self
{
- Self { vertices, indices }
+ Self::cube_with_options(CubeOptions::default().size(size))
}
- #[must_use]
- pub fn vertices(&self) -> &[Vertex]
+ pub fn cube_with_options(options: CubeOptions) -> Self
+ {
+ let CubeOptions {
+ size,
+ top_face:
+ CubeFaceOptions {
+ uv_upper_left: top_uv_upper_left,
+ uv_lower_right: top_uv_lower_right,
+ },
+ bottom_face:
+ CubeFaceOptions {
+ uv_upper_left: bot_uv_upper_left,
+ uv_lower_right: bot_uv_lower_right,
+ },
+ left_face:
+ CubeFaceOptions {
+ uv_upper_left: left_uv_upper_left,
+ uv_lower_right: left_uv_lower_right,
+ },
+ right_face:
+ CubeFaceOptions {
+ uv_upper_left: right_uv_upper_left,
+ uv_lower_right: right_uv_lower_right,
+ },
+ back_face:
+ CubeFaceOptions {
+ uv_upper_left: back_uv_upper_left,
+ uv_lower_right: back_uv_lower_right,
+ },
+ front_face:
+ CubeFaceOptions {
+ uv_upper_left: front_uv_upper_left,
+ uv_lower_right: front_uv_lower_right,
+ },
+ } = options;
+
+ let half_w = size.width / 2.0;
+ let half_h = size.height / 2.0;
+ let half_d = size.depth / 2.0;
+
+ #[inline(always)]
+ fn vertex(pos: [f32; 3], uv: [f32; 2], normal: Vec3<f32>) -> [f32; 8]
+ {
+ let [pos_x, pos_y, pos_z] = pos;
+ let [uv_x, uv_y] = uv;
+
+ [
+ pos_x, pos_y, pos_z, uv_x, uv_y, normal.x, normal.y, normal.z,
+ ]
+ }
+
+ let top_vertices = [
+ (
+ [half_w, half_h, half_d],
+ [top_uv_upper_left.x, top_uv_upper_left.y],
+ ),
+ (
+ [-half_w, half_h, half_d],
+ [top_uv_lower_right.x, top_uv_upper_left.y],
+ ),
+ (
+ [half_w, half_h, -half_d],
+ [top_uv_upper_left.x, top_uv_lower_right.y],
+ ),
+ (
+ [-half_w, half_h, -half_d],
+ [top_uv_lower_right.x, top_uv_lower_right.y],
+ ),
+ ]
+ .map(|(pos, uv)| vertex(pos, uv, Vec3::UP));
+
+ let bottom_vertices = [
+ (
+ [half_w, -half_h, half_d],
+ [bot_uv_upper_left.x, bot_uv_upper_left.y],
+ ),
+ (
+ [-half_w, -half_h, half_d],
+ [bot_uv_lower_right.x, bot_uv_upper_left.y],
+ ),
+ (
+ [half_w, -half_h, -half_d],
+ [bot_uv_upper_left.x, bot_uv_lower_right.y],
+ ),
+ (
+ [-half_w, -half_h, -half_d],
+ [bot_uv_lower_right.x, bot_uv_lower_right.y],
+ ),
+ ]
+ .map(|(pos, uv)| vertex(pos, uv, Vec3::DOWN));
+
+ let left_vertices = [
+ (
+ [-half_w, half_h, half_d],
+ [left_uv_upper_left.x, left_uv_upper_left.y],
+ ),
+ (
+ [-half_w, half_h, -half_d],
+ [left_uv_lower_right.x, left_uv_upper_left.y],
+ ),
+ (
+ [-half_w, -half_h, half_d],
+ [left_uv_upper_left.x, left_uv_lower_right.y],
+ ),
+ (
+ [-half_w, -half_h, -half_d],
+ [left_uv_lower_right.x, left_uv_lower_right.y],
+ ),
+ ]
+ .map(|(pos, uv)| vertex(pos, uv, Vec3::LEFT));
+
+ let right_vertices = [
+ (
+ [half_w, half_h, half_d],
+ [right_uv_upper_left.x, right_uv_upper_left.y],
+ ),
+ (
+ [half_w, half_h, -half_d],
+ [right_uv_lower_right.x, right_uv_upper_left.y],
+ ),
+ (
+ [half_w, -half_h, half_d],
+ [right_uv_upper_left.x, right_uv_lower_right.y],
+ ),
+ (
+ [half_w, -half_h, -half_d],
+ [right_uv_lower_right.x, right_uv_lower_right.y],
+ ),
+ ]
+ .map(|(pos, uv)| vertex(pos, uv, Vec3::RIGHT));
+
+ let back_vertices = [
+ (
+ [half_w, half_h, -half_d],
+ [back_uv_upper_left.x, back_uv_upper_left.y],
+ ),
+ (
+ [-half_w, half_h, -half_d],
+ [back_uv_lower_right.x, back_uv_upper_left.y],
+ ),
+ (
+ [half_w, -half_h, -half_d],
+ [back_uv_upper_left.x, back_uv_lower_right.y],
+ ),
+ (
+ [-half_w, -half_h, -half_d],
+ [back_uv_lower_right.x, back_uv_lower_right.y],
+ ),
+ ]
+ .map(|(pos, uv)| vertex(pos, uv, Vec3::BACK));
+
+ let front_vertices = [
+ (
+ [half_w, half_h, half_d],
+ [front_uv_upper_left.x, front_uv_upper_left.y],
+ ),
+ (
+ [-half_w, half_h, half_d],
+ [front_uv_lower_right.x, front_uv_upper_left.y],
+ ),
+ (
+ [half_w, -half_h, half_d],
+ [front_uv_upper_left.x, front_uv_lower_right.y],
+ ),
+ (
+ [-half_w, -half_h, half_d],
+ [front_uv_lower_right.x, front_uv_lower_right.y],
+ ),
+ ]
+ .map(|(pos, uv)| vertex(pos, uv, Vec3::FRONT));
+
+ Mesh::builder()
+ .vertices(unsafe {
+ vertex_buffer::VertexBuffer::from_raw(
+ &[
+ VertexAttrProperties {
+ label: VertexLabel::Position,
+ ty: VertexAttrType::Float32Array { length: 3 },
+ layout: Layout::new::<[f32; 3]>(),
+ byte_offset: 0,
+ },
+ VertexAttrProperties {
+ label: VertexLabel::UvFromTopLeft,
+ ty: VertexAttrType::Float32Array { length: 2 },
+ layout: Layout::new::<[f32; 2]>(),
+ byte_offset: size_of::<[f32; 3]>(),
+ },
+ VertexAttrProperties {
+ label: VertexLabel::Normal,
+ ty: VertexAttrType::Float32Array { length: 3 },
+ layout: Layout::new::<[f32; 3]>(),
+ byte_offset: size_of::<[f32; 3]>() + size_of::<[f32; 2]>(),
+ },
+ ],
+ [
+ top_vertices,
+ bottom_vertices,
+ left_vertices,
+ right_vertices,
+ back_vertices,
+ front_vertices,
+ ]
+ .as_bytes()
+ .to_vec(),
+ )
+ })
+ .indices([
+ 0, 1, 2, 1, 3, 2, 4, 5, 6, 5, 7, 6, 8, 9, 10, 9, 11, 10, 12, 13, 14, 13,
+ 15, 14, 16, 17, 18, 17, 19, 18, 20, 21, 22, 21, 23, 22,
+ ])
+ .build()
+ }
+
+ pub fn builder() -> Builder
+ {
+ Builder::default()
+ }
+
+ pub fn vertex_buf(&self) -> &vertex_buffer::VertexBuffer
{
- &self.vertices
+ &self.vertex_buf
+ }
+
+ pub fn vertex_buf_mut(&mut self) -> &mut vertex_buffer::VertexBuffer
+ {
+ &mut self.vertex_buf
}
#[must_use]
@@ -30,4 +255,311 @@ impl Mesh
{
self.indices.as_deref()
}
+
+ #[must_use]
+ pub fn indices_mut(&mut self) -> Option<&mut [u32]>
+ {
+ self.indices.as_deref_mut()
+ }
+
+ pub fn set_indices(&mut self, indices: impl IntoIterator<Item = u32>)
+ {
+ let curr_indices = self.indices.get_or_insert_with(|| Vec::new());
+
+ curr_indices.clear();
+
+ curr_indices.extend(indices.into_iter());
+ }
+
+ /// Finds the vertex positions that are furthest in every 3D direction. Keep in mind
+ /// that this can be quite time-expensive if the mesh has many vertices.
+ pub fn find_furthest_vertex_positions(&self) -> DirectionPositions
+ {
+ let mut pos_iter = self
+ .vertex_buf()
+ .iter::<[f32; 3]>(VertexLabel::Position)
+ .map(|vertex_pos| Vec3::from(*vertex_pos))
+ .into_iter();
+
+ let first_pos = pos_iter.next().unwrap();
+
+ pos_iter
+ .fold(
+ FurthestPosAcc {
+ up: FurthestPos::new(first_pos, &Vec3::UP),
+ down: FurthestPos::new(first_pos, &Vec3::DOWN),
+ left: FurthestPos::new(first_pos, &Vec3::LEFT),
+ right: FurthestPos::new(first_pos, &Vec3::RIGHT),
+ back: FurthestPos::new(first_pos, &Vec3::BACK),
+ front: FurthestPos::new(first_pos, &Vec3::FRONT),
+ },
+ |mut furthest_pos_acc, pos| {
+ furthest_pos_acc.up.update_if_further(pos);
+ furthest_pos_acc.down.update_if_further(pos);
+ furthest_pos_acc.left.update_if_further(pos);
+ furthest_pos_acc.right.update_if_further(pos);
+ furthest_pos_acc.back.update_if_further(pos);
+ furthest_pos_acc.front.update_if_further(pos);
+
+ furthest_pos_acc
+ },
+ )
+ .into()
+ }
+}
+
+/// Mesh builder
+#[derive(Debug, Clone, Default)]
+pub struct Builder
+{
+ vertex_buf: vertex_buffer::VertexBuffer,
+ indices: Option<Vec<u32>>,
+}
+
+impl Builder
+{
+ pub fn new() -> Self
+ {
+ Self::default()
+ }
+
+ pub fn vertices(mut self, vertices_bytes: vertex_buffer::VertexBuffer) -> Self
+ {
+ self.vertex_buf = vertices_bytes;
+ self
+ }
+
+ pub fn indices(mut self, indices: impl IntoIterator<Item = u32>) -> Self
+ {
+ self.indices = Some(indices.into_iter().collect());
+ self
+ }
+
+ pub fn build(self) -> Mesh
+ {
+ Mesh {
+ vertex_buf: self.vertex_buf,
+ indices: self.indices,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub enum VertexAttrType
+{
+ Float32,
+ Float32Array
+ {
+ length: usize,
+ },
+}
+
+impl VertexAttrType
+{
+ pub fn layout(&self) -> Layout
+ {
+ match self {
+ Self::Float32 => Layout::new::<f32>(),
+ Self::Float32Array { length } => Layout::array::<f32>(*length).unwrap(),
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct CubeOptions
+{
+ pub size: Dimens3<f32>,
+ pub top_face: CubeFaceOptions,
+ pub bottom_face: CubeFaceOptions,
+ pub front_face: CubeFaceOptions,
+ pub back_face: CubeFaceOptions,
+ pub left_face: CubeFaceOptions,
+ pub right_face: CubeFaceOptions,
+}
+
+impl CubeOptions
+{
+ #[must_use]
+ pub fn size(mut self, size: Dimens3<f32>) -> Self
+ {
+ self.size = size;
+ self
+ }
+
+ #[must_use]
+ pub fn top_face(mut self, top_face: CubeFaceOptions) -> Self
+ {
+ self.top_face = top_face;
+ self
+ }
+
+ #[must_use]
+ pub fn bottom_face(mut self, bottom_face: CubeFaceOptions) -> Self
+ {
+ self.bottom_face = bottom_face;
+ self
+ }
+
+ #[must_use]
+ pub fn front_face(mut self, front_face: CubeFaceOptions) -> Self
+ {
+ self.front_face = front_face;
+ self
+ }
+
+ #[must_use]
+ pub fn back_face(mut self, back_face: CubeFaceOptions) -> Self
+ {
+ self.back_face = back_face;
+ self
+ }
+
+ #[must_use]
+ pub fn left_face(mut self, left_face: CubeFaceOptions) -> Self
+ {
+ self.left_face = left_face;
+ self
+ }
+
+ #[must_use]
+ pub fn right_face(mut self, right_face: CubeFaceOptions) -> Self
+ {
+ self.right_face = right_face;
+ self
+ }
+}
+
+impl Default for CubeOptions
+{
+ fn default() -> Self
+ {
+ Self {
+ size: Dimens3 { width: 1.0, height: 1.0, depth: 1.0 },
+ top_face: CubeFaceOptions::default(),
+ bottom_face: CubeFaceOptions::default(),
+ front_face: CubeFaceOptions::default(),
+ back_face: CubeFaceOptions::default(),
+ left_face: CubeFaceOptions::default(),
+ right_face: CubeFaceOptions::default(),
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct CubeFaceOptions
+{
+ /// Seeing the face head on, this is the texture coordinate for the corner that
+ /// would be on the upper left of the face.
+ ///
+ /// The default is `(0, 0)`
+ pub uv_upper_left: Vec2<f32>,
+
+ /// Seeing the face head on, this is the texture coordinate for the corner that
+ /// would be on the lower right of the face.
+ ///
+ /// The default is `(1, 1)`
+ pub uv_lower_right: Vec2<f32>,
+}
+
+impl CubeFaceOptions
+{
+ /// Seeing the face head on, this is the texture coordinate for the corner that
+ /// would be on the upper left of the face.
+ ///
+ /// The default is `(0, 0)`
+ pub fn uv_upper_left(mut self, uv_upper_left: Vec2<f32>) -> Self
+ {
+ self.uv_upper_left = uv_upper_left;
+ self
+ }
+
+ /// Seeing the face head on, this is the texture coordinate for the corner that
+ /// would be on the lower right of the face.
+ ///
+ /// The default is `(1, 1)`
+ pub fn uv_lower_right(mut self, uv_lower_right: Vec2<f32>) -> Self
+ {
+ self.uv_lower_right = uv_lower_right;
+ self
+ }
+}
+
+impl Default for CubeFaceOptions
+{
+ fn default() -> Self
+ {
+ Self {
+ uv_upper_left: Vec2 { x: 0.0, y: 0.0 },
+ uv_lower_right: Vec2 { x: 1.0, y: 1.0 },
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct DirectionPositions
+{
+ pub up: Vec3<f32>,
+ pub down: Vec3<f32>,
+ pub left: Vec3<f32>,
+ pub right: Vec3<f32>,
+ pub back: Vec3<f32>,
+ pub front: Vec3<f32>,
+}
+
+impl<'mesh> From<FurthestPosAcc<'mesh>> for DirectionPositions
+{
+ fn from(acc: FurthestPosAcc<'mesh>) -> Self
+ {
+ Self {
+ up: acc.up.pos,
+ down: acc.down.pos,
+ left: acc.left.pos,
+ right: acc.right.pos,
+ back: acc.back.pos,
+ front: acc.front.pos,
+ }
+ }
+}
+
+#[derive(Debug)]
+struct FurthestPosAcc<'mesh>
+{
+ up: FurthestPos<'mesh>,
+ down: FurthestPos<'mesh>,
+ left: FurthestPos<'mesh>,
+ right: FurthestPos<'mesh>,
+ back: FurthestPos<'mesh>,
+ front: FurthestPos<'mesh>,
+}
+
+#[derive(Debug)]
+struct FurthestPos<'mesh>
+{
+ pos: Vec3<f32>,
+ dot_prod: f32,
+ direction: &'mesh Vec3<f32>,
+}
+
+impl<'mesh> FurthestPos<'mesh>
+{
+ fn new(pos: Vec3<f32>, direction: &'mesh Vec3<f32>) -> Self
+ {
+ Self {
+ pos,
+ dot_prod: direction.dot(&pos),
+ direction,
+ }
+ }
+
+ fn update_if_further(&mut self, point: Vec3<f32>)
+ {
+ let point_dot_prod = self.direction.dot(&point);
+
+ if point_dot_prod > self.dot_prod {
+ self.pos = point;
+ self.dot_prod = point_dot_prod;
+ }
+ }
}
diff --git a/engine/src/mesh/cube.rs b/engine/src/mesh/cube.rs
deleted file mode 100644
index 7cdf885..0000000
--- a/engine/src/mesh/cube.rs
+++ /dev/null
@@ -1,702 +0,0 @@
-use crate::math::calc_triangle_surface_normal;
-use crate::mesh::Mesh;
-use crate::util::builder;
-use crate::vector::Vec3;
-use crate::vertex::{Builder as VertexBuilder, Vertex};
-
-builder! {
-/// Cube mesh creation specification.
-#[builder(name = CreationSpecBuilder, derives = (Debug, Default))]
-#[derive(Debug, Default)]
-#[non_exhaustive]
-pub struct CreationSpec
-{
- pub width: f32,
- pub height: f32,
- pub depth: f32,
-}
-}
-
-impl CreationSpec
-{
- /// Returns a new `CreationSpec` builder.
- #[must_use]
- pub fn builder() -> CreationSpecBuilder
- {
- CreationSpecBuilder::default()
- }
-}
-
-#[derive(Debug)]
-pub enum Side
-{
- Front,
- Back,
- Left,
- Right,
- Top,
- Bottom,
-}
-
-#[derive(Debug)]
-pub enum Corner
-{
- TopRight,
- TopLeft,
- BottomRight,
- BottomLeft,
-}
-
-/// Creates a cube mesh.
-pub fn create(
- creation_spec: CreationSpec,
- vertex_builder_cb: impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder,
-) -> Mesh
-{
- let mut vertices = [const { None }; VertexIndex::VARIANT_CNT];
-
- create_front(&creation_spec, &mut vertices, &vertex_builder_cb);
- create_back(&creation_spec, &mut vertices, &vertex_builder_cb);
- create_right(&creation_spec, &mut vertices, &vertex_builder_cb);
- create_left(&creation_spec, &mut vertices, &vertex_builder_cb);
- create_top(&creation_spec, &mut vertices, &vertex_builder_cb);
- create_bottom(&creation_spec, &mut vertices, &vertex_builder_cb);
-
- Mesh::new(
- vertices.map(Option::unwrap).to_vec(),
- Some(
- VERTEX_INDICES
- .into_iter()
- .flatten()
- .map(|index| index as u32)
- .collect(),
- ),
- )
-}
-
-macro_rules! one {
- ($tt: tt) => {
- 1
- };
-}
-
-macro_rules! enum_with_variant_cnt {
- (
- $(#[$attr: meta])*
- enum $name: ident {
- $($variant: ident,)*
- }
- ) => {
- $(#[$attr])*
- enum $name {
- $($variant,)*
- }
-
- impl $name {
- const VARIANT_CNT: usize = 0 $(+ one!($variant))*;
- }
- };
-}
-
-enum_with_variant_cnt! {
-#[repr(u32)]
-enum VertexIndex
-{
- FrontTopRight,
- FrontBottomRight,
- FrontBottomLeft,
- FrontTopLeft,
-
- BackTopRight,
- BackBottomRight,
- BackBottomLeft,
- BackTopLeft,
-
- RightBackTop,
- RightBackBottom,
- RightFrontTop,
- RightFrontBottom,
-
- LeftBackTop,
- LeftBackBottom,
- LeftFrontTop,
- LeftFrontBottom,
-
- TopBackRight,
- TopBackLeft,
- TopFrontRight,
- TopFrontLeft,
-
- BottomBackRight,
- BottomBackLeft,
- BottomFrontRight,
- BottomFrontLeft,
-}
-}
-
-fn create_front(
- creation_spec: &CreationSpec,
- vertices: &mut [Option<Vertex>],
- vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder,
-)
-{
- let front_top_right_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: creation_spec.height / 2.0,
- z: -(creation_spec.depth / 2.0),
- };
-
- let front_bottom_right_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: -(creation_spec.height / 2.0),
- z: -(creation_spec.depth / 2.0),
- };
-
- let front_bottom_left_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: -(creation_spec.height / 2.0),
- z: -(creation_spec.depth / 2.0),
- };
-
- let front_top_left_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: creation_spec.height / 2.0,
- z: -(creation_spec.depth / 2.0),
- };
-
- let front_normal = calc_triangle_surface_normal(
- &front_top_right_pos,
- &front_bottom_right_pos,
- &front_top_left_pos,
- );
-
- vertices[VertexIndex::FrontTopRight as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(front_top_right_pos)
- .normal(front_normal),
- Side::Front,
- Corner::TopRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::FrontBottomRight as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(front_bottom_right_pos)
- .normal(front_normal),
- Side::Front,
- Corner::BottomRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::FrontBottomLeft as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(front_bottom_left_pos)
- .normal(front_normal),
- Side::Front,
- Corner::BottomLeft,
- )
- .build(),
- );
-
- vertices[VertexIndex::FrontTopLeft as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(front_top_left_pos)
- .normal(front_normal),
- Side::Front,
- Corner::TopLeft,
- )
- .build(),
- );
-}
-
-fn create_back(
- creation_spec: &CreationSpec,
- vertices: &mut [Option<Vertex>],
- vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder,
-)
-{
- let back_top_right_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: creation_spec.height / 2.0,
- z: creation_spec.depth / 2.0,
- };
-
- let back_bottom_right_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: -(creation_spec.height / 2.0),
- z: creation_spec.depth / 2.0,
- };
-
- let back_bottom_left_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: -(creation_spec.height / 2.0),
- z: creation_spec.depth / 2.0,
- };
-
- let back_top_left_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: creation_spec.height / 2.0,
- z: creation_spec.depth / 2.0,
- };
-
- let back_normal = -calc_triangle_surface_normal(
- &back_top_right_pos,
- &back_bottom_right_pos,
- &back_top_left_pos,
- );
-
- vertices[VertexIndex::BackTopRight as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(back_top_right_pos)
- .normal(back_normal),
- Side::Back,
- Corner::TopRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::BackBottomRight as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(back_bottom_right_pos)
- .normal(back_normal),
- Side::Back,
- Corner::BottomRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::BackBottomLeft as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(back_bottom_left_pos)
- .normal(back_normal),
- Side::Back,
- Corner::BottomLeft,
- )
- .build(),
- );
-
- vertices[VertexIndex::BackTopLeft as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(back_top_left_pos)
- .normal(back_normal),
- Side::Back,
- Corner::TopLeft,
- )
- .build(),
- );
-}
-
-fn create_right(
- creation_spec: &CreationSpec,
- vertices: &mut [Option<Vertex>],
- vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder,
-)
-{
- let right_back_top_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: creation_spec.height / 2.0,
- z: creation_spec.depth / 2.0,
- };
-
- let right_back_bottom_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: -(creation_spec.height / 2.0),
- z: creation_spec.depth / 2.0,
- };
-
- let right_front_top_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: creation_spec.height / 2.0,
- z: -(creation_spec.depth / 2.0),
- };
-
- let right_front_bottom_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: -(creation_spec.height / 2.0),
- z: -(creation_spec.depth / 2.0),
- };
-
- let right_normal = calc_triangle_surface_normal(
- &right_back_top_pos,
- &right_back_bottom_pos,
- &right_front_top_pos,
- );
-
- vertices[VertexIndex::RightBackTop as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(right_back_top_pos)
- .normal(right_normal),
- Side::Right,
- Corner::TopLeft,
- )
- .build(),
- );
-
- vertices[VertexIndex::RightBackBottom as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(right_back_bottom_pos)
- .normal(right_normal),
- Side::Right,
- Corner::BottomLeft,
- )
- .build(),
- );
-
- vertices[VertexIndex::RightFrontTop as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(right_front_top_pos)
- .normal(right_normal),
- Side::Right,
- Corner::TopRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::RightFrontBottom as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(right_front_bottom_pos)
- .normal(right_normal),
- Side::Right,
- Corner::BottomRight,
- )
- .build(),
- );
-}
-
-fn create_left(
- creation_spec: &CreationSpec,
- vertices: &mut [Option<Vertex>],
- vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder,
-)
-{
- let left_back_top_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: creation_spec.height / 2.0,
- z: creation_spec.depth / 2.0,
- };
-
- let left_back_bottom_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: -(creation_spec.height / 2.0),
- z: creation_spec.depth / 2.0,
- };
-
- let left_front_top_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: creation_spec.height / 2.0,
- z: -(creation_spec.depth / 2.0),
- };
-
- let left_front_bottom_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: -(creation_spec.height / 2.0),
- z: -(creation_spec.depth / 2.0),
- };
-
- let left_normal = -calc_triangle_surface_normal(
- &left_back_top_pos,
- &left_back_bottom_pos,
- &left_front_top_pos,
- );
-
- vertices[VertexIndex::LeftBackTop as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(left_back_top_pos)
- .normal(left_normal),
- Side::Left,
- Corner::TopRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::LeftBackBottom as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(left_back_bottom_pos)
- .normal(left_normal),
- Side::Left,
- Corner::BottomRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::LeftFrontTop as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(left_front_top_pos)
- .normal(left_normal),
- Side::Left,
- Corner::TopLeft,
- )
- .build(),
- );
-
- vertices[VertexIndex::LeftFrontBottom as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(left_front_bottom_pos)
- .normal(left_normal),
- Side::Left,
- Corner::BottomLeft,
- )
- .build(),
- );
-}
-
-fn create_top(
- creation_spec: &CreationSpec,
- vertices: &mut [Option<Vertex>],
- vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder,
-)
-{
- let top_back_right_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: creation_spec.height / 2.0,
- z: creation_spec.depth / 2.0,
- };
-
- let top_back_left_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: creation_spec.height / 2.0,
- z: creation_spec.depth / 2.0,
- };
-
- let top_front_left_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: creation_spec.height / 2.0,
- z: -(creation_spec.depth / 2.0),
- };
-
- let top_front_right_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: creation_spec.height / 2.0,
- z: -(creation_spec.depth / 2.0),
- };
-
- let top_normal = -calc_triangle_surface_normal(
- &top_back_right_pos,
- &top_back_left_pos,
- &top_front_right_pos,
- );
-
- vertices[VertexIndex::TopBackRight as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(top_back_right_pos)
- .normal(top_normal),
- Side::Top,
- Corner::TopRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::TopBackLeft as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(top_back_left_pos)
- .normal(top_normal),
- Side::Top,
- Corner::TopLeft,
- )
- .build(),
- );
-
- vertices[VertexIndex::TopFrontLeft as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(top_front_left_pos)
- .normal(top_normal),
- Side::Top,
- Corner::BottomLeft,
- )
- .build(),
- );
-
- vertices[VertexIndex::TopFrontRight as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(top_front_right_pos)
- .normal(top_normal),
- Side::Top,
- Corner::BottomRight,
- )
- .build(),
- );
-}
-
-fn create_bottom(
- creation_spec: &CreationSpec,
- vertices: &mut [Option<Vertex>],
- vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder,
-)
-{
- let bottom_back_right_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: -(creation_spec.height / 2.0),
- z: (creation_spec.depth / 2.0),
- };
-
- let bottom_back_left_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: -(creation_spec.height / 2.0),
- z: creation_spec.depth / 2.0,
- };
-
- let bottom_front_right_pos = Vec3 {
- x: creation_spec.width / 2.0,
- y: -(creation_spec.height / 2.0),
- z: -(creation_spec.depth / 2.0),
- };
-
- let bottom_front_left_pos = Vec3 {
- x: -(creation_spec.width / 2.0),
- y: -(creation_spec.height / 2.0),
- z: -(creation_spec.depth / 2.0),
- };
-
- let bottom_normal = calc_triangle_surface_normal(
- &bottom_back_right_pos,
- &bottom_back_left_pos,
- &bottom_front_right_pos,
- );
-
- vertices[VertexIndex::BottomBackRight as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(bottom_back_right_pos)
- .normal(bottom_normal),
- Side::Bottom,
- Corner::BottomRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::BottomBackLeft as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(bottom_back_left_pos)
- .normal(bottom_normal),
- Side::Bottom,
- Corner::BottomLeft,
- )
- .build(),
- );
-
- vertices[VertexIndex::BottomFrontRight as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(bottom_front_right_pos)
- .normal(bottom_normal),
- Side::Bottom,
- Corner::TopRight,
- )
- .build(),
- );
-
- vertices[VertexIndex::BottomFrontLeft as usize] = Some(
- vertex_builder_cb(
- VertexBuilder::default()
- .pos(bottom_front_left_pos)
- .normal(bottom_normal),
- Side::Bottom,
- Corner::TopLeft,
- )
- .build(),
- );
-}
-
-const VERTEX_INDICES_FRONT: [VertexIndex; 6] = [
- // 🮝
- VertexIndex::FrontTopRight,
- VertexIndex::FrontBottomRight,
- VertexIndex::FrontTopLeft,
- //
- // 🮟
- VertexIndex::FrontBottomRight,
- VertexIndex::FrontBottomLeft,
- VertexIndex::FrontTopLeft,
-];
-
-const VERTEX_INDICES_BACK: [VertexIndex; 6] = [
- // 🮝
- VertexIndex::BackTopRight,
- VertexIndex::BackBottomRight,
- VertexIndex::BackTopLeft,
- //
- // 🮟
- VertexIndex::BackBottomRight,
- VertexIndex::BackBottomLeft,
- VertexIndex::BackTopLeft,
-];
-
-const VERTEX_INDICES_RIGHT: [VertexIndex; 6] = [
- // 🮝
- VertexIndex::RightBackTop,
- VertexIndex::RightBackBottom,
- VertexIndex::RightFrontTop,
- //
- // 🮟
- VertexIndex::RightBackBottom,
- VertexIndex::RightFrontBottom,
- VertexIndex::RightFrontTop,
-];
-
-const VERTEX_INDICES_LEFT: [VertexIndex; 6] = [
- // 🮝
- VertexIndex::LeftBackTop,
- VertexIndex::LeftBackBottom,
- VertexIndex::LeftFrontTop,
- //
- // 🮟
- VertexIndex::LeftBackBottom,
- VertexIndex::LeftFrontBottom,
- VertexIndex::LeftFrontTop,
-];
-
-const VERTEX_INDICES_TOP: [VertexIndex; 6] = [
- // 🮝
- VertexIndex::TopBackRight,
- VertexIndex::TopBackLeft,
- VertexIndex::TopFrontRight,
- //
- // 🮟
- VertexIndex::TopBackLeft,
- VertexIndex::TopFrontLeft,
- VertexIndex::TopFrontRight,
-];
-
-const VERTEX_INDICES_BOTTOM: [VertexIndex; 6] = [
- // 🮝
- VertexIndex::BottomBackRight,
- VertexIndex::BottomBackLeft,
- VertexIndex::BottomFrontRight,
- //
- // 🮟
- VertexIndex::BottomBackLeft,
- VertexIndex::BottomFrontLeft,
- VertexIndex::BottomFrontRight,
-];
-
-const VERTEX_INDICES: [[VertexIndex; 6]; 6] = [
- VERTEX_INDICES_FRONT,
- VERTEX_INDICES_BACK,
- VERTEX_INDICES_RIGHT,
- VERTEX_INDICES_LEFT,
- VERTEX_INDICES_TOP,
- VERTEX_INDICES_BOTTOM,
-];
diff --git a/engine/src/mesh/vertex_buffer.rs b/engine/src/mesh/vertex_buffer.rs
new file mode 100644
index 0000000..5c03b45
--- /dev/null
+++ b/engine/src/mesh/vertex_buffer.rs
@@ -0,0 +1,281 @@
+use std::alloc::Layout;
+use std::borrow::Cow;
+use std::marker::PhantomData;
+
+use seq_macro::seq;
+use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
+
+use crate::mesh::VertexAttrType;
+
+pub trait VertexAttrValue:
+ IntoBytes + FromBytes + KnownLayout + Immutable + 'static
+{
+ fn ty() -> VertexAttrType;
+}
+
+impl VertexAttrValue for f32
+{
+ fn ty() -> VertexAttrType
+ {
+ VertexAttrType::Float32
+ }
+}
+
+impl<const LEN: usize> VertexAttrValue for [f32; LEN]
+{
+ fn ty() -> VertexAttrType
+ {
+ VertexAttrType::Float32Array { length: LEN }
+ }
+}
+
+#[derive(Debug)]
+pub struct NamedVertexAttr<Value: VertexAttrValue>
+{
+ pub label: VertexLabel,
+ pub value: Value,
+}
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct VertexAttrProperties
+{
+ pub label: VertexLabel,
+ pub ty: VertexAttrType,
+ pub layout: Layout,
+ pub byte_offset: usize,
+}
+
+#[derive(Debug)]
+pub struct VertexAttrInfo
+{
+ pub label: VertexLabel,
+ pub ty: VertexAttrType,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum VertexLabel
+{
+ Position,
+ Normal,
+
+ /// Texture coordinate with (0, 0) being at the top left.
+ UvFromTopLeft,
+ Color,
+ Other(Cow<'static, str>),
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct VertexBuffer
+{
+ buf: Vec<u8>,
+ vertex_size: usize,
+ vertex_attr_props: Vec<VertexAttrProperties>,
+}
+
+impl VertexBuffer
+{
+ pub unsafe fn from_raw(
+ vertex_attr_props: &[VertexAttrProperties],
+ buffer: Vec<u8>,
+ ) -> Self
+ {
+ let last_vertex_attr_props = vertex_attr_props
+ .iter()
+ .max_by_key(|props| props.byte_offset)
+ .expect("No vertex attribute properties are given");
+
+ let vertex_size =
+ last_vertex_attr_props.byte_offset + last_vertex_attr_props.layout.size();
+
+ Self {
+ buf: buffer,
+ vertex_size,
+ vertex_attr_props: vertex_attr_props.to_vec(),
+ }
+ }
+
+ pub fn with_capacity(vertex_attrs: &[VertexAttrInfo], capacity: usize) -> Self
+ {
+ let mut vertex_attr_props = vertex_attrs
+ .iter()
+ .map(|VertexAttrInfo { label, ty }| VertexAttrProperties {
+ label: label.clone(),
+ ty: ty.clone(),
+ layout: ty.layout(),
+ byte_offset: 0,
+ })
+ .collect::<Vec<_>>();
+
+ let mut vertex_layout = Layout::new::<()>();
+
+ for VertexAttrProperties {
+ layout: vertex_attr_layout,
+ byte_offset: vertex_attr_byte_offset,
+ ..
+ } in &mut vertex_attr_props
+ {
+ let (new_struct_layout, byte_offset) =
+ vertex_layout.extend(*vertex_attr_layout).unwrap();
+
+ *vertex_attr_byte_offset = byte_offset;
+ vertex_layout = new_struct_layout;
+ }
+
+ let vertex_layout = vertex_layout.pad_to_align();
+
+ Self {
+ buf: Vec::with_capacity(vertex_layout.size() * capacity),
+ vertex_size: vertex_layout.size(),
+ vertex_attr_props,
+ }
+ }
+
+ pub fn push<VertexAttrs: NamedVertexAttrs>(&mut self, vertex: VertexAttrs)
+ {
+ assert_eq!(
+ self.vertex_attr_props.len(),
+ vertex.vertex_attr_cnt(),
+ "Vertex has incorrect amount of attributes"
+ );
+
+ if self.buf.spare_capacity_mut().len() < self.vertex_size {
+ self.buf.reserve_exact(self.vertex_size * (self.len() / 2));
+ }
+
+ let spare_capacity = self.buf.spare_capacity_mut();
+
+ let vertex_attrs = vertex.vertex_attrs();
+
+ for (vertex_label, vertex_attr_bytes, vertex_attr_ty) in vertex_attrs {
+ let vertex_attr_props = self
+ .vertex_attr_props
+ .iter()
+ .find(|vertex_attr_props| &vertex_attr_props.label == vertex_label)
+ .unwrap();
+
+ assert_eq!(vertex_attr_ty, vertex_attr_props.ty);
+
+ let start_offset = vertex_attr_props.byte_offset;
+
+ let end_offset = start_offset + vertex_attr_props.layout.size();
+
+ spare_capacity[start_offset..end_offset]
+ .write_copy_of_slice(vertex_attr_bytes);
+ }
+
+ unsafe {
+ self.buf.set_len(self.buf.len() + self.vertex_size);
+ }
+ }
+
+ pub fn vertex_attr_props(&self) -> &[VertexAttrProperties]
+ {
+ &self.vertex_attr_props
+ }
+
+ pub fn len(&self) -> usize
+ {
+ assert_eq!(self.buf.len() % self.vertex_size, 0, "Invalid length");
+
+ self.buf.len() / self.vertex_size
+ }
+
+ pub fn vertex_size(&self) -> usize
+ {
+ self.vertex_size
+ }
+
+ pub fn as_bytes(&self) -> &[u8]
+ {
+ &self.buf
+ }
+
+ pub fn clear(&mut self)
+ {
+ self.buf.clear();
+ }
+
+ pub fn iter<VertexAttr: VertexAttrValue>(
+ &self,
+ vertex_label: VertexLabel,
+ ) -> Iter<'_, VertexAttr>
+ {
+ let vertex_attr_props = self
+ .vertex_attr_props
+ .iter()
+ .find(|vertex_attr_props| vertex_attr_props.label == vertex_label)
+ .unwrap();
+
+ assert_eq!(VertexAttr::ty(), vertex_attr_props.ty);
+
+ Iter {
+ buf: self,
+ vertex_attr_props,
+ curr_index: 0,
+ _pd: PhantomData,
+ }
+ }
+}
+
+pub struct Iter<'a, VertexAttr: VertexAttrValue>
+{
+ buf: &'a VertexBuffer,
+ vertex_attr_props: &'a VertexAttrProperties,
+ curr_index: usize,
+ _pd: PhantomData<VertexAttr>,
+}
+
+impl<'a, VertexAttr: VertexAttrValue> Iterator for Iter<'a, VertexAttr>
+{
+ type Item = &'a VertexAttr;
+
+ fn next(&mut self) -> Option<Self::Item>
+ {
+ let start_offset =
+ (self.buf.vertex_size * self.curr_index) + self.vertex_attr_props.byte_offset;
+
+ let end_offset = start_offset + self.vertex_attr_props.layout.size();
+
+ let bytes = self.buf.buf.get(start_offset..end_offset)?;
+
+ self.curr_index += 1;
+
+ Some(VertexAttr::ref_from_bytes(bytes).unwrap())
+ }
+}
+
+pub trait NamedVertexAttrs
+{
+ fn vertex_attr_cnt(&self) -> usize;
+
+ fn vertex_attrs(&self)
+ -> impl Iterator<Item = (&VertexLabel, &[u8], VertexAttrType)>;
+}
+
+macro_rules! impl_named_vertex_attrs {
+ ($cnt: tt) => {
+ seq!(I in 0..$cnt {
+ impl<#(VertexAttr~I: VertexAttrValue,)*>
+ NamedVertexAttrs for (#(NamedVertexAttr<VertexAttr~I>,)*)
+ {
+ fn vertex_attr_cnt(&self) -> usize
+ {
+ $cnt
+ }
+
+ fn vertex_attrs(&self)
+ -> impl Iterator<Item = (&VertexLabel, &[u8], VertexAttrType)>
+ {
+ [#(
+ (&self.I.label, self.I.value.as_bytes(), VertexAttr~I::ty()),
+ )*].into_iter()
+ }
+ }
+ });
+ };
+}
+
+seq!(I in 0..16 {
+ impl_named_vertex_attrs!(I);
+});
diff --git a/engine/src/model.rs b/engine/src/model.rs
new file mode 100644
index 0000000..1269afd
--- /dev/null
+++ b/engine/src/model.rs
@@ -0,0 +1,112 @@
+use crate::asset::{Assets, Handle as AssetHandle};
+use crate::ecs::Component;
+use crate::material::Material;
+use crate::mesh::Mesh;
+
+pub mod asset;
+
+#[derive(Debug, Clone, Component)]
+#[non_exhaustive]
+pub struct Model
+{
+ pub spec_asset: AssetHandle<Spec>,
+}
+
+impl Model
+{
+ pub fn new(asset_handle: AssetHandle<Spec>) -> Self
+ {
+ Self { spec_asset: asset_handle }
+ }
+}
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct Spec
+{
+ pub mesh_asset: Option<AssetHandle<Mesh>>,
+ pub materials: Vec<MaterialDescription>,
+}
+
+impl Spec
+{
+ pub fn builder() -> SpecBuilder
+ {
+ SpecBuilder::default()
+ }
+
+ pub fn find_first_material<'assets>(
+ &'assets self,
+ assets: &'assets Assets,
+ ) -> MaterialSearchResult<'assets>
+ {
+ let Some(material_desc) = self.materials.first() else {
+ return MaterialSearchResult::NoMaterials;
+ };
+
+ if assets.get(&material_desc.asset).is_none() {
+ tracing::trace!("Missing material asset");
+ return MaterialSearchResult::NotFound;
+ }
+
+ MaterialSearchResult::Found(&material_desc.asset)
+ }
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct SpecBuilder
+{
+ mesh_asset: Option<AssetHandle<Mesh>>,
+ materials: Vec<MaterialDescription>,
+}
+
+impl SpecBuilder
+{
+ pub fn mesh(mut self, asset: AssetHandle<Mesh>) -> Self
+ {
+ self.mesh_asset = Some(asset);
+
+ self
+ }
+
+ pub fn materials(
+ mut self,
+ materials: impl IntoIterator<Item = MaterialDescription>,
+ ) -> Self
+ {
+ self.materials = materials.into_iter().collect();
+
+ self
+ }
+
+ #[tracing::instrument(skip_all)]
+ pub fn build(self) -> Spec
+ {
+ Spec {
+ mesh_asset: self.mesh_asset,
+ materials: self.materials,
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct MaterialDescription
+{
+ pub asset: AssetHandle<Material>,
+}
+
+impl MaterialDescription
+{
+ pub fn new(asset: AssetHandle<Material>) -> Self
+ {
+ Self { asset }
+ }
+}
+
+pub enum MaterialSearchResult<'a>
+{
+ Found(&'a AssetHandle<Material>),
+ NotFound,
+ NoMaterials,
+}
diff --git a/engine/src/model/asset.rs b/engine/src/model/asset.rs
new file mode 100644
index 0000000..52a6733
--- /dev/null
+++ b/engine/src/model/asset.rs
@@ -0,0 +1,120 @@
+use std::fs::read_to_string;
+use std::path::{Path, PathBuf};
+
+use ecs::util::Either;
+
+use crate::asset::{Assets, Label as AssetLabel, Submitter as AssetSubmitter};
+use crate::material::Material;
+use crate::model::{MaterialDescription, Spec};
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct Settings
+{
+ /// Whether all UVs should be flipped on the Y axis. This is necessary if the UVs
+ /// have their origin at the bottom left corner of the image
+ ///
+ /// The default is `true`.
+ pub y_flip_uvs: bool,
+}
+
+impl Settings
+{
+ /// Whether all UVs should be flipped on the Y axis. This is necessary if the UVs have
+ /// their origin at the bottom left corner of the image
+ ///
+ /// The default is `true`.
+ pub fn y_flip_uvs(mut self, y_flip_uvs: bool) -> Self
+ {
+ self.y_flip_uvs = y_flip_uvs;
+ self
+ }
+}
+
+impl Default for Settings
+{
+ fn default() -> Self
+ {
+ Self { y_flip_uvs: true }
+ }
+}
+
+pub fn add_importers(assets: &mut Assets)
+{
+ assets.set_importer(["obj"], import_wavefront_obj_asset);
+}
+
+fn import_wavefront_obj_asset(
+ asset_submitter: &mut AssetSubmitter<'_>,
+ path: &Path,
+ settings: Option<&Settings>,
+) -> Result<(), Error>
+{
+ let settings = match settings {
+ Some(settings) => settings,
+ None => &Settings::default(),
+ };
+
+ let parent_path = path
+ .parent()
+ .ok_or_else(|| Error::InvalidPath(path.to_path_buf()))?;
+
+ let obj = crate::file_format::wavefront::obj::parse(
+ &read_to_string(path)
+ .map_err(|err| Error::ReadFailed(err, path.to_path_buf()))?,
+ )?;
+
+ let mesh = obj.to_mesh(
+ crate::file_format::wavefront::obj::ToMeshOptions::default()
+ .y_flip_uvs(settings.y_flip_uvs),
+ )?;
+
+ let mesh_asset = asset_submitter.submit_store_named("mesh", mesh);
+
+ if obj.mtl_libs.len() > 1 {
+ return Err(Error::MoreThanOneMaterialLibrary);
+ }
+
+ asset_submitter.submit_store(
+ Spec::builder()
+ .mesh(mesh_asset)
+ .materials(if obj.mtl_libs.is_empty() {
+ Either::A([].into_iter())
+ } else {
+ Either::B(
+ obj.unique_used_material_names
+ .iter()
+ .zip(std::iter::repeat(obj.mtl_libs.iter()).flatten())
+ .map(|(material_name, mtl_lib)| {
+ MaterialDescription::new(
+ asset_submitter.submit_load_other::<Material>(
+ AssetLabel {
+ path: parent_path.join(mtl_lib).into(),
+ name: Some(material_name.as_ref().into()),
+ },
+ ),
+ )
+ }),
+ )
+ })
+ .build(),
+ );
+
+ Ok(())
+}
+
+#[derive(Debug, thiserror::Error)]
+enum Error
+{
+ #[error("Invalid path '{}'", .0.display())]
+ InvalidPath(PathBuf),
+
+ #[error("Failed to read file {}", .1.display())]
+ ReadFailed(#[source] std::io::Error, PathBuf),
+
+ #[error("More than one material library is specified. This is not supported")]
+ MoreThanOneMaterialLibrary,
+
+ #[error(transparent)]
+ Other(#[from] crate::file_format::wavefront::obj::Error),
+}
diff --git a/engine/src/opengl/buffer.rs b/engine/src/opengl/buffer.rs
deleted file mode 100644
index 2be7f12..0000000
--- a/engine/src/opengl/buffer.rs
+++ /dev/null
@@ -1,92 +0,0 @@
-use std::marker::PhantomData;
-use std::mem::size_of_val;
-
-#[derive(Debug)]
-pub struct Buffer<Item>
-{
- buf: gl::types::GLuint,
- _pd: PhantomData<Item>,
-}
-
-impl<Item> Buffer<Item>
-{
- pub fn new() -> Self
- {
- let mut buffer = gl::types::GLuint::default();
-
- unsafe {
- gl::CreateBuffers(1, &mut buffer);
- };
-
- Self { buf: buffer, _pd: PhantomData }
- }
-
- /// Stores items in the currently bound buffer.
- pub fn store(&mut self, items: &[Item], usage: Usage)
- {
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::NamedBufferData(
- self.buf,
- size_of_val(items) as gl::types::GLsizeiptr,
- items.as_ptr().cast(),
- usage.into_gl(),
- );
- }
- }
-
- pub fn object(&self) -> gl::types::GLuint
- {
- self.buf
- }
-
- /// Does a weak clone of this buffer. The buffer itself is NOT copied in any way this
- /// function only copies the internal buffer ID.
- ///
- /// # Safety
- /// The returned `Buffer` must not be dropped if another `Buffer` referencing the
- /// same buffer ID is used later or if a [`VertexArray`] is used later.
- ///
- /// [`VertexArray`]: crate::opengl::vertex_array::VertexArray
- pub unsafe fn clone_weak(&self) -> Self
- {
- Self { buf: self.buf, _pd: PhantomData }
- }
-}
-
-impl<Item> Drop for Buffer<Item>
-{
- fn drop(&mut self)
- {
- unsafe {
- gl::DeleteBuffers(1, &self.buf);
- }
- }
-}
-
-/// Buffer usage.
-#[derive(Debug)]
-#[allow(dead_code)]
-pub enum Usage
-{
- /// The buffer data is set only once and used by the GPU at most a few times.
- Stream,
-
- /// The buffer data is set only once and used many times.
- Static,
-
- /// The buffer data is changed a lot and used many times.
- Dynamic,
-}
-
-impl Usage
-{
- fn into_gl(self) -> gl::types::GLenum
- {
- match self {
- Self::Stream => gl::STREAM_DRAW,
- Self::Static => gl::STATIC_DRAW,
- Self::Dynamic => gl::DYNAMIC_DRAW,
- }
- }
-}
diff --git a/engine/src/opengl/debug.rs b/engine/src/opengl/debug.rs
deleted file mode 100644
index 203590a..0000000
--- a/engine/src/opengl/debug.rs
+++ /dev/null
@@ -1,145 +0,0 @@
-use std::ffi::c_void;
-use std::io::{stderr, Write};
-use std::panic::catch_unwind;
-use std::ptr::null_mut;
-use std::sync::Mutex;
-
-use crate::opengl::util::gl_enum;
-
-pub type MessageCallback = fn(
- source: MessageSource,
- ty: MessageType,
- id: u32,
- severity: MessageSeverity,
- message: &str,
-);
-
-pub fn enable_debug_output()
-{
- unsafe {
- gl::Enable(gl::DEBUG_OUTPUT);
- gl::Enable(gl::DEBUG_OUTPUT_SYNCHRONOUS);
- }
-}
-
-pub fn set_debug_message_callback(cb: MessageCallback)
-{
- *DEBUG_MESSAGE_CB.lock().unwrap() = Some(cb);
-
- unsafe {
- gl::DebugMessageCallback(Some(debug_message_cb), null_mut());
- }
-}
-
-pub fn set_debug_message_control(
- source: Option<MessageSource>,
- ty: Option<MessageType>,
- severity: Option<MessageSeverity>,
- ids: &[u32],
- ids_action: MessageIdsAction,
-)
-{
- // Ids shouldn't realistically be large enough to cause a panic here
- let ids_len: i32 = ids.len().try_into().unwrap();
-
- unsafe {
- gl::DebugMessageControl(
- source.map_or(gl::DONT_CARE, |source| source as u32),
- ty.map_or(gl::DONT_CARE, |ty| ty as u32),
- severity.map_or(gl::DONT_CARE, |severity| severity as u32),
- ids_len,
- ids.as_ptr(),
- ids_action as u8,
- );
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-#[allow(dead_code)]
-pub enum MessageIdsAction
-{
- Enable = 1,
- Disable = 0,
-}
-
-gl_enum! {
-pub enum MessageSource
-{
- Api = gl::DEBUG_SOURCE_API,
- WindowSystem = gl::DEBUG_SOURCE_WINDOW_SYSTEM,
- ShaderCompiler = gl::DEBUG_SOURCE_SHADER_COMPILER,
- ThirdParty = gl::DEBUG_SOURCE_THIRD_PARTY,
- Application = gl::DEBUG_SOURCE_APPLICATION,
- Other = gl::DEBUG_SOURCE_OTHER,
-}
-}
-
-gl_enum! {
-pub enum MessageType
-{
- DeprecatedBehavior = gl::DEBUG_TYPE_DEPRECATED_BEHAVIOR,
- Error = gl::DEBUG_TYPE_ERROR,
- Marker = gl::DEBUG_TYPE_MARKER,
- Other = gl::DEBUG_TYPE_OTHER,
- Performance = gl::DEBUG_TYPE_PERFORMANCE,
- PopGroup = gl::DEBUG_TYPE_POP_GROUP,
- PushGroup = gl::DEBUG_TYPE_PUSH_GROUP,
- Portability = gl::DEBUG_TYPE_PORTABILITY,
- UndefinedBehavior = gl::DEBUG_TYPE_UNDEFINED_BEHAVIOR,
-}
-}
-
-gl_enum! {
-pub enum MessageSeverity
-{
- High = gl::DEBUG_SEVERITY_HIGH,
- Medium = gl::DEBUG_SEVERITY_MEDIUM,
- Low = gl::DEBUG_SEVERITY_LOW,
- Notification = gl::DEBUG_SEVERITY_NOTIFICATION,
-}
-}
-
-static DEBUG_MESSAGE_CB: Mutex<Option<MessageCallback>> = Mutex::new(None);
-
-extern "system" fn debug_message_cb(
- source: gl::types::GLenum,
- ty: gl::types::GLenum,
- id: gl::types::GLuint,
- severity: gl::types::GLenum,
- message_length: gl::types::GLsizei,
- message: *const gl::types::GLchar,
- _user_param: *mut c_void,
-)
-{
- // Unwinds are catched because unwinding from Rust code into foreign code is UB.
- let res = catch_unwind(|| {
- let cb_lock = DEBUG_MESSAGE_CB.lock().unwrap();
-
- if let Some(cb) = *cb_lock {
- let msg_source = MessageSource::from_gl(source).unwrap();
- let msg_type = MessageType::from_gl(ty).unwrap();
- let msg_severity = MessageSeverity::from_gl(severity).unwrap();
-
- let msg_length = usize::try_from(message_length).unwrap();
-
- // SAFETY: The received message should be a valid ASCII string
- let message = unsafe {
- std::str::from_utf8_unchecked(std::slice::from_raw_parts(
- message.cast(),
- msg_length,
- ))
- };
-
- cb(msg_source, msg_type, id, msg_severity, message);
- }
- });
-
- if res.is_err() {
- // eprintln is not used since it can panic and unwinds are unwanted because
- // unwinding from Rust code into foreign code is UB.
- stderr()
- .write_all(b"ERROR: Panic in debug message callback")
- .ok();
- println!();
- }
-}
diff --git a/engine/src/opengl/mod.rs b/engine/src/opengl/mod.rs
deleted file mode 100644
index 0b1bb8a..0000000
--- a/engine/src/opengl/mod.rs
+++ /dev/null
@@ -1,107 +0,0 @@
-use bitflags::bitflags;
-
-use crate::data_types::dimens::Dimens;
-use crate::vector::Vec2;
-
-pub mod buffer;
-pub mod shader;
-pub mod texture;
-pub mod vertex_array;
-
-mod util;
-
-#[cfg(feature = "debug")]
-pub mod debug;
-
-pub fn set_viewport(position: Vec2<u32>, size: Dimens<u32>)
-{
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::Viewport(
- position.x as i32,
- position.y as i32,
- size.width as i32,
- size.height as i32,
- );
- }
-}
-
-pub fn clear_buffers(mask: BufferClearMask)
-{
- unsafe {
- gl::Clear(mask.bits());
- }
-}
-
-pub fn set_polygon_mode(face: impl Into<PolygonModeFace>, mode: impl Into<PolygonMode>)
-{
- unsafe {
- gl::PolygonMode(face.into() as u32, mode.into() as u32);
- }
-}
-
-pub fn enable(capacity: Capability)
-{
- unsafe {
- gl::Enable(capacity as u32);
- }
-}
-
-bitflags! {
- #[derive(Debug, Clone, Copy)]
- pub struct BufferClearMask: u32 {
- const COLOR = gl::COLOR_BUFFER_BIT;
- const DEPTH = gl::DEPTH_BUFFER_BIT;
- const STENCIL = gl::STENCIL_BUFFER_BIT;
- }
-}
-
-#[derive(Debug)]
-#[repr(u32)]
-pub enum Capability
-{
- DepthTest = gl::DEPTH_TEST,
- MultiSample = gl::MULTISAMPLE,
-}
-
-#[derive(Debug)]
-#[repr(u32)]
-pub enum PolygonMode
-{
- Point = gl::POINT,
- Line = gl::LINE,
- Fill = gl::FILL,
-}
-
-impl From<crate::draw_flags::PolygonMode> for PolygonMode
-{
- fn from(mode: crate::draw_flags::PolygonMode) -> Self
- {
- match mode {
- crate::draw_flags::PolygonMode::Point => Self::Point,
- crate::draw_flags::PolygonMode::Fill => Self::Fill,
- crate::draw_flags::PolygonMode::Line => Self::Line,
- }
- }
-}
-
-#[derive(Debug)]
-#[repr(u32)]
-pub enum PolygonModeFace
-{
- Front = gl::FRONT,
- Back = gl::BACK,
- FrontAndBack = gl::FRONT_AND_BACK,
-}
-
-impl From<crate::draw_flags::PolygonModeFace> for PolygonModeFace
-{
- fn from(face: crate::draw_flags::PolygonModeFace) -> Self
- {
- match face {
- crate::draw_flags::PolygonModeFace::Front => Self::Front,
- crate::draw_flags::PolygonModeFace::Back => Self::Back,
- crate::draw_flags::PolygonModeFace::FrontAndBack => Self::FrontAndBack,
- }
- }
-}
diff --git a/engine/src/opengl/shader.rs b/engine/src/opengl/shader.rs
deleted file mode 100644
index 070897e..0000000
--- a/engine/src/opengl/shader.rs
+++ /dev/null
@@ -1,240 +0,0 @@
-use std::ffi::CStr;
-use std::ptr::null_mut;
-
-use crate::matrix::Matrix;
-use crate::shader::Kind;
-use crate::vector::Vec3;
-
-#[derive(Debug)]
-pub struct Shader
-{
- shader: gl::types::GLuint,
-}
-
-impl Shader
-{
- pub fn new(kind: Kind) -> Self
- {
- let shader = unsafe { gl::CreateShader(kind.into_gl()) };
-
- Self { shader }
- }
-
- pub fn set_source(&self, source: &str) -> Result<(), Error>
- {
- if !source.is_ascii() {
- return Err(Error::SourceNotAscii);
- }
-
- unsafe {
- #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
- gl::ShaderSource(
- self.shader,
- 1,
- &source.as_ptr().cast(),
- &(source.len() as gl::types::GLint),
- );
- }
-
- Ok(())
- }
-
- pub fn compile(&self) -> Result<(), Error>
- {
- unsafe {
- gl::CompileShader(self.shader);
- }
-
- let mut compile_success = gl::types::GLint::default();
-
- unsafe {
- gl::GetShaderiv(self.shader, gl::COMPILE_STATUS, &mut compile_success);
- }
-
- if compile_success == 0 {
- let info_log = self.get_info_log();
-
- return Err(Error::CompileFailed(info_log));
- }
-
- Ok(())
- }
-
- fn get_info_log(&self) -> String
- {
- let mut buf = vec![gl::types::GLchar::default(); 512];
-
- unsafe {
- #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
- gl::GetShaderInfoLog(
- self.shader,
- buf.len() as gl::types::GLsizei,
- null_mut(),
- buf.as_mut_ptr(),
- );
- }
-
- let info_log = unsafe { CStr::from_ptr(buf.as_ptr()) };
-
- unsafe { String::from_utf8_unchecked(info_log.to_bytes().to_vec()) }
- }
-}
-
-impl Drop for Shader
-{
- fn drop(&mut self)
- {
- unsafe {
- gl::DeleteShader(self.shader);
- }
- }
-}
-
-impl Kind
-{
- fn into_gl(self) -> gl::types::GLenum
- {
- match self {
- Self::Vertex => gl::VERTEX_SHADER,
- Self::Fragment => gl::FRAGMENT_SHADER,
- }
- }
-}
-
-/// Shader program
-#[derive(Debug, PartialEq, Eq, Hash)]
-pub struct Program
-{
- program: gl::types::GLuint,
-}
-
-impl Program
-{
- pub fn new() -> Self
- {
- let program = unsafe { gl::CreateProgram() };
-
- Self { program }
- }
-
- pub fn attach(&self, shader: &Shader)
- {
- unsafe {
- gl::AttachShader(self.program, shader.shader);
- }
- }
-
- pub fn link(&self) -> Result<(), Error>
- {
- unsafe {
- gl::LinkProgram(self.program);
- }
-
- let mut link_success = gl::types::GLint::default();
-
- unsafe {
- gl::GetProgramiv(self.program, gl::LINK_STATUS, &mut link_success);
- }
-
- if link_success == 0 {
- let info_log = self.get_info_log();
-
- return Err(Error::CompileFailed(info_log));
- }
-
- Ok(())
- }
-
- pub fn activate(&self)
- {
- unsafe {
- gl::UseProgram(self.program);
- }
- }
-
- pub fn set_uniform_matrix_4fv(&mut self, name: &CStr, matrix: &Matrix<f32, 4, 4>)
- {
- let uniform_location =
- unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) };
-
- unsafe {
- gl::ProgramUniformMatrix4fv(
- self.program,
- uniform_location,
- 1,
- gl::FALSE,
- matrix.as_ptr(),
- );
- }
- }
-
- pub fn set_uniform_vec_3fv(&mut self, name: &CStr, vec: &Vec3<f32>)
- {
- let uniform_location =
- unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) };
-
- unsafe {
- gl::ProgramUniform3fv(self.program, uniform_location, 1, vec.as_ptr());
- }
- }
-
- pub fn set_uniform_1fv(&mut self, name: &CStr, num: f32)
- {
- let uniform_location =
- unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) };
-
- unsafe {
- gl::ProgramUniform1fv(self.program, uniform_location, 1, &num);
- }
- }
-
- pub fn set_uniform_1i(&mut self, name: &CStr, num: i32)
- {
- let uniform_location =
- unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) };
-
- unsafe {
- gl::ProgramUniform1i(self.program, uniform_location, num);
- }
- }
-
- fn get_info_log(&self) -> String
- {
- let mut buf = vec![gl::types::GLchar::default(); 512];
-
- unsafe {
- #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
- gl::GetProgramInfoLog(
- self.program,
- buf.len() as gl::types::GLsizei,
- null_mut(),
- buf.as_mut_ptr(),
- );
- }
-
- let info_log = unsafe { CStr::from_ptr(buf.as_ptr()) };
-
- unsafe { String::from_utf8_unchecked(info_log.to_bytes().to_vec()) }
- }
-}
-
-impl Drop for Program
-{
- fn drop(&mut self)
- {
- unsafe {
- gl::DeleteProgram(self.program);
- }
- }
-}
-
-/// Shader error.
-#[derive(Debug, thiserror::Error)]
-pub enum Error
-{
- #[error("All characters in source are not within the ASCII range")]
- SourceNotAscii,
-
- #[error("Failed to compile: {0}")]
- CompileFailed(String),
-}
diff --git a/engine/src/opengl/texture.rs b/engine/src/opengl/texture.rs
deleted file mode 100644
index 074ade7..0000000
--- a/engine/src/opengl/texture.rs
+++ /dev/null
@@ -1,240 +0,0 @@
-use crate::data_types::dimens::Dimens;
-use crate::texture::{Id, Properties};
-
-#[derive(Debug)]
-pub struct Texture
-{
- texture: gl::types::GLuint,
-}
-
-impl Texture
-{
- pub fn new() -> Self
- {
- let mut texture = gl::types::GLuint::default();
-
- unsafe {
- gl::CreateTextures(gl::TEXTURE_2D, 1, &mut texture);
- };
-
- Self { texture }
- }
-
- pub fn bind(&self)
- {
- unsafe {
- gl::BindTexture(gl::TEXTURE_2D, self.texture);
- }
- }
-
- pub fn generate(
- &mut self,
- dimens: Dimens<u32>,
- data: &[u8],
- pixel_data_format: PixelDataFormat,
- )
- {
- self.alloc_image(pixel_data_format, dimens, data);
-
- unsafe {
- gl::GenerateTextureMipmap(self.texture);
- }
- }
-
- pub fn apply_properties(&mut self, properties: &Properties)
- {
- self.set_wrap(properties.wrap);
- self.set_magnifying_filter(properties.magnifying_filter);
- self.set_minifying_filter(properties.minifying_filter);
- }
-
- pub fn set_wrap(&mut self, wrapping: Wrapping)
- {
- let wrapping_gl = wrapping.to_gl();
-
- #[allow(clippy::cast_possible_wrap)]
- unsafe {
- gl::TextureParameteri(self.texture, gl::TEXTURE_WRAP_S, wrapping_gl as i32);
- gl::TextureParameteri(self.texture, gl::TEXTURE_WRAP_T, wrapping_gl as i32);
- }
- }
-
- pub fn set_magnifying_filter(&mut self, filtering: Filtering)
- {
- let filtering_gl = filtering.to_gl();
-
- #[allow(clippy::cast_possible_wrap)]
- unsafe {
- gl::TextureParameteri(
- self.texture,
- gl::TEXTURE_MAG_FILTER,
- filtering_gl as i32,
- );
- }
- }
-
- pub fn set_minifying_filter(&mut self, filtering: Filtering)
- {
- let filtering_gl = filtering.to_gl();
-
- #[allow(clippy::cast_possible_wrap)]
- unsafe {
- gl::TextureParameteri(
- self.texture,
- gl::TEXTURE_MIN_FILTER,
- filtering_gl as i32,
- );
- }
- }
-
- fn alloc_image(
- &mut self,
- pixel_data_format: PixelDataFormat,
- dimens: Dimens<u32>,
- data: &[u8],
- )
- {
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::TextureStorage2D(
- self.texture,
- 1,
- pixel_data_format.to_sized_internal_format(),
- dimens.width as i32,
- dimens.height as i32,
- );
-
- #[allow(clippy::cast_possible_wrap)]
- gl::TextureSubImage2D(
- self.texture,
- 0,
- 0,
- 0,
- dimens.width as i32,
- dimens.height as i32,
- pixel_data_format.to_format(),
- gl::UNSIGNED_BYTE,
- data.as_ptr().cast(),
- );
- }
- }
-}
-
-impl Drop for Texture
-{
- fn drop(&mut self)
- {
- unsafe {
- gl::DeleteTextures(1, &self.texture);
- }
- }
-}
-
-/// Texture wrapping.
-#[derive(Debug, Clone, Copy)]
-pub enum Wrapping
-{
- Repeat,
- MirroredRepeat,
- ClampToEdge,
- ClampToBorder,
-}
-
-impl Wrapping
-{
- fn to_gl(self) -> gl::types::GLenum
- {
- match self {
- Self::Repeat => gl::REPEAT,
- Self::MirroredRepeat => gl::MIRRORED_REPEAT,
- Self::ClampToEdge => gl::CLAMP_TO_EDGE,
- Self::ClampToBorder => gl::CLAMP_TO_BORDER,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub enum Filtering
-{
- Nearest,
- Linear,
-}
-
-impl Filtering
-{
- fn to_gl(self) -> gl::types::GLenum
- {
- match self {
- Self::Linear => gl::LINEAR,
- Self::Nearest => gl::NEAREST,
- }
- }
-}
-
-/// Texture pixel data format.
-#[derive(Debug, Clone, Copy)]
-pub enum PixelDataFormat
-{
- Rgb8,
- Rgba8,
-}
-
-impl PixelDataFormat
-{
- fn to_sized_internal_format(self) -> gl::types::GLenum
- {
- match self {
- Self::Rgb8 => gl::RGB8,
- Self::Rgba8 => gl::RGBA8,
- }
- }
-
- fn to_format(self) -> gl::types::GLenum
- {
- match self {
- Self::Rgb8 => gl::RGB,
- Self::Rgba8 => gl::RGBA,
- }
- }
-}
-
-pub fn set_active_texture_unit(texture_unit: TextureUnit)
-{
- unsafe {
- gl::ActiveTexture(texture_unit.into_gl());
- }
-}
-
-macro_rules! texture_unit_enum {
- (cnt=$cnt: literal) => {
- seq_macro::seq!(N in 0..$cnt {
- #[derive(Debug, Clone, Copy)]
- pub enum TextureUnit {
- #(
- No~N,
- )*
- }
-
- impl TextureUnit {
- fn into_gl(self) -> gl::types::GLenum {
- match self {
- #(
- Self::No~N => gl::TEXTURE~N,
- )*
- }
- }
-
- pub fn from_texture_id(texture_id: Id) -> Option<Self> {
- match texture_id.into_inner() {
- #(
- N => Some(Self::No~N),
- )*
- _ => None
- }
- }
- }
- });
- };
-}
-
-texture_unit_enum!(cnt = 31);
diff --git a/engine/src/opengl/util.rs b/engine/src/opengl/util.rs
deleted file mode 100644
index e60778f..0000000
--- a/engine/src/opengl/util.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-// May only be used when certain crate features are enabled
-#![allow(unused_macros, unused_imports)]
-
-macro_rules! gl_enum {
- (
- $visibility: vis enum $name: ident
- {$(
- $variant: ident = gl::$gl_enum: ident,
- )+}
- ) => {
- #[derive(Debug, Clone, Copy)]
- #[repr(u32)]
- $visibility enum $name
- {$(
- $variant = gl::$gl_enum,
- )+}
-
- impl $name {
- fn from_gl(num: gl::types::GLenum) -> Option<Self>
- {
- match num {
- $(gl::$gl_enum => Some(Self::$variant),)+
- _ => None
- }
- }
- }
- };
-}
-
-pub(crate) use gl_enum;
diff --git a/engine/src/opengl/vertex_array.rs b/engine/src/opengl/vertex_array.rs
deleted file mode 100644
index da5d91e..0000000
--- a/engine/src/opengl/vertex_array.rs
+++ /dev/null
@@ -1,183 +0,0 @@
-use std::mem::size_of;
-
-use crate::opengl::buffer::Buffer;
-use crate::vertex::Vertex;
-
-#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
-const VERTEX_STRIDE: i32 = size_of::<Vertex>() as i32;
-
-#[derive(Debug)]
-pub struct VertexArray
-{
- array: gl::types::GLuint,
-}
-
-impl VertexArray
-{
- pub fn new() -> Self
- {
- let mut array = 0;
-
- unsafe {
- gl::CreateVertexArrays(1, &mut array);
- }
-
- Self { array }
- }
-
- /// Draws the currently bound vertex array.
- pub fn draw_arrays(primitive_kind: PrimitiveKind, start_index: u32, cnt: u32)
- {
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::DrawArrays(
- primitive_kind.into_gl(),
- start_index as gl::types::GLint,
- cnt as gl::types::GLsizei,
- );
- }
- }
-
- /// Draws the currently bound vertex array.
- pub fn draw_elements(primitive_kind: PrimitiveKind, offset: u32, cnt: u32)
- {
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::DrawElements(
- primitive_kind.into_gl(),
- cnt as gl::types::GLsizei,
- gl::UNSIGNED_INT,
- (offset as gl::types::GLint) as *const _,
- );
- }
- }
-
- pub fn bind_element_buffer(&mut self, element_buffer: &Buffer<u32>)
- {
- unsafe {
- gl::VertexArrayElementBuffer(self.array, element_buffer.object());
- }
- }
-
- pub fn bind_vertex_buffer(
- &mut self,
- binding_index: u32,
- vertex_buffer: &Buffer<Vertex>,
- offset: isize,
- )
- {
- unsafe {
- gl::VertexArrayVertexBuffer(
- self.array,
- binding_index,
- vertex_buffer.object(),
- offset,
- VERTEX_STRIDE,
- );
- }
- }
-
- pub fn enable_attrib(&mut self, attrib_index: u32)
- {
- unsafe {
- gl::EnableVertexArrayAttrib(self.array, attrib_index as gl::types::GLuint);
- }
- }
-
- pub fn set_attrib_format(
- &mut self,
- attrib_index: u32,
- data_type: DataType,
- normalized: bool,
- offset: u32,
- )
- {
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::VertexArrayAttribFormat(
- self.array,
- attrib_index,
- data_type.size() as gl::types::GLint,
- data_type as u32,
- if normalized { gl::TRUE } else { gl::FALSE },
- offset,
- );
- }
- }
-
- /// Associate a vertex attribute and a vertex buffer binding.
- pub fn set_attrib_vertex_buf_binding(
- &mut self,
- attrib_index: u32,
- vertex_buf_binding_index: u32,
- )
- {
- unsafe {
- gl::VertexArrayAttribBinding(
- self.array,
- attrib_index,
- vertex_buf_binding_index,
- );
- }
- }
-
- pub fn bind(&self)
- {
- unsafe { gl::BindVertexArray(self.array) }
- }
-
- /// Does a weak clone of this vertex array. The vertex array itself is NOT copied in
- /// any way this function only copies the internal vertex array ID.
- ///
- /// # Safety
- /// The returned `VertexArray` must not be dropped if another `VertexArray`
- /// referencing the same vertex array ID is used later.
- pub unsafe fn clone_unsafe(&self) -> Self
- {
- Self { array: self.array }
- }
-}
-
-impl Drop for VertexArray
-{
- fn drop(&mut self)
- {
- unsafe {
- gl::DeleteVertexArrays(1, &self.array);
- }
- }
-}
-
-#[derive(Debug)]
-pub enum PrimitiveKind
-{
- Triangles,
-}
-
-impl PrimitiveKind
-{
- fn into_gl(self) -> gl::types::GLenum
- {
- match self {
- Self::Triangles => gl::TRIANGLES,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-#[repr(u32)]
-pub enum DataType
-{
- Float = gl::FLOAT,
-}
-
-impl DataType
-{
- pub fn size(self) -> u32
- {
- #[allow(clippy::cast_possible_truncation)]
- match self {
- Self::Float => size_of::<gl::types::GLfloat>() as u32,
- }
- }
-}
diff --git a/engine/src/performance.rs b/engine/src/performance.rs
deleted file mode 100644
index ffc5c27..0000000
--- a/engine/src/performance.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-use std::time::Instant;
-
-use ecs::component::local::Local;
-use ecs::system::{Into, System};
-use ecs::Component;
-
-use crate::event::PostPresent as PostPresentEvent;
-
-#[derive(Debug, Default)]
-#[non_exhaustive]
-pub struct Extension {}
-
-impl ecs::extension::Extension for Extension
-{
- fn collect(self, mut collector: ecs::extension::Collector<'_>)
- {
- collector.add_system(
- PostPresentEvent,
- log_perf.into_system().initialize((State::default(),)),
- );
- }
-}
-
-#[cfg(feature = "debug")]
-macro_rules! log_perf {
- ($($tt: tt)*) => {
- tracing::info!($($tt)*);
- };
-}
-
-#[cfg(not(feature = "debug"))]
-macro_rules! log_perf {
- ($($tt: tt)*) => {
- println!($($tt)*);
- };
-}
-
-fn log_perf(mut state: Local<State>)
-{
- let Some(last_time) = state.last_time else {
- state.last_time = Some(Instant::now());
- return;
- };
-
- let time_now = Instant::now();
-
- state.last_time = Some(time_now);
-
- log_perf!(
- "Frame time: {}us",
- time_now.duration_since(last_time).as_micros()
- );
-}
-
-#[derive(Debug, Default, Component)]
-struct State
-{
- last_time: Option<Instant>,
-}
diff --git a/engine/src/projection.rs b/engine/src/projection.rs
index aa84a9f..b3bfa00 100644
--- a/engine/src/projection.rs
+++ b/engine/src/projection.rs
@@ -1,57 +1,210 @@
-use crate::matrix::Matrix;
+use crate::builder;
+use crate::data_types::dimens::Dimens;
+use crate::matrix::{CellPos as MatrixCellPos, Matrix};
+use crate::reflection::Reflection;
+use crate::vector::Vec2;
+use crate::windowing::dpi::PhysicalSize;
-#[derive(Debug)]
+#[derive(Debug, Clone, Reflection)]
#[non_exhaustive]
pub enum Projection
{
Perspective(Perspective),
+ Orthographic(Orthographic),
+}
+
+impl Projection
+{
+ pub fn to_matrix_rh(
+ &self,
+ window_size: PhysicalSize<u32>,
+ clip_volume: ClipVolume,
+ ) -> Matrix<f32, 4, 4>
+ {
+ match self {
+ Projection::Perspective(perspective_proj) => perspective_proj.to_matrix_rh(
+ window_size.width as f32 / window_size.height as f32,
+ clip_volume,
+ ),
+ Projection::Orthographic(orthographic_proj) => {
+ orthographic_proj.to_matrix_rh(window_size, clip_volume)
+ }
+ }
+ }
}
/// Perspective projection parameters.
-#[derive(Debug)]
+#[derive(Debug, Clone, Reflection)]
pub struct Perspective
{
- pub fov_radians: f32,
+ /// Field-of-view in degrees
+ pub fov_degs: f32,
pub far: f32,
pub near: f32,
}
+impl Perspective
+{
+ /// Creates a perspective projection matrix using right-handed coordinates.
+ #[inline]
+ pub fn to_matrix_rh(&self, aspect: f32, clip_volume: ClipVolume)
+ -> Matrix<f32, 4, 4>
+ {
+ let mut out = Matrix::new();
+
+ let fov_rads = self.fov_degs.to_radians();
+
+ match clip_volume {
+ ClipVolume::NegOneToOne => {
+ out[MatrixCellPos { row: 0, col: 0 }] =
+ (1.0 / (fov_rads / 2.0).tan()) / aspect;
+ out[MatrixCellPos { row: 1, col: 1 }] = 1.0 / (fov_rads / 2.0).tan();
+ out[MatrixCellPos { row: 2, col: 2 }] =
+ (self.near + self.far) / (self.near - self.far);
+ out[MatrixCellPos { row: 2, col: 3 }] =
+ (2.0 * self.near * self.far) / (self.near - self.far);
+ out[MatrixCellPos { row: 3, col: 2 }] = -1.0;
+ }
+ }
+
+ out
+ }
+}
+
impl Default for Perspective
{
fn default() -> Self
{
Self {
- fov_radians: 80.0f32.to_radians(),
+ fov_degs: 80.0,
far: 100.0,
near: 0.1,
}
}
}
-pub(crate) fn new_perspective_matrix(
- perspective: &Perspective,
- aspect: f32,
-) -> Matrix<f32, 4, 4>
+#[derive(Debug, Default, Clone, Reflection)]
+pub enum OrthographicSize
+{
+ FixedSize(Dimens<f32>),
+
+ #[default]
+ WindowSize,
+}
+
+builder! {
+#[builder(name = OrthographicBuilder, derives=(Debug, Clone))]
+#[derive(Debug, Clone, Reflection)]
+#[non_exhaustive]
+pub struct Orthographic
+{
+ pub near: f32,
+ pub far: f32,
+ pub viewport_origin: Vec2<f32>,
+ pub size: OrthographicSize,
+}
+}
+
+impl Orthographic
+{
+ pub fn builder() -> OrthographicBuilder
+ {
+ OrthographicBuilder::default()
+ }
+
+ /// Creates a orthographic projection matrix using right-handed coordinates.
+ pub fn to_matrix_rh(
+ &self,
+ window_size: PhysicalSize<u32>,
+ clip_volume: ClipVolume,
+ ) -> Matrix<f32, 4, 4>
+ {
+ let size = match self.size {
+ OrthographicSize::FixedSize(fixed_size) => fixed_size,
+ OrthographicSize::WindowSize => Dimens {
+ width: window_size.width as f32,
+ height: window_size.height as f32,
+ },
+ };
+
+ let origin_x = size.width * self.viewport_origin.x;
+ let origin_y = size.height * self.viewport_origin.y;
+
+ let left = -origin_x;
+ let right = size.width - origin_x;
+
+ let bottom = -origin_y;
+ let top = size.height - origin_y;
+
+ let near = self.near;
+ let far = self.far;
+
+ orthographic_rh(
+ OrthographicParams { left, right, bottom, top, near, far },
+ clip_volume,
+ )
+ }
+}
+
+impl Default for Orthographic
{
- let mut out = Matrix::new();
+ fn default() -> Self
+ {
+ Self::builder().build()
+ }
+}
- out.set_cell(0, 0, (1.0 / (perspective.fov_radians / 2.0).tan()) / aspect);
+impl Default for OrthographicBuilder
+{
+ fn default() -> Self
+ {
+ Self {
+ near: 0.0,
+ far: 1000.0,
+ viewport_origin: Vec2 { x: 0.5, y: 0.5 },
+ size: OrthographicSize::WindowSize,
+ }
+ }
+}
- out.set_cell(1, 1, 1.0 / (perspective.fov_radians / 2.0).tan());
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[non_exhaustive]
+pub enum ClipVolume
+{
+ /// -1 to +1. This is the OpenGL clip volume definition.
+ NegOneToOne,
+}
- out.set_cell(
- 2,
- 2,
- (perspective.near + perspective.far) / (perspective.near - perspective.far),
- );
+#[derive(Debug, Clone)]
+pub struct OrthographicParams
+{
+ pub left: f32,
+ pub right: f32,
+ pub bottom: f32,
+ pub top: f32,
+ pub near: f32,
+ pub far: f32,
+}
- out.set_cell(
- 2,
- 3,
- (2.0 * perspective.near * perspective.far) / (perspective.near - perspective.far),
- );
+/// Creates a orthographic projection matrix using right-handed coordinates.
+pub fn orthographic_rh(
+ OrthographicParams { left, right, bottom, top, near, far }: OrthographicParams,
+ clip_volume: ClipVolume,
+) -> Matrix<f32, 4, 4>
+{
+ let mut result = Matrix::<f32, 4, 4>::new();
- out.set_cell(3, 2, -1.0);
+ match clip_volume {
+ ClipVolume::NegOneToOne => {
+ result[MatrixCellPos { row: 0, col: 0 }] = 2.0 / (right - left);
+ result[MatrixCellPos { row: 1, col: 1 }] = 2.0 / (top - bottom);
+ result[MatrixCellPos { row: 2, col: 2 }] = -2.0 / (far - near);
+ result[MatrixCellPos { row: 0, col: 3 }] = -(right + left) / (right - left);
+ result[MatrixCellPos { row: 1, col: 3 }] = -(top + bottom) / (top - bottom);
+ result[MatrixCellPos { row: 2, col: 3 }] = -(far + near) / (far - near);
+ result[MatrixCellPos { row: 3, col: 3 }] = 1.0;
+ }
+ }
- out
+ result
}
diff --git a/engine/src/reflection.rs b/engine/src/reflection.rs
new file mode 100644
index 0000000..fd7e379
--- /dev/null
+++ b/engine/src/reflection.rs
@@ -0,0 +1,2 @@
+pub use engine_macros::Reflection;
+pub use engine_reflection::*;
diff --git a/engine/src/renderer.rs b/engine/src/renderer.rs
deleted file mode 100644
index 2544919..0000000
--- a/engine/src/renderer.rs
+++ /dev/null
@@ -1 +0,0 @@
-pub mod opengl;
diff --git a/engine/src/renderer/opengl.rs b/engine/src/renderer/opengl.rs
deleted file mode 100644
index a353c6a..0000000
--- a/engine/src/renderer/opengl.rs
+++ /dev/null
@@ -1,719 +0,0 @@
-//! OpenGL renderer.
-
-use std::collections::HashMap;
-use std::ffi::{c_void, CString};
-use std::ops::Deref;
-use std::process::abort;
-
-use ecs::actions::Actions;
-use ecs::component::local::Local;
-use ecs::query::options::{Not, With};
-use ecs::sole::Single;
-use ecs::system::{Into as _, System};
-use ecs::{Component, Query};
-
-use crate::camera::{Active as ActiveCamera, Camera};
-use crate::color::Color;
-use crate::data_types::dimens::Dimens;
-use crate::draw_flags::{DrawFlags, NoDraw, PolygonModeConfig};
-use crate::event::{Present as PresentEvent, Start as StartEvent};
-use crate::lighting::{DirectionalLight, GlobalLight, PointLight};
-use crate::material::{Flags as MaterialFlags, Material};
-use crate::matrix::Matrix;
-use crate::mesh::Mesh;
-use crate::opengl::buffer::{Buffer, Usage as BufferUsage};
-#[cfg(feature = "debug")]
-use crate::opengl::debug::{MessageSeverity, MessageSource, MessageType};
-use crate::opengl::shader::{
- Error as GlShaderError,
- Program as GlShaderProgram,
- Shader as GlShader,
-};
-use crate::opengl::texture::{
- set_active_texture_unit,
- Texture as GlTexture,
- TextureUnit,
-};
-use crate::opengl::vertex_array::{
- DataType as VertexArrayDataType,
- PrimitiveKind,
- VertexArray,
-};
-use crate::opengl::{clear_buffers, enable, BufferClearMask, Capability};
-use crate::projection::{new_perspective_matrix, Projection};
-use crate::shader::Program as ShaderProgram;
-use crate::texture::{Id as TextureId, Texture};
-use crate::transform::{Position, Scale};
-use crate::util::NeverDrop;
-use crate::vector::{Vec2, Vec3};
-use crate::vertex::{AttributeComponentType, Vertex};
-use crate::window::Window;
-
-type RenderableEntity = (
- Mesh,
- ShaderProgram,
- Material,
- Option<MaterialFlags>,
- Option<Position>,
- Option<Scale>,
- Option<DrawFlags>,
- Option<GlObjects>,
-);
-
-#[derive(Debug, Default)]
-pub struct Extension {}
-
-impl ecs::extension::Extension for Extension
-{
- fn collect(self, mut collector: ecs::extension::Collector<'_>)
- {
- collector.add_system(StartEvent, initialize);
-
- collector.add_system(
- PresentEvent,
- render
- .into_system()
- .initialize((GlobalGlObjects::default(),)),
- );
- }
-}
-
-fn initialize(window: Single<Window>)
-{
- window
- .make_context_current()
- .expect("Failed to make window context current");
-
- gl::load_with(|symbol| match window.get_proc_address(symbol) {
- Ok(addr) => addr as *const c_void,
- Err(err) => {
- println!(
- "FATAL ERROR: Failed to get adress of OpenGL function {symbol}: {err}",
- );
-
- abort();
- }
- });
-
- #[cfg(feature = "debug")]
- initialize_debug();
-
- let window_size = window.size().expect("Failed to get window size");
-
- set_viewport(Vec2 { x: 0, y: 0 }, window_size);
-
- window.set_framebuffer_size_callback(|new_window_size| {
- set_viewport(Vec2::ZERO, new_window_size);
- });
-
- enable(Capability::DepthTest);
- enable(Capability::MultiSample);
-}
-
-#[allow(clippy::too_many_arguments)]
-fn render(
- query: Query<RenderableEntity, Not<With<NoDraw>>>,
- point_light_query: Query<(PointLight,)>,
- directional_lights: Query<(DirectionalLight,)>,
- camera_query: Query<(Camera, Position, ActiveCamera)>,
- window: Single<Window>,
- global_light: Single<GlobalLight>,
- mut gl_objects: Local<GlobalGlObjects>,
- mut actions: Actions,
-)
-{
- let Some((camera, camera_pos, _)) = camera_query.iter().next() else {
- #[cfg(feature = "debug")]
- tracing::warn!("No current camera. Nothing will be rendered");
- return;
- };
-
- let point_lights = point_light_query
- .iter()
- .map(|(point_light,)| point_light)
- .collect::<Vec<_>>();
-
- let directional_lights = directional_lights.iter().collect::<Vec<_>>();
-
- let GlobalGlObjects {
- shader_programs: gl_shader_programs,
- textures: gl_textures,
- } = &mut *gl_objects;
-
- clear_buffers(BufferClearMask::COLOR | BufferClearMask::DEPTH);
-
- for (
- entity_index,
- (
- mesh,
- shader_program,
- material,
- material_flags,
- position,
- scale,
- draw_flags,
- gl_objects,
- ),
- ) in query.iter().enumerate()
- {
- let material_flags = material_flags
- .map(|material_flags| material_flags.clone())
- .unwrap_or_default();
-
- let shader_program = gl_shader_programs
- .entry(shader_program.u64_hash())
- .or_insert_with(|| create_gl_shader_program(&shader_program).unwrap());
-
- let new_gl_objects;
-
- let gl_objects = if let Some(gl_objects) = gl_objects.as_deref() {
- gl_objects
- } else {
- // TODO: Account for when meshes are changed
- let gl_objects = GlObjects::new(&mesh);
-
- new_gl_objects = Some(gl_objects.clone());
-
- actions.add_components(
- query.get_entity_uid(entity_index).unwrap(),
- (gl_objects,),
- );
-
- &*new_gl_objects.unwrap()
- };
-
- apply_transformation_matrices(
- Transformation {
- position: position.map(|pos| *pos).unwrap_or_default().position,
- scale: scale.map(|scale| *scale).unwrap_or_default().scale,
- },
- shader_program,
- &camera,
- &camera_pos,
- window.size().expect("Failed to get window size"),
- );
-
- apply_light(
- &material,
- &material_flags,
- &global_light,
- shader_program,
- point_lights.as_slice(),
- directional_lights
- .iter()
- .map(|(dir_light,)| &**dir_light)
- .collect::<Vec<_>>()
- .as_slice(),
- &camera_pos,
- );
-
- for texture in &material.textures {
- let gl_texture = gl_textures
- .entry(texture.id())
- .or_insert_with(|| create_gl_texture(texture));
-
- let texture_unit =
- TextureUnit::from_texture_id(texture.id()).unwrap_or_else(|| {
- panic!("Texture id {} is a invalid texture unit", texture.id());
- });
-
- set_active_texture_unit(texture_unit);
-
- gl_texture.bind();
- }
-
- shader_program.activate();
-
- if let Some(draw_flags) = &draw_flags {
- crate::opengl::set_polygon_mode(
- draw_flags.polygon_mode_config.face,
- draw_flags.polygon_mode_config.mode,
- );
- }
-
- draw_mesh(gl_objects);
-
- if draw_flags.is_some() {
- let default_polygon_mode_config = PolygonModeConfig::default();
-
- crate::opengl::set_polygon_mode(
- default_polygon_mode_config.face,
- default_polygon_mode_config.mode,
- );
- }
- }
-}
-
-#[derive(Debug, Default, Component)]
-struct GlobalGlObjects
-{
- shader_programs: HashMap<u64, GlShaderProgram>,
- textures: HashMap<TextureId, GlTexture>,
-}
-
-fn set_viewport(position: Vec2<u32>, size: Dimens<u32>)
-{
- crate::opengl::set_viewport(position, size);
-}
-
-#[cfg(feature = "debug")]
-fn initialize_debug()
-{
- use crate::opengl::debug::{
- enable_debug_output,
- set_debug_message_callback,
- set_debug_message_control,
- MessageIdsAction,
- };
-
- enable_debug_output();
-
- set_debug_message_callback(opengl_debug_message_cb);
-
- set_debug_message_control(None, None, None, &[], MessageIdsAction::Disable);
-}
-
-fn draw_mesh(gl_objects: &GlObjects)
-{
- gl_objects.vertex_arr.bind();
-
- if gl_objects.index_buffer.is_some() {
- VertexArray::draw_elements(PrimitiveKind::Triangles, 0, gl_objects.index_cnt);
- } else {
- VertexArray::draw_arrays(PrimitiveKind::Triangles, 0, 3);
- }
-}
-
-fn create_gl_texture(texture: &Texture) -> GlTexture
-{
- let mut gl_texture = GlTexture::new();
-
- gl_texture.generate(
- *texture.dimensions(),
- texture.image().as_bytes(),
- texture.pixel_data_format(),
- );
-
- gl_texture.apply_properties(texture.properties());
-
- gl_texture
-}
-
-fn create_gl_shader_program(
- shader_program: &ShaderProgram,
-) -> Result<GlShaderProgram, GlShaderError>
-{
- let gl_shaders = shader_program
- .shaders()
- .iter()
- .map(|shader| {
- let gl_shader = GlShader::new(shader.kind());
-
- gl_shader.set_source(shader.source())?;
- gl_shader.compile()?;
-
- Ok(gl_shader)
- })
- .collect::<Result<Vec<_>, _>>()?;
-
- let gl_shader_program = GlShaderProgram::new();
-
- for gl_shader in &gl_shaders {
- gl_shader_program.attach(gl_shader);
- }
-
- gl_shader_program.link()?;
-
- Ok(gl_shader_program)
-}
-
-#[derive(Debug, Component)]
-struct GlObjects
-{
- /// Vertex and index buffer has to live as long as the vertex array
- vertex_buffer: Buffer<Vertex>,
- index_buffer: Option<Buffer<u32>>,
- index_cnt: u32,
-
- vertex_arr: VertexArray,
-}
-
-impl GlObjects
-{
- #[cfg_attr(feature = "debug", tracing::instrument(skip_all))]
- fn new(mesh: &Mesh) -> Self
- {
- #[cfg(feature = "debug")]
- tracing::trace!(
- "Creating vertex array, vertex buffer{}",
- if mesh.indices().is_some() {
- " and index buffer"
- } else {
- ""
- }
- );
-
- let mut vertex_arr = VertexArray::new();
- let mut vertex_buffer = Buffer::new();
-
- vertex_buffer.store(mesh.vertices(), BufferUsage::Static);
-
- vertex_arr.bind_vertex_buffer(0, &vertex_buffer, 0);
-
- let mut offset = 0u32;
-
- for attrib in Vertex::attrs() {
- vertex_arr.enable_attrib(attrib.index);
-
- vertex_arr.set_attrib_format(
- attrib.index,
- match attrib.component_type {
- AttributeComponentType::Float => VertexArrayDataType::Float,
- },
- false,
- offset,
- );
-
- vertex_arr.set_attrib_vertex_buf_binding(attrib.index, 0);
-
- offset += attrib.component_size * attrib.component_cnt as u32;
- }
-
- if let Some(indices) = mesh.indices() {
- let mut index_buffer = Buffer::new();
-
- index_buffer.store(indices, BufferUsage::Static);
-
- vertex_arr.bind_element_buffer(&index_buffer);
-
- return Self {
- vertex_buffer,
- index_buffer: Some(index_buffer),
- index_cnt: indices.len().try_into().unwrap(),
- vertex_arr,
- };
- }
-
- Self {
- vertex_buffer,
- index_buffer: None,
- index_cnt: 0,
- vertex_arr,
- }
- }
-
- pub fn clone(&self) -> NeverDrop<Self>
- {
- NeverDrop::new(Self {
- // SAFETY: The vertex buffer will never become dropped (NeverDrop ensures it)
- vertex_buffer: unsafe { self.vertex_buffer.clone_weak() },
- index_buffer: self
- .index_buffer
- .as_ref()
- // SAFETY: The index buffer will never become dropped (NeverDrop ensures
- // it)
- .map(|index_buffer| unsafe { index_buffer.clone_weak() }),
- index_cnt: self.index_cnt,
- // SAFETY: The vertex array will never become dropped (NeverDrop ensures it)
- vertex_arr: unsafe { self.vertex_arr.clone_unsafe() },
- })
- }
-}
-
-fn apply_transformation_matrices(
- transformation: Transformation,
- gl_shader_program: &mut GlShaderProgram,
- camera: &Camera,
- camera_pos: &Position,
- window_size: Dimens<u32>,
-)
-{
- gl_shader_program
- .set_uniform_matrix_4fv(c"model", &create_transformation_matrix(transformation));
-
- let view = create_view(camera, camera_pos);
-
- gl_shader_program.set_uniform_matrix_4fv(c"view", &view);
-
- #[allow(clippy::cast_precision_loss)]
- let projection = match &camera.projection {
- Projection::Perspective(perspective) => new_perspective_matrix(
- perspective,
- window_size.width as f32 / window_size.height as f32,
- ),
- };
-
- gl_shader_program.set_uniform_matrix_4fv(c"projection", &projection);
-}
-
-fn apply_light<PointLightHolder>(
- material: &Material,
- material_flags: &MaterialFlags,
- global_light: &GlobalLight,
- gl_shader_program: &mut GlShaderProgram,
- point_lights: &[PointLightHolder],
- directional_lights: &[&DirectionalLight],
- camera_pos: &Position,
-) where
- PointLightHolder: Deref<Target = PointLight>,
-{
- debug_assert!(
- point_lights.len() < 64,
- "Shader cannot handle more than 64 point lights"
- );
-
- debug_assert!(
- directional_lights.len() < 64,
- "Shader cannot handle more than 64 directional lights"
- );
-
- for (dir_light_index, dir_light) in directional_lights.iter().enumerate() {
- gl_shader_program.set_uniform_vec_3fv(
- &create_light_uniform_name(
- "directional_lights",
- dir_light_index,
- "direction",
- ),
- &dir_light.direction,
- );
-
- set_light_phong_uniforms(
- gl_shader_program,
- "directional_lights",
- dir_light_index,
- *dir_light,
- );
- }
-
- // There probably won't be more than 2147483648 directional lights
- #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
- gl_shader_program
- .set_uniform_1i(c"directional_light_cnt", directional_lights.len() as i32);
-
- for (point_light_index, point_light) in point_lights.iter().enumerate() {
- gl_shader_program.set_uniform_vec_3fv(
- &create_light_uniform_name("point_lights", point_light_index, "position"),
- &point_light.position,
- );
-
- set_light_phong_uniforms(
- gl_shader_program,
- "point_lights",
- point_light_index,
- &**point_light,
- );
-
- set_light_attenuation_uniforms(
- gl_shader_program,
- "point_lights",
- point_light_index,
- point_light,
- );
- }
-
- // There probably won't be more than 2147483648 point lights
- #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
- gl_shader_program.set_uniform_1i(c"point_light_cnt", point_lights.len() as i32);
-
- gl_shader_program.set_uniform_vec_3fv(
- c"material.ambient",
- &if material_flags.use_ambient_color {
- material.ambient.clone()
- } else {
- global_light.ambient.clone()
- }
- .into(),
- );
-
- gl_shader_program
- .set_uniform_vec_3fv(c"material.diffuse", &material.diffuse.clone().into());
-
- #[allow(clippy::cast_possible_wrap)]
- gl_shader_program
- .set_uniform_vec_3fv(c"material.specular", &material.specular.clone().into());
-
- #[allow(clippy::cast_possible_wrap)]
- gl_shader_program.set_uniform_1i(
- c"material.ambient_map",
- material.ambient_map.into_inner() as i32,
- );
-
- #[allow(clippy::cast_possible_wrap)]
- gl_shader_program.set_uniform_1i(
- c"material.diffuse_map",
- material.diffuse_map.into_inner() as i32,
- );
-
- #[allow(clippy::cast_possible_wrap)]
- gl_shader_program.set_uniform_1i(
- c"material.specular_map",
- material.specular_map.into_inner() as i32,
- );
-
- gl_shader_program.set_uniform_1fv(c"material.shininess", material.shininess);
-
- gl_shader_program.set_uniform_vec_3fv(c"view_pos", &camera_pos.position);
-}
-
-fn set_light_attenuation_uniforms(
- gl_shader_program: &mut GlShaderProgram,
- light_array: &str,
- light_index: usize,
- light: &PointLight,
-)
-{
- gl_shader_program.set_uniform_1fv(
- &create_light_uniform_name(
- light_array,
- light_index,
- "attenuation_props.constant",
- ),
- light.attenuation_params.constant,
- );
-
- gl_shader_program.set_uniform_1fv(
- &create_light_uniform_name(light_array, light_index, "attenuation_props.linear"),
- light.attenuation_params.linear,
- );
-
- gl_shader_program.set_uniform_1fv(
- &create_light_uniform_name(
- light_array,
- light_index,
- "attenuation_props.quadratic",
- ),
- light.attenuation_params.quadratic,
- );
-}
-
-fn set_light_phong_uniforms(
- gl_shader_program: &mut GlShaderProgram,
- light_array: &str,
- light_index: usize,
- light: &impl Light,
-)
-{
- gl_shader_program.set_uniform_vec_3fv(
- &create_light_uniform_name(light_array, light_index, "phong.diffuse"),
- &light.diffuse().clone().into(),
- );
-
- gl_shader_program.set_uniform_vec_3fv(
- &create_light_uniform_name(light_array, light_index, "phong.specular"),
- &light.specular().clone().into(),
- );
-}
-
-trait Light
-{
- fn diffuse(&self) -> &Color<f32>;
- fn specular(&self) -> &Color<f32>;
-}
-
-impl Light for PointLight
-{
- fn diffuse(&self) -> &Color<f32>
- {
- &self.diffuse
- }
-
- fn specular(&self) -> &Color<f32>
- {
- &self.specular
- }
-}
-
-impl Light for DirectionalLight
-{
- fn diffuse(&self) -> &Color<f32>
- {
- &self.diffuse
- }
-
- fn specular(&self) -> &Color<f32>
- {
- &self.specular
- }
-}
-
-fn create_light_uniform_name(
- light_array: &str,
- light_index: usize,
- light_field: &str,
-) -> CString
-{
- unsafe {
- CString::from_vec_with_nul_unchecked(
- format!("{light_array}[{light_index}].{light_field}\0").into(),
- )
- }
-}
-
-fn create_view(camera: &Camera, camera_pos: &Position) -> Matrix<f32, 4, 4>
-{
- let mut view = Matrix::new();
-
- view.look_at(&camera_pos.position, &camera.target, &camera.global_up);
-
- view
-}
-
-#[cfg(feature = "debug")]
-#[tracing::instrument(skip_all)]
-fn opengl_debug_message_cb(
- source: MessageSource,
- ty: MessageType,
- id: u32,
- severity: MessageSeverity,
- message: &str,
-)
-{
- use std::backtrace::{Backtrace, BacktraceStatus};
-
- use tracing::{event, Level};
-
- macro_rules! create_event {
- ($level: expr) => {
- event!($level, ?source, ?ty, id, ?severity, message);
- };
- }
-
- if matches!(severity, MessageSeverity::Notification) {
- return;
- }
-
- match ty {
- MessageType::Error => {
- create_event!(Level::ERROR);
-
- let backtrace = Backtrace::capture();
-
- if matches!(backtrace.status(), BacktraceStatus::Captured) {
- event!(Level::TRACE, "{backtrace}");
- }
- }
- MessageType::Other => {
- create_event!(Level::INFO);
- }
- _ => {
- create_event!(Level::WARN);
- }
- };
-}
-
-#[derive(Debug)]
-struct Transformation
-{
- position: Vec3<f32>,
- scale: Vec3<f32>,
-}
-
-fn create_transformation_matrix(transformation: Transformation) -> Matrix<f32, 4, 4>
-{
- let mut matrix = Matrix::new_identity();
-
- matrix.translate(&transformation.position);
- matrix.scale(&transformation.scale);
-
- matrix
-}
diff --git a/engine/src/rendering.rs b/engine/src/rendering.rs
new file mode 100644
index 0000000..6840dbe
--- /dev/null
+++ b/engine/src/rendering.rs
@@ -0,0 +1,715 @@
+use std::collections::VecDeque;
+use std::sync::atomic::{AtomicU64, Ordering};
+
+use bitflags::bitflags;
+use engine_macros::Reflection;
+
+use crate::asset::Handle as AssetHandle;
+use crate::builder;
+use crate::data_types::dimens::Dimens;
+use crate::draw_flags::PolygonModeConfig;
+use crate::ecs::actions::Actions;
+use crate::ecs::component::local::Local;
+use crate::ecs::event::component::{EventMatchExt, Removed};
+use crate::ecs::pair::{ChildOf, Pair};
+use crate::ecs::phase::{Phase, POST_UPDATE as POST_UPDATE_PHASE};
+use crate::ecs::query::term::With;
+use crate::ecs::sole::Single;
+use crate::ecs::system::initializable::Initializable;
+use crate::ecs::system::observer::Observe;
+use crate::ecs::system::Into;
+use crate::ecs::{declare_entity, pair, Component, Query, Sole};
+use crate::image::{ColorType as ImageColorType, Image};
+use crate::mesh::Mesh;
+use crate::rendering::blending::Config as BlendingConfig;
+use crate::rendering::object::{Id as ObjectId, Store as ObjectStore};
+use crate::rendering::shader::cursor::Binding as ShaderBinding;
+use crate::rendering::shader::Program as ShaderProgram;
+use crate::texture::{CubeMapFace, Properties as TextureProperties};
+use crate::vector::Vec2;
+use crate::windowing::dpi::PhysicalSize;
+use crate::windowing::window::Window;
+
+pub mod backend;
+pub mod blending;
+pub mod main_render_pass;
+pub mod object;
+pub mod shader;
+
+static NEXT_SURFACE_ID: AtomicU64 = AtomicU64::new(0);
+
+declare_entity! {
+pub PRE_RENDER_PHASE: (Phase, pair!(ChildOf, { *POST_UPDATE_PHASE }));
+
+pub RENDER_PHASE: (Phase, pair!(ChildOf, { *PRE_RENDER_PHASE }));
+
+pub POST_RENDER_PHASE: (Phase, pair!(ChildOf, { *RENDER_PHASE }));
+}
+
+builder! {
+#[builder(name=ExtensionBuilder, derives=(Debug, Clone, Default))]
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct Extension {
+ pub graphics_props: GraphicsProperties,
+}
+}
+
+impl Extension
+{
+ pub fn builder() -> ExtensionBuilder
+ {
+ ExtensionBuilder::default()
+ }
+}
+
+impl crate::ecs::extension::Extension for Extension
+{
+ fn collect(self, mut collector: crate::ecs::extension::Collector<'_>)
+ {
+ collector.spawn_declared_entity(&PRE_RENDER_PHASE);
+ collector.spawn_declared_entity(&RENDER_PHASE);
+ collector.spawn_declared_entity(&POST_RENDER_PHASE);
+
+ let _ = collector.add_sole(RenderPasses::default());
+ let _ = collector.add_sole(CommandQueue::default());
+ let _ = collector.add_sole(ObjectStore::default());
+
+ let _ = collector.add_sole(self.graphics_props);
+
+ collector.add_system(
+ *PRE_RENDER_PHASE,
+ main_render_pass::add_main_render_pass
+ .into_system()
+ .initialize(Default::default()),
+ );
+
+ collector.add_system(
+ *RENDER_PHASE,
+ enqueue_commands_from_render_passes
+ .into_system()
+ .initialize((ActiveDrawProperties::default(),)),
+ );
+
+ collector.add_observer(handle_window_removed);
+
+ crate::rendering::shader::prepare(&mut collector);
+
+ crate::rendering::backend::get_default().collect(collector);
+ }
+}
+
+impl Default for Extension
+{
+ fn default() -> Self
+ {
+ Self::builder().build()
+ }
+}
+
+/// Marker component for windows that should be renderer to.
+#[derive(Debug, Default, Clone, Copy, Component, Reflection)]
+pub struct TargetWindow;
+
+builder! {
+#[builder(name=GraphicsPropertiesBuilder, derives=(Debug, Clone))]
+#[derive(Debug, Clone, Sole)]
+#[non_exhaustive]
+pub struct GraphicsProperties
+{
+ /// Number of samples for multisampling. `None` means no multisampling.
+ #[builder(skip_generate_fn)]
+ pub multisampling_sample_cnt: Option<u8>,
+
+ /// Whether graphics API debugging is enabled.
+ pub debug: bool,
+
+ /// Whether depth testing is enabled
+ pub depth_test: bool,
+}
+}
+
+impl GraphicsProperties
+{
+ pub fn builder() -> GraphicsPropertiesBuilder
+ {
+ GraphicsPropertiesBuilder::default()
+ }
+}
+
+impl Default for GraphicsProperties
+{
+ fn default() -> Self
+ {
+ Self::builder().build()
+ }
+}
+
+impl GraphicsPropertiesBuilder
+{
+ pub fn multisampling_sample_cnt(mut self, multisampling_sample_cnt: u8) -> Self
+ {
+ self.multisampling_sample_cnt = Some(multisampling_sample_cnt);
+ self
+ }
+
+ pub fn no_multisampling(mut self) -> Self
+ {
+ self.multisampling_sample_cnt = None;
+ self
+ }
+}
+
+impl Default for GraphicsPropertiesBuilder
+{
+ fn default() -> Self
+ {
+ Self {
+ multisampling_sample_cnt: Some(4),
+ debug: false,
+ depth_test: true,
+ }
+ }
+}
+
+#[derive(Debug, Component)]
+pub struct Surface
+{
+ pub id: SurfaceId,
+ pub size: PhysicalSize<u32>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct SurfaceId
+{
+ inner: u64,
+}
+
+impl SurfaceId
+{
+ pub fn new_unique() -> Self
+ {
+ Self {
+ inner: NEXT_SURFACE_ID.fetch_add(1, Ordering::Relaxed),
+ }
+ }
+}
+
+#[derive(Debug, Default, Sole)]
+pub struct RenderPasses
+{
+ pub passes: VecDeque<RenderPass>,
+}
+
+#[derive(Debug)]
+pub struct RenderPass
+{
+ pub commands: Vec<Command>,
+ pub draw_properties: DrawProperties,
+}
+
+#[derive(Debug, Reflection)]
+#[non_exhaustive]
+pub enum Command
+{
+ RemoveSurface(SurfaceId),
+ MakeCurrent(SurfaceId),
+ SetSurfaceSize(SurfaceId, PhysicalSize<u32>),
+ ClearBuffers(BufferClearMask),
+ SwapBuffers(SurfaceId),
+ CreateShaderProgram(ObjectId, ShaderProgram),
+ ActivateShader(ObjectId),
+ SetShaderBinding(ObjectId, ShaderBinding),
+ CreateTexture
+ {
+ obj_id: ObjectId,
+ pixel_data_format: TexturePixelDataFormat,
+ creation: TextureCreation,
+ properties: TextureProperties,
+ },
+ UpdateTexture
+ {
+ obj_id: ObjectId,
+ pixels: Box<[u8]>,
+ pixel_data_format: TexturePixelDataFormat,
+ update: TextureUpdate,
+ },
+ RemoveTexture(ObjectId),
+ CreateFramebuffer(ObjectId),
+ RemoveFramebuffer(ObjectId),
+ UpdateFramebufferProperties
+ {
+ object_id: ObjectId,
+ properties: FramebufferProperties,
+ update_flags: FramebufferPropertiesUpdateFlags,
+ },
+ CreateMesh
+ {
+ obj_id: ObjectId,
+ mesh: AssetOrValue<Mesh>,
+ usage: MeshUsage,
+ },
+ UpdateMesh
+ {
+ obj_id: ObjectId,
+ mesh: Mesh,
+ usage: MeshUsage,
+ },
+ RemoveMesh(ObjectId),
+ DrawMesh(ObjectId, DrawMeshOptions),
+ UpdateDrawProperties(DrawProperties, DrawPropertiesUpdateFlags),
+}
+
+#[derive(Debug)]
+pub enum AssetOrValue<T: 'static>
+{
+ Asset(AssetHandle<T>),
+ Value(T),
+}
+
+builder! {
+ #[builder(name = DrawMeshOptionsBuilder, derives = (Debug, Default, Clone))]
+ #[derive(Debug, Default, Clone)]
+ #[non_exhaustive]
+ pub struct DrawMeshOptions {
+ pub element_offset: u32,
+ pub vertex_offset: u32,
+
+ #[builder(skip_generate_fn)]
+ pub element_cnt: Option<u32>,
+ }
+}
+
+impl DrawMeshOptions
+{
+ pub fn builder() -> DrawMeshOptionsBuilder
+ {
+ DrawMeshOptionsBuilder::default()
+ }
+}
+
+impl DrawMeshOptionsBuilder
+{
+ pub fn element_cnt(mut self, element_cnt: u32) -> Self
+ {
+ self.element_cnt = Some(element_cnt);
+ self
+ }
+}
+
+bitflags! {
+ #[derive(Debug, Clone, Copy)]
+ pub struct BufferClearMask: u8 {
+ const COLOR = 1;
+ const DEPTH = 2;
+ const STENCIL = 3;
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum MeshUsage
+{
+ /// The mesh data is set only once and used by the GPU at most a few times.
+ Stream,
+
+ /// The mesh data is set only once and used many times.
+ Static,
+
+ /// The mesh data is changed a lot and used many times.
+ Dynamic,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ScissorBox
+{
+ /// Size of the scissor box in window coordinates. When `None`, the dimensions of the
+ /// window is used
+ pub size: Option<Dimens<u16>>,
+
+ /// Position (in window coordinates) of the lower left corner of the scissor box.
+ pub lower_left_corner_pos: Vec2<u16>,
+}
+
+impl Default for ScissorBox
+{
+ fn default() -> Self
+ {
+ Self {
+ size: None,
+ lower_left_corner_pos: Vec2 { x: 0, y: 0 },
+ }
+ }
+}
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub enum DepthFunction
+{
+ /// Never passes.
+ Never,
+
+ /// Passes if the incoming depth value is less than the stored depth value.
+ #[default]
+ Less,
+
+ /// Passes if the incoming depth value is equal to the stored depth value.
+ Equal,
+
+ /// Passes if the incoming depth value is less than or equal to the stored depth
+ /// value.
+ LessOrEqual,
+
+ /// Passes if the incoming depth value is greater than the stored depth value.
+ Greater,
+
+ /// Passes if the incoming depth value is not equal to the stored depth value.
+ NotEqual,
+
+ /// Passes if the incoming depth value is greater than or equal to the stored depth
+ /// value.
+ GreaterOrEqual,
+
+ /// Always passes.
+ Always,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub struct DrawProperties
+{
+ pub polygon_mode_config: PolygonModeConfig,
+ pub blending_enabled: bool,
+ pub blending_config: BlendingConfig,
+ pub depth_test_enabled: bool,
+ pub depth_function: DepthFunction,
+ pub scissor_test_enabled: bool,
+ pub scissor_box: ScissorBox,
+ pub face_culling_enabled: bool,
+ pub face_culling_mode: FaceCullingMode,
+
+ /// `None` for default framebuffer
+ pub draw_framebuffer: Option<ObjectId>,
+
+ /// `None` for default framebuffer
+ pub read_framebuffer: Option<ObjectId>,
+}
+
+impl Default for DrawProperties
+{
+ fn default() -> Self
+ {
+ Self {
+ polygon_mode_config: PolygonModeConfig::default(),
+ blending_enabled: false,
+ blending_config: BlendingConfig::default(),
+ depth_test_enabled: true,
+ depth_function: DepthFunction::default(),
+ scissor_test_enabled: false,
+ scissor_box: ScissorBox::default(),
+ face_culling_enabled: false,
+ face_culling_mode: FaceCullingMode::default(),
+ draw_framebuffer: None,
+ read_framebuffer: None,
+ }
+ }
+}
+
+bitflags! {
+ #[derive(Debug, Clone, Copy)]
+ pub struct DrawPropertiesUpdateFlags: u16
+ {
+ const POLYGON_MODE_CONFIG = 1 << 0;
+ const BLENDING_CONFIG = 1 << 1;
+ const BLENDING_ENABLED = 1 << 2;
+ const DEPTH_TEST_ENABLED = 1 << 3;
+ const DEPTH_FUNCTION = 1 << 4;
+ const SCISSOR_TEST_ENABLED = 1 << 5;
+ const SCISSOR_BOX = 1 << 6;
+ const FACE_CULLING_ENABLED = 1 << 7;
+ const FACE_CULLING_MODE = 1 << 8;
+ const DRAW_FRAMEBUFFER = 1 << 9;
+ const READ_FRAMEBUFFER = 1 << 10;
+ }
+}
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum FaceCullingMode
+{
+ Front,
+
+ #[default]
+ Back,
+
+ FrontAndBack,
+}
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub enum TextureCreation
+{
+ Texture2D
+ {
+ size: Dimens<u32>,
+ image: Option<Box<[u8]>>,
+ },
+ CubeMap
+ {
+ size: Dimens<u32>,
+ images: Option<[(CubeMapFace, Box<[u8]>); 6]>,
+ },
+}
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub enum TextureUpdate
+{
+ Texture2D
+ {
+ size: Dimens<u32>,
+
+ /// Specifies texel index offsets in the x and y directions within the texture
+ /// pixels.
+ offset: Vec2<u32>,
+ },
+ CubeMap
+ {
+ size: Dimens<u32>,
+
+ /// Specifies texel index offsets in the x and y directions within the texture
+ /// pixels.
+ offset: Vec2<u32>,
+
+ face: CubeMapFace,
+ },
+}
+
+/// Rendering command FIFO queue.
+#[derive(Debug, Sole)]
+pub struct CommandQueue
+{
+ queue: VecDeque<Command>,
+}
+
+impl CommandQueue
+{
+ pub fn push(&mut self, command: Command)
+ {
+ self.queue.push_back(command);
+ }
+
+ pub fn drain(&mut self) -> impl Iterator<Item = Command> + use<'_>
+ {
+ self.queue.drain(..)
+ }
+}
+
+impl Default for CommandQueue
+{
+ fn default() -> Self
+ {
+ CommandQueue { queue: VecDeque::with_capacity(100) }
+ }
+}
+
+#[tracing::instrument(skip_all)]
+fn enqueue_commands_from_render_passes(
+ window_surface_query: Query<(&mut Surface, &Window, With<TargetWindow>)>,
+ mut command_queue: Single<CommandQueue>,
+ mut render_passes: Single<RenderPasses>,
+ mut active_draw_props: Local<ActiveDrawProperties>,
+)
+{
+ let Ok(command_queue) = command_queue.get_mut() else {
+ unreachable!();
+ };
+
+ let Ok(render_passes) = render_passes.get_mut() else {
+ unreachable!();
+ };
+
+ let mut last_render_pass_draw_props = active_draw_props.draw_properties.clone();
+
+ for (mut window_surface, window) in &window_surface_query {
+ if window_surface.size != window.inner_size {
+ command_queue.push(Command::SetSurfaceSize(
+ window_surface.id,
+ window.inner_size,
+ ));
+
+ window_surface.size = window.inner_size.clone();
+ }
+ }
+
+ for render_pass in render_passes.passes.drain(..) {
+ if render_pass.draw_properties != last_render_pass_draw_props {
+ command_queue.push(Command::UpdateDrawProperties(
+ render_pass.draw_properties.clone(),
+ DrawPropertiesUpdateFlags::all(),
+ ));
+
+ last_render_pass_draw_props = render_pass.draw_properties;
+ }
+
+ let last_updated_draw_props = render_pass
+ .commands
+ .iter()
+ .filter_map(|command| match command {
+ Command::UpdateDrawProperties(draw_props, _) => Some(draw_props.clone()),
+ _ => None,
+ })
+ .last();
+
+ command_queue.queue.extend(render_pass.commands);
+
+ if let Some(last_updated_draw_props) = last_updated_draw_props {
+ last_render_pass_draw_props = last_updated_draw_props;
+ }
+ }
+
+ active_draw_props.draw_properties = last_render_pass_draw_props;
+
+ for (window_surface, _) in &window_surface_query {
+ command_queue.push(Command::SwapBuffers(window_surface.id));
+ }
+}
+
+#[tracing::instrument(skip_all)]
+fn handle_window_removed(
+ observe: Observe<Pair<Removed, Window>>,
+ mut command_queue: Single<CommandQueue>,
+ mut actions: Actions,
+)
+{
+ let Ok(command_queue) = command_queue.get_mut() else {
+ unreachable!();
+ };
+
+ for evt_match in &observe {
+ let window_ent_id = evt_match.entity_id();
+
+ let window_ent = evt_match.get_entity();
+
+ if window_ent.get::<TargetWindow>().is_none() {
+ continue;
+ }
+
+ tracing::debug!(
+ entity_id = %window_ent_id,
+ title = %evt_match.get_ent_target_comp().title,
+ "Handling removal of window"
+ );
+
+ let Some(window_surface) = window_ent.get::<Surface>() else {
+ continue;
+ };
+
+ actions.remove_comps::<(Surface,)>(window_ent_id);
+
+ command_queue
+ .queue
+ .push_front(Command::RemoveSurface(window_surface.id));
+ }
+}
+
+#[derive(Debug, Default, Clone, Component)]
+struct ActiveDrawProperties
+{
+ pub draw_properties: DrawProperties,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+#[non_exhaustive]
+pub enum RgbTextureDataType
+{
+ UnsignedByte,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+#[non_exhaustive]
+pub enum RgbaTextureDataType
+{
+ UnsignedByte,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+#[non_exhaustive]
+pub enum SrgbTextureDataType
+{
+ UnsignedByte,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+#[non_exhaustive]
+pub enum SrgbaTextureDataType
+{
+ UnsignedByte,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+#[non_exhaustive]
+pub enum DepthTextureDataType
+{
+ Float32,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+#[non_exhaustive]
+pub enum TexturePixelDataFormat
+{
+ Rgb(RgbTextureDataType),
+ Rgba(RgbaTextureDataType),
+ Srgb(SrgbTextureDataType),
+ Srgba(SrgbaTextureDataType),
+ Depth(DepthTextureDataType),
+}
+
+impl TexturePixelDataFormat
+{
+ pub fn for_image(image: &Image) -> Option<Self>
+ {
+ let is_srgb = image.color_space_is_srgb();
+
+ match image.color_type() {
+ ImageColorType::Rgb8 if is_srgb => Some(TexturePixelDataFormat::Srgb(
+ SrgbTextureDataType::UnsignedByte,
+ )),
+ ImageColorType::Rgb8 => Some(TexturePixelDataFormat::Rgb(
+ RgbTextureDataType::UnsignedByte,
+ )),
+ ImageColorType::Rgba8 if is_srgb => Some(TexturePixelDataFormat::Srgba(
+ SrgbaTextureDataType::UnsignedByte,
+ )),
+ ImageColorType::Rgba8 => Some(TexturePixelDataFormat::Rgba(
+ RgbaTextureDataType::UnsignedByte,
+ )),
+ _ => None,
+ }
+ }
+}
+
+/// Index of a color texture attached to a framebuffer.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub struct FramebufferColorTextureIndex(pub u8);
+
+#[derive(Debug, Default, Clone)]
+#[non_exhaustive]
+pub struct FramebufferProperties
+{
+ pub depth_texture: Option<FramebufferTexture>,
+ pub color_draw_buffer: Option<FramebufferColorTextureIndex>,
+}
+
+bitflags! {
+ #[derive(Debug, Default, Clone)]
+ pub struct FramebufferPropertiesUpdateFlags: u8
+ {
+ const DEPTH_TEXTURE = 1 << 0;
+ const COLOR_DRAW_BUFFER = 1 << 1;
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct FramebufferTexture
+{
+ pub object_id: ObjectId,
+ pub mipmap_level: u16,
+}
diff --git a/engine/src/rendering/backend.rs b/engine/src/rendering/backend.rs
new file mode 100644
index 0000000..d69b338
--- /dev/null
+++ b/engine/src/rendering/backend.rs
@@ -0,0 +1,9 @@
+use ecs::extension::Extension;
+
+pub mod opengl;
+
+/// Returns the default rendering backend.
+pub fn get_default() -> impl Extension
+{
+ self::opengl::Extension::default()
+}
diff --git a/engine/src/rendering/backend/opengl.rs b/engine/src/rendering/backend/opengl.rs
new file mode 100644
index 0000000..5b009ca
--- /dev/null
+++ b/engine/src/rendering/backend/opengl.rs
@@ -0,0 +1,2060 @@
+//! OpenGL rendering backend.
+
+use std::borrow::Cow;
+use std::collections::HashMap;
+use std::convert::Infallible;
+use std::hint::cold_path;
+use std::num::NonZero;
+
+use ecs::query::term::With;
+use glutin::config::Config as GlutinConfig;
+use glutin::display::GetGlDisplay;
+use glutin::error::Error as GlutinError;
+use glutin::prelude::{GlDisplay, PossiblyCurrentGlContext};
+use glutin::surface::{
+ GlSurface as _,
+ Surface as GlutinSurface,
+ WindowSurface as GlutinWindowSurface,
+};
+use intmap::IntMap;
+use opengl_bindings::blending::{
+ configure as gl_blending_configure,
+ Configuration as GlBlendingConfig,
+ Equation as GlBlendingEquation,
+ Factor as GlBlendingFactor,
+};
+use opengl_bindings::buffer::{
+ BindingTarget as GlBufferBindingTarget,
+ Buffer as GlBuffer,
+};
+use opengl_bindings::debug::{
+ set_debug_message_callback,
+ set_debug_message_control,
+ MessageIdsAction,
+ MessageSeverity,
+ MessageSource,
+ MessageType,
+ SetDebugMessageControlError as GlSetDebugMessageControlError,
+};
+use opengl_bindings::framebuffer::{
+ bind as gl_bind_framebuffer,
+ Attachment as GlFramebufferAttachment,
+ ColorAttachment as GlFramebufferColorAttachment,
+ Framebuffer as GlFramebuffer,
+ Target as GlFramebufferTarget,
+};
+use opengl_bindings::misc::{
+ clear_buffers,
+ define_scissor_box as gl_define_scissor_box,
+ enable,
+ get_viewport as gl_get_viewport,
+ set_depth_function as gl_set_depth_function,
+ set_enabled,
+ set_face_culling_mode as gl_set_face_culling_mode,
+ set_viewport as gl_set_viewport,
+ BufferClearMask as GlBufferClearMask,
+ Capability,
+ DepthFunction as GlDepthFunction,
+ FaceCullingMode as GlFaceCullingMode,
+ PolygonMode as GlPolygonMode,
+ PolygonModeFace as GlPolygonModeFace,
+};
+use opengl_bindings::shader::{
+ Error as GlShaderError,
+ Kind as ShaderKind,
+ Program as GlShaderProgram,
+ Shader as GlShader,
+};
+use opengl_bindings::texture::{
+ CubeMapFace as GlCubeMapTextureFace,
+ Error as GlTextureError,
+ Filtering as GlTextureFiltering,
+ PixelDataFormat as GlTexturePixelDataFormat,
+ Texture as GlTexture,
+ Wrapping as GlTextureWrapping,
+};
+use opengl_bindings::vertex_array::{
+ DrawError as GlDrawError,
+ PrimitiveKind,
+ VertexArray,
+};
+use opengl_bindings::{
+ MakeContextCurrentError as GlMakeContextCurrentError,
+ MaybeCurrentContextWithFns,
+};
+use raw_window_handle::WindowHandle;
+use safer_ffi::layout::ReprC;
+use zerocopy::IntoBytes;
+
+use crate::asset::Assets;
+use crate::data_types::color::Color;
+use crate::data_types::dimens::Dimens;
+use crate::draw_flags::{PolygonMode, PolygonModeFace};
+use crate::ecs::actions::Actions;
+use crate::ecs::query::term::Without;
+use crate::ecs::sole::Single;
+use crate::ecs::{Component, Query, Sole};
+use crate::reflection::EnumReflectionExt;
+use crate::rendering::backend::opengl::glutin_compat::{
+ DisplayBuilder,
+ Error as GlutinCompatError,
+};
+use crate::rendering::backend::opengl::graphics_mesh::GraphicsMesh;
+use crate::rendering::blending::{
+ Equation as BlendingEquation,
+ Factor as BlendingFactor,
+};
+use crate::rendering::object::{
+ Id as ObjectId,
+ Kind as ObjectKind,
+ Object,
+ RawValue as ObjectRawValue,
+ Store as ObjectStore,
+};
+use crate::rendering::shader::cursor::{
+ Binding as ShaderBinding,
+ BindingLocation as ShaderBindingLocation,
+ BindingValue as ShaderBindingValue,
+};
+use crate::rendering::shader::{
+ Error as ShaderError,
+ Program as ShaderProgram,
+ ProgramMetadata as ShaderProgramMetadata,
+ Stage as ShaderStage,
+};
+use crate::rendering::{
+ AssetOrValue,
+ BufferClearMask,
+ Command,
+ CommandQueue,
+ DepthFunction,
+ DrawMeshOptions,
+ DrawProperties,
+ DrawPropertiesUpdateFlags,
+ FaceCullingMode,
+ FramebufferProperties,
+ FramebufferPropertiesUpdateFlags,
+ GraphicsProperties,
+ Surface,
+ SurfaceId,
+ TargetWindow,
+ TextureCreation,
+ TexturePixelDataFormat,
+ TextureUpdate,
+ POST_RENDER_PHASE,
+ RENDER_PHASE,
+};
+use crate::texture::{
+ Border as TextureBorder,
+ CubeMapFace as CubeMapTextureFace,
+ Filtering as TextureFiltering,
+ Properties as TextureProperties,
+ Wrapping as TextureWrapping,
+};
+use crate::util::OptionExt;
+use crate::vector::{Vec2, Vec3};
+use crate::windowing::dpi::PhysicalSize;
+use crate::windowing::window::{
+ Closed as WindowClosed,
+ CreationAttributes as WindowCreationAttributes,
+ CreationReady,
+ Window,
+};
+use crate::windowing::Context as WindowingContext;
+
+mod glutin_compat;
+mod graphics_mesh;
+
+#[derive(Debug, Component)]
+struct WindowGlConfig
+{
+ gl_config: GlutinConfig,
+}
+
+#[derive(Sole, Default)]
+struct GraphicsContext
+{
+ gl_context: Option<MaybeCurrentContextWithFns>,
+ surfaces: HashMap<SurfaceId, GraphicsContextSurface>,
+ backend_resources: BackendResourceStore,
+}
+
+#[derive(Debug, Default)]
+struct BackendResourceStore
+{
+ inner: IntMap<BackendResourceId, BackendResource>,
+ next_id: BackendResourceId,
+}
+
+impl BackendResourceStore
+{
+ fn try_create_resource<Err>(
+ &mut self,
+ object_store: &mut ObjectStore,
+ object_id: ObjectId,
+ resource_fn: impl FnOnce() -> Result<BackendResource, Err>,
+ ) -> Result<(), Err>
+ {
+ if object_store.contains_non_pending_with_id(&object_id) {
+ cold_path();
+ tracing::error!(
+ ?object_id,
+ "Object store already contains a object with this ID"
+ );
+ return Ok(());
+ }
+
+ let resource = resource_fn()?;
+
+ let res_id = self.next_id;
+
+ self.next_id.0 += 1;
+
+ let object_kind = resource.object_kind();
+
+ self.inner.insert(res_id, resource);
+
+ object_store.insert(object_id, Object::from_raw(res_id.0, object_kind));
+
+ Ok(())
+ }
+
+ fn get_resource(
+ &self,
+ object_store: &ObjectStore,
+ object_id: ObjectId,
+ object_kind: ObjectKind,
+ ) -> Option<&BackendResource>
+ {
+ let Some(object) = object_store.get_obj(&object_id) else {
+ cold_path();
+ tracing::error!(
+ ?object_id,
+ %object_kind,
+ "Object does not exist in the rendering object store"
+ );
+ return None;
+ };
+
+ if object.kind() != object_kind {
+ cold_path();
+ tracing::error!(
+ ?object_id,
+ expected_object_kind = %object_kind,
+ found_object_kind = %object.kind(),
+ "Unexpected object kind of object in object store"
+ );
+ return None;
+ }
+
+ let resource_id = BackendResourceId(object.as_raw());
+
+ let Some(resource) = self.inner.get(resource_id) else {
+ cold_path();
+ tracing::error!(
+ ?object_id,
+ %object_kind,
+ ?resource_id,
+ "Backend resource does not exist"
+ );
+ return None;
+ };
+
+ debug_assert_eq!(resource.object_kind(), object_kind);
+
+ Some(resource)
+ }
+
+ fn get_resource_mut(
+ &mut self,
+ object_store: &ObjectStore,
+ object_id: ObjectId,
+ object_kind: ObjectKind,
+ ) -> Option<&mut BackendResource>
+ {
+ let Some(object) = object_store.get_obj(&object_id) else {
+ cold_path();
+ tracing::error!(
+ ?object_id,
+ %object_kind,
+ "Object does not exist in the rendering object store"
+ );
+ return None;
+ };
+
+ if object.kind() != object_kind {
+ cold_path();
+ tracing::error!(
+ ?object_id,
+ expected_object_kind = %object_kind,
+ found_object_kind = %object.kind(),
+ "Unexpected object kind of object in object store"
+ );
+ return None;
+ }
+
+ let resource_id = BackendResourceId(object.as_raw());
+
+ let Some(resource) = self.inner.get_mut(resource_id) else {
+ cold_path();
+ tracing::error!(
+ ?object_id,
+ %object_kind,
+ ?resource_id,
+ "Backend resource does not exist"
+ );
+ return None;
+ };
+
+ debug_assert_eq!(resource.object_kind(), object_kind);
+
+ Some(resource)
+ }
+
+ fn remove_resource(
+ &mut self,
+ object_store: &mut ObjectStore,
+ object_id: ObjectId,
+ object_kind: ObjectKind,
+ curr_gl_ctx: &MaybeCurrentContextWithFns,
+ )
+ {
+ if let Some(object) = object_store.get_obj(&object_id) {
+ if object.kind() != object_kind {
+ cold_path();
+ tracing::error!(
+ ?object_id,
+ expected_object_kind = %object_kind,
+ found_object_kind = %object.kind(),
+ "Unexpected object kind"
+ );
+ return;
+ }
+ }
+
+ let Some(object) = object_store.remove(&object_id).flatten() else {
+ tracing::error!(
+ ?object_id,
+ %object_kind,
+ "Object does not exist in the rendering object store"
+ );
+ return;
+ };
+
+ let resource_id = BackendResourceId(object.as_raw());
+
+ let Some(resource) = self.inner.remove(resource_id) else {
+ cold_path();
+ tracing::error!(
+ ?object_id,
+ %object_kind,
+ ?resource_id,
+ "Backend resource does not exist"
+ );
+ return;
+ };
+
+ resource.destroy(curr_gl_ctx);
+ }
+}
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+struct BackendResourceId(ObjectRawValue);
+
+impl intmap::IntKey for BackendResourceId
+{
+ type Int = ObjectRawValue;
+
+ const PRIME: Self::Int = <ObjectRawValue as intmap::IntKey>::PRIME;
+
+ fn into_int(self) -> Self::Int
+ {
+ self.0.into_int()
+ }
+}
+
+#[derive(Debug)]
+struct GraphicsContextSurface
+{
+ window_surface: GlutinSurface<GlutinWindowSurface>,
+ size: PhysicalSize<u32>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+struct ShaderBindingIndex(u32);
+
+impl intmap::IntKey for ShaderBindingIndex
+{
+ type Int = u32;
+
+ const PRIME: Self::Int = <u32 as intmap::IntKey>::PRIME;
+
+ fn into_int(self) -> Self::Int
+ {
+ self.0
+ }
+}
+
+#[derive(Debug)]
+enum BackendShaderBinding
+{
+ Uniform(GlBuffer<u8>),
+ Texture(GlTexture),
+}
+
+impl BackendShaderBinding
+{
+ fn bind(
+ &self,
+ gl_context: &MaybeCurrentContextWithFns,
+ binding_index: ShaderBindingIndex,
+ )
+ {
+ match self {
+ BackendShaderBinding::Uniform(uniform_buf) => {
+ uniform_buf.bind_to_indexed_target(
+ gl_context,
+ GlBufferBindingTarget::UniformBuffer,
+ binding_index.0,
+ );
+ }
+ BackendShaderBinding::Texture(texture) => {
+ texture.bind_to_texture_unit(gl_context, binding_index.0)
+ }
+ }
+ }
+}
+
+#[derive(Debug)]
+enum BackendResource
+{
+ Mesh
+ {
+ mesh: GraphicsMesh,
+ vertex_attrs_updated_for_shader: Option<ObjectId>,
+ },
+ Shader
+ {
+ program: GlShaderProgram,
+ program_metadata: ShaderProgramMetadata,
+ bindings: IntMap<ShaderBindingIndex, BackendShaderBinding>,
+ },
+ Texture
+ {
+ texture: GlTexture,
+ pixel_data_format: TexturePixelDataFormat,
+ kind: TextureKind,
+ },
+ Framebuffer
+ {
+ framebuffer: GlFramebuffer
+ },
+}
+
+impl BackendResource
+{
+ fn object_kind(&self) -> ObjectKind
+ {
+ match self {
+ Self::Mesh { .. } => ObjectKind::Mesh,
+ Self::Shader { .. } => ObjectKind::ShaderProgram,
+ Self::Texture { .. } => ObjectKind::Texture,
+ Self::Framebuffer { .. } => ObjectKind::Framebuffer,
+ }
+ }
+
+ fn destroy(&self, curr_gl_ctx: &MaybeCurrentContextWithFns)
+ {
+ match self {
+ Self::Mesh { mesh, .. } => {
+ mesh.destroy(curr_gl_ctx);
+ }
+ Self::Shader { program, .. } => {
+ program.clone().delete(curr_gl_ctx);
+ }
+ Self::Texture { texture, .. } => {
+ texture.clone().delete(curr_gl_ctx);
+ }
+ Self::Framebuffer { framebuffer } => {
+ framebuffer.clone().delete(curr_gl_ctx);
+ }
+ }
+ }
+}
+
+#[derive(Debug)]
+enum TextureKind
+{
+ Texture2D,
+ CubeMap,
+}
+
+#[derive(Debug, Default)]
+#[non_exhaustive]
+pub struct Extension {}
+
+impl crate::ecs::extension::Extension for Extension
+{
+ fn collect(self, mut collector: crate::ecs::extension::Collector<'_>)
+ {
+ collector.add_system(*RENDER_PHASE, handle_commands);
+
+ collector.add_system(*POST_RENDER_PHASE, prepare_windows);
+ collector.add_system(*POST_RENDER_PHASE, init_window_graphics);
+
+ let _ = collector.add_sole(GraphicsContext::default());
+ }
+}
+
+fn prepare_windows(
+ window_query: Query<(
+ Option<&Window>,
+ &mut WindowCreationAttributes,
+ With<TargetWindow>,
+ Without<CreationReady>,
+ Without<WindowGlConfig>,
+ Without<WindowClosed>,
+ )>,
+ windowing_context: Single<WindowingContext>,
+ graphics_props: Single<GraphicsProperties>,
+ mut actions: Actions,
+) -> Result<(), crate::Error>
+{
+ let windowing_context = windowing_context.get()?;
+ let graphics_props = graphics_props.get()?;
+
+ let Some(display_handle) = windowing_context.display_handle() else {
+ return Ok(());
+ };
+
+ for (window_ent_id, (window, mut window_creation_attrs)) in
+ window_query.iter_with_euids()
+ {
+ tracing::debug!("Preparing window entity {window_ent_id} for use in rendering");
+
+ let mut glutin_config_template_builder =
+ glutin::config::ConfigTemplateBuilder::new();
+
+ if let Some(multisampling_sample_cnt) = graphics_props.multisampling_sample_cnt {
+ glutin_config_template_builder = glutin_config_template_builder
+ .with_multisampling(multisampling_sample_cnt);
+ }
+
+ let window_handle = match window
+ .as_ref()
+ .map(|window| unsafe {
+ windowing_context.get_window_as_handle(&window.wid())
+ })
+ .flatten()
+ .transpose()
+ {
+ Ok(window_handle) => window_handle,
+ Err(err) => {
+ tracing::error!("Failed to get window handle: {err}");
+ continue;
+ }
+ };
+
+ let (new_window_creation_attrs, gl_config) = match DisplayBuilder::new()
+ .with_window_attributes(window_creation_attrs.clone())
+ .build(
+ window_handle,
+ &display_handle,
+ glutin_config_template_builder,
+ |mut cfgs| cfgs.next(),
+ ) {
+ Ok((new_window_creation_attrs, gl_config)) => {
+ (new_window_creation_attrs, gl_config)
+ }
+ Err(GlutinCompatError::WindowRequired) => {
+ actions.add_components(window_ent_id, (CreationReady,));
+ continue;
+ }
+ Err(err) => {
+ tracing::error!("Failed to create platform graphics display: {err}");
+ continue;
+ }
+ };
+
+ *window_creation_attrs = new_window_creation_attrs;
+
+ actions.add_components(window_ent_id, (WindowGlConfig { gl_config },));
+
+ if window.is_none() {
+ actions.add_components(window_ent_id, (CreationReady,));
+ }
+ }
+
+ Ok(())
+}
+
+#[tracing::instrument(skip_all)]
+fn init_window_graphics(
+ window_query: Query<(
+ &Window,
+ &WindowGlConfig,
+ With<TargetWindow>,
+ Without<Surface>,
+ )>,
+ windowing_context: Single<WindowingContext>,
+ graphics_props: Single<GraphicsProperties>,
+ mut graphics_ctx: Single<GraphicsContext>,
+ mut actions: Actions,
+) -> Result<(), crate::Error>
+{
+ let Ok(graphics_ctx) = graphics_ctx.get_mut() else {
+ unreachable!();
+ };
+
+ let windowing_context = windowing_context.get()?;
+ let graphics_props = graphics_props.get()?;
+
+ for (window_ent_id, (window, window_gl_config)) in window_query.iter_with_euids() {
+ tracing::info!(
+ window_entity_id=%window_ent_id,
+ window_title=&*window.title,
+ "Initializing graphics for window"
+ );
+
+ let display = window_gl_config.gl_config.display();
+
+ let window_handle =
+ match unsafe { windowing_context.get_window_as_handle(&window.wid()) }
+ .transpose()
+ {
+ Ok(Some(window_handle)) => window_handle,
+ Ok(None) => {
+ tracing::error!(
+ wid = ?window.wid(),
+ entity_id = %window_ent_id,
+ "Windowing context does not contain window"
+ );
+ continue;
+ }
+ Err(err) => {
+ tracing::error!("Failed to get window handle: {err}");
+ continue;
+ }
+ };
+
+ let Ok(window_inner_size) =
+ PhysicalSize::<NonZero<u32>>::try_convert_from(window.inner_size.clone())
+ else {
+ tracing::error!(
+ "Cannot create a surface for a window with a width/height of 0",
+ );
+ continue;
+ };
+
+ let window_surface = match unsafe {
+ display.create_window_surface(
+ &window_gl_config.gl_config,
+ &glutin::surface::SurfaceAttributesBuilder::<
+ glutin::surface::WindowSurface,
+ >::new()
+ .build(
+ window_handle.as_raw(),
+ window_inner_size.width,
+ window_inner_size.height,
+ ),
+ )
+ } {
+ Ok(window_surface) => window_surface,
+ Err(err) => {
+ tracing::error!("Failed to create window surface: {err}");
+ continue;
+ }
+ };
+
+ let gl_context = match graphics_ctx.gl_context.get_or_try_insert_with_fn(|| {
+ create_gl_context(
+ &window_gl_config.gl_config,
+ &graphics_props,
+ window_handle,
+ &window_surface,
+ )
+ }) {
+ Ok(gl_context) => gl_context,
+ Err(err) => {
+ tracing::error!("Failed to create GL context: {err}");
+ continue;
+ }
+ };
+
+ if let Err(err) = gl_context.make_current(&window_surface) {
+ tracing::error!("Failed to make GL context current: {err}");
+ continue;
+ };
+
+ if let Err(err) = gl_set_viewport(
+ &gl_context,
+ &Vec2 { x: 0, y: 0 }.into(),
+ &opengl_bindings::data_types::Dimens {
+ width: window.inner_size.width,
+ height: window.inner_size.height,
+ },
+ ) {
+ tracing::error!("Failed to set viewport: {err}");
+ }
+
+ set_enabled(
+ &gl_context,
+ Capability::DepthTest,
+ graphics_props.depth_test,
+ );
+
+ set_enabled(
+ &gl_context,
+ Capability::MultiSample,
+ graphics_props.multisampling_sample_cnt.is_some(),
+ );
+
+ if graphics_props.debug {
+ enable(&gl_context, Capability::DebugOutput);
+ enable(&gl_context, Capability::DebugOutputSynchronous);
+
+ set_debug_message_callback(&gl_context, opengl_debug_message_cb);
+
+ match set_debug_message_control(
+ &gl_context,
+ None,
+ None,
+ None,
+ &[],
+ MessageIdsAction::Disable,
+ ) {
+ Ok(()) => {}
+ Err(GlSetDebugMessageControlError::TooManyIds {
+ id_cnt: _,
+ max_id_cnt: _,
+ }) => {
+ unreachable!() // No ids are given
+ }
+ }
+ }
+
+ let surface_id = SurfaceId::new_unique();
+
+ actions.add_components(
+ window_ent_id,
+ (Surface {
+ id: surface_id,
+ size: window.inner_size.clone(),
+ },),
+ );
+
+ graphics_ctx.surfaces.insert(
+ surface_id,
+ GraphicsContextSurface {
+ window_surface,
+ size: window.inner_size.clone(),
+ },
+ );
+ }
+
+ Ok(())
+}
+
+#[tracing::instrument(skip_all)]
+fn handle_commands(
+ mut graphics_ctx: Single<GraphicsContext>,
+ mut object_store: Single<ObjectStore>,
+ mut command_queue: Single<CommandQueue>,
+ assets: Single<Assets>,
+) -> Result<(), crate::Error>
+{
+ let Ok(graphics_ctx) = graphics_ctx.get_mut() else {
+ unreachable!();
+ };
+
+ let object_store = object_store.get_mut()?;
+ let command_queue = command_queue.get_mut()?;
+ let assets = assets.get()?;
+
+ let GraphicsContext {
+ ref gl_context,
+ ref mut surfaces,
+ ref mut backend_resources,
+ } = *graphics_ctx;
+
+ let Some(gl_context) = gl_context else {
+ return Ok(());
+ };
+
+ let mut activated_gl_shader_program: Option<(
+ ObjectId,
+ GlShaderProgram,
+ ShaderProgramMetadata,
+ )> = None;
+
+ for command in command_queue.drain() {
+ let tracing_span = tracing::info_span!(
+ "handle_cmd",
+ command = %command.get_variant_reflection().name,
+ );
+ let _tracing_span_enter = tracing_span.enter();
+
+ match command {
+ Command::RemoveSurface(surface_id) => {
+ let Some(surface) = surfaces.remove(&surface_id) else {
+ tracing::error!(surface_id=?surface_id, "Surface does not exist");
+ continue;
+ };
+
+ if surface.window_surface.is_current(gl_context.context()) {
+ if let Err(err) = gl_context.context().make_not_current_in_place() {
+ tracing::error!("Failed to make GL context not current: {err}");
+ }
+
+ if let Err(err) = gl_context.make_current_surfaceless() {
+ tracing::error!("Failed to make GL context current: {err}");
+ }
+ }
+
+ drop(surface);
+ }
+ Command::MakeCurrent(surface_id) => {
+ let Some(surface) = surfaces.get(&surface_id) else {
+ tracing::error!(surface_id=?surface_id, "Surface does not exist");
+ continue;
+ };
+
+ if let Err(err) = gl_context.make_current(&surface.window_surface) {
+ tracing::error!("Failed to make graphics context current: {err}");
+ continue;
+ }
+
+ if let Err(err) = gl_set_viewport(
+ gl_context,
+ &Vec2 { x: 0, y: 0 }.into(),
+ &opengl_bindings::data_types::Dimens {
+ width: surface.size.width,
+ height: surface.size.height,
+ },
+ ) {
+ tracing::error!("Failed to set viewport: {err}");
+ }
+ }
+ Command::SetSurfaceSize(surface_id, new_surface_size) => {
+ let Some(surface) = surfaces.get_mut(&surface_id) else {
+ tracing::error!(surface_id=?surface_id, "Surface does not exist");
+ continue;
+ };
+
+ surface.size = new_surface_size;
+
+ if !surface.window_surface.is_current(gl_context.context()) {
+ continue;
+ }
+
+ if let Err(err) = gl_set_viewport(
+ gl_context,
+ &Vec2 { x: 0, y: 0 }.into(),
+ &opengl_bindings::data_types::Dimens {
+ width: surface.size.width,
+ height: surface.size.height,
+ },
+ ) {
+ tracing::error!("Failed to set viewport: {err}");
+ }
+ }
+ Command::ClearBuffers(buffer_clear_mask) => {
+ let mut clear_mask = GlBufferClearMask::empty();
+
+ clear_mask.set(
+ GlBufferClearMask::COLOR,
+ buffer_clear_mask.contains(BufferClearMask::COLOR),
+ );
+
+ clear_mask.set(
+ GlBufferClearMask::DEPTH,
+ buffer_clear_mask.contains(BufferClearMask::DEPTH),
+ );
+
+ clear_mask.set(
+ GlBufferClearMask::STENCIL,
+ buffer_clear_mask.contains(BufferClearMask::STENCIL),
+ );
+
+ clear_buffers(gl_context, clear_mask);
+ }
+ Command::SwapBuffers(surface_id) => {
+ let Some(surface) = surfaces.get(&surface_id) else {
+ tracing::error!(surface_id=?surface_id, "Surface does not exist");
+ continue;
+ };
+
+ if let Err(err) =
+ surface.window_surface.swap_buffers(gl_context.context())
+ {
+ tracing::error!("Failed to swap buffers: {err}");
+ }
+ }
+ Command::CreateShaderProgram(object_id, shader_program) => {
+ if let Err(err) = backend_resources
+ .try_create_resource::<CreateShaderError>(
+ object_store,
+ object_id,
+ || {
+ Ok(BackendResource::Shader {
+ program: create_shader_program(
+ gl_context,
+ &shader_program,
+ )?,
+ program_metadata: shader_program.metadata().clone(),
+ bindings: IntMap::with_capacity(4),
+ })
+ },
+ )
+ {
+ tracing::error!("Failed to create shader program: {err}");
+ }
+ }
+ Command::ActivateShader(shader_object_id) => {
+ let Some(resource) = backend_resources.get_resource(
+ object_store,
+ shader_object_id,
+ ObjectKind::ShaderProgram,
+ ) else {
+ continue;
+ };
+
+ let BackendResource::Shader { program, program_metadata, bindings } =
+ resource
+ else {
+ unreachable!();
+ };
+
+ program.activate(gl_context);
+
+ for (binding_index, binding) in bindings.iter() {
+ binding.bind(gl_context, binding_index);
+ }
+
+ activated_gl_shader_program =
+ Some((shader_object_id, program.clone(), program_metadata.clone()));
+ }
+ Command::SetShaderBinding(
+ shader_object_id,
+ ShaderBinding {
+ location: binding_location,
+ value: binding_value,
+ },
+ ) => {
+ let binding = set_shader_binding(
+ backend_resources,
+ object_store,
+ gl_context,
+ shader_object_id.clone(),
+ &binding_location,
+ binding_value,
+ );
+
+ if let Some(binding) = binding {
+ if Some(shader_object_id)
+ == activated_gl_shader_program
+ .as_ref()
+ .map(|(obj_id, ..)| *obj_id)
+ {
+ binding.bind(
+ gl_context,
+ ShaderBindingIndex(binding_location.binding_index),
+ );
+ }
+ }
+ }
+ Command::CreateTexture {
+ obj_id,
+ pixel_data_format,
+ creation,
+ properties,
+ } => {
+ let kind = match &creation {
+ TextureCreation::Texture2D { .. } => TextureKind::Texture2D,
+ TextureCreation::CubeMap { .. } => TextureKind::CubeMap,
+ };
+
+ if let Err(err) = backend_resources
+ .try_create_resource::<opengl_bindings::texture::Error>(
+ object_store,
+ obj_id,
+ || {
+ let gl_texture = create_gl_texture(
+ gl_context,
+ creation,
+ pixel_data_format,
+ &properties,
+ )?;
+
+ Ok(BackendResource::Texture {
+ texture: gl_texture,
+ pixel_data_format,
+ kind,
+ })
+ },
+ )
+ {
+ tracing::error!("Failed to create texture: {err}");
+ }
+ }
+ Command::UpdateTexture {
+ obj_id,
+ pixels,
+ pixel_data_format,
+ update,
+ } => {
+ let Some(texture_resource) = backend_resources.get_resource(
+ object_store,
+ obj_id,
+ ObjectKind::Texture,
+ ) else {
+ continue;
+ };
+
+ let BackendResource::Texture {
+ texture: gl_texture,
+ pixel_data_format: tex_pixel_data_format,
+ kind: tex_metadata,
+ } = texture_resource
+ else {
+ unreachable!();
+ };
+
+ if !matches!(
+ (&update, tex_metadata),
+ (TextureUpdate::Texture2D { .. }, TextureKind::Texture2D)
+ | (TextureUpdate::CubeMap { .. }, TextureKind::CubeMap)
+ ) {
+ tracing::error!(
+ texture_object_id = ?obj_id,
+ "Texture is of incorrect kind"
+ );
+ continue;
+ }
+
+ if &pixel_data_format != tex_pixel_data_format {
+ cold_path();
+ tracing::error!(
+ texture_object_id = ?obj_id,
+ expected_pixel_data_format = ?*tex_pixel_data_format,
+ found_pixel_data_format = ?pixel_data_format,
+ "Texture has incorrect pixel data format"
+ );
+ continue;
+ }
+
+ if let Err(err) = update_texture_object(
+ gl_context,
+ gl_texture,
+ pixels,
+ pixel_data_format,
+ update,
+ ) {
+ tracing::error!("Failed to update texture object: {err}");
+ }
+ }
+ Command::RemoveTexture(object_id) => {
+ backend_resources.remove_resource(
+ object_store,
+ object_id,
+ ObjectKind::Texture,
+ gl_context,
+ );
+ }
+ Command::CreateFramebuffer(object_id) => {
+ backend_resources.try_create_resource::<Infallible>(
+ object_store,
+ object_id,
+ || {
+ Ok(BackendResource::Framebuffer {
+ framebuffer: GlFramebuffer::new(gl_context),
+ })
+ },
+ );
+ }
+ Command::RemoveFramebuffer(object_id) => {
+ backend_resources.remove_resource(
+ object_store,
+ object_id,
+ ObjectKind::Framebuffer,
+ gl_context,
+ );
+ }
+ Command::UpdateFramebufferProperties {
+ object_id,
+ properties,
+ update_flags,
+ } => {
+ update_framebuffer_properties(
+ backend_resources,
+ object_store,
+ gl_context,
+ object_id,
+ properties,
+ update_flags,
+ );
+ }
+ Command::CreateMesh { obj_id, mesh, usage: mesh_usage } => {
+ let mesh = match &mesh {
+ AssetOrValue::Asset(mesh_asset) => {
+ let Some(mesh) = assets.get(&mesh_asset) else {
+ tracing::error!(
+ asset_id=?mesh_asset.id(),
+ "Mesh asset does not exist"
+ );
+ continue;
+ };
+
+ mesh
+ }
+ AssetOrValue::Value(mesh) => mesh,
+ };
+
+ if let Err(err) = backend_resources
+ .try_create_resource::<graphics_mesh::Error>(
+ object_store,
+ obj_id,
+ || {
+ Ok(BackendResource::Mesh {
+ mesh: GraphicsMesh::new(gl_context, &mesh, mesh_usage)?,
+ vertex_attrs_updated_for_shader: None,
+ })
+ },
+ )
+ {
+ tracing::error!("Failed to create mesh: {err}");
+ }
+ }
+ Command::UpdateMesh { obj_id, mesh, usage: mesh_usage } => {
+ let Some(resource) = backend_resources.get_resource_mut(
+ object_store,
+ obj_id,
+ ObjectKind::Mesh,
+ ) else {
+ continue;
+ };
+
+ let BackendResource::Mesh { mesh: graphics_mesh, .. } = resource else {
+ unreachable!();
+ };
+
+ if let Err(err) = graphics_mesh.update(gl_context, &mesh, mesh_usage) {
+ tracing::error!("Failed to update mesh: {err}");
+ }
+ }
+ Command::RemoveMesh(object_id) => {
+ backend_resources.remove_resource(
+ object_store,
+ object_id,
+ ObjectKind::Mesh,
+ gl_context,
+ );
+ }
+ Command::DrawMesh(object_id, draw_mesh_opts) => {
+ let Some(resource) = backend_resources.get_resource_mut(
+ object_store,
+ object_id,
+ ObjectKind::Mesh,
+ ) else {
+ continue;
+ };
+
+ let BackendResource::Mesh {
+ mesh: graphics_mesh,
+ vertex_attrs_updated_for_shader,
+ } = resource
+ else {
+ unreachable!();
+ };
+
+ let Some((shader_program_obj_id, _, shader_program_metadata)) =
+ &activated_gl_shader_program
+ else {
+ tracing::error!("No shader program is activated");
+ continue;
+ };
+
+ if vertex_attrs_updated_for_shader.is_none_or(
+ |vertex_attrs_updated_for_shader| {
+ vertex_attrs_updated_for_shader != *shader_program_obj_id
+ },
+ ) {
+ let Some(shader_program_vertex_desc) =
+ &shader_program_metadata.vertex_desc
+ else {
+ tracing::error!(
+ "Activated shader program does not have a vertex description"
+ );
+ continue;
+ };
+
+ if let Err(err) = graphics_mesh.update_vertex_attrs_for_shader(
+ gl_context,
+ shader_program_vertex_desc,
+ ) {
+ tracing::error!("Cannot draw mesh: {err}");
+ continue;
+ }
+
+ *vertex_attrs_updated_for_shader = Some(*shader_program_obj_id);
+ }
+
+ if let Err(err) = draw_mesh(gl_context, graphics_mesh, &draw_mesh_opts) {
+ tracing::error!("Failed to draw mesh: {err}");
+ };
+ }
+ Command::UpdateDrawProperties(properties, update_flags) => {
+ update_draw_properties(
+ backend_resources,
+ object_store,
+ gl_context,
+ properties,
+ update_flags,
+ );
+ }
+ }
+ }
+
+ Ok(())
+}
+
+fn create_gl_context(
+ gl_config: &GlutinConfig,
+ graphics_props: &GraphicsProperties,
+ window_handle: WindowHandle<'_>,
+ surface: &GlutinSurface<GlutinWindowSurface>,
+) -> Result<MaybeCurrentContextWithFns, CreateGlContextError>
+{
+ let display = gl_config.display();
+
+ let glutin_context = unsafe {
+ display.create_context(
+ gl_config,
+ &glutin::context::ContextAttributesBuilder::new()
+ .with_debug(graphics_props.debug)
+ .build(Some(window_handle.as_raw())),
+ )
+ }
+ .map_err(CreateGlContextError::CreateGlutinContext)?;
+
+ MaybeCurrentContextWithFns::new(glutin_context, &surface)
+ .map_err(CreateGlContextError::MakeContextCurrent)
+}
+
+#[derive(Debug, thiserror::Error)]
+enum CreateGlContextError
+{
+ #[error("Glutin context creation failed")]
+ CreateGlutinContext(#[source] GlutinError),
+
+ #[error("Making GL context current failed")]
+ MakeContextCurrent(#[source] GlMakeContextCurrentError),
+}
+
+#[tracing::instrument(skip_all)]
+fn update_texture_object(
+ curr_gl_ctx: &MaybeCurrentContextWithFns,
+ gl_texture: &GlTexture,
+ pixels: Box<[u8]>,
+ pixel_data_format: TexturePixelDataFormat,
+ update: TextureUpdate,
+) -> Result<(), GlTextureError>
+{
+ match update {
+ TextureUpdate::Texture2D { size, offset } => {
+ gl_texture.store_image_2d(
+ curr_gl_ctx,
+ 0,
+ offset.into(),
+ size.into(),
+ tex_pixel_data_format_into_gl(pixel_data_format),
+ &pixels,
+ )?;
+ }
+ TextureUpdate::CubeMap { size, offset, face } => {
+ gl_texture.store_image_3d(
+ curr_gl_ctx,
+ 0,
+ Vec3 {
+ x: offset.x,
+ y: offset.y,
+ z: cube_map_texture_face_to_gl(face) as u32,
+ }
+ .into(),
+ [size.width, size.height, 1],
+ tex_pixel_data_format_into_gl(pixel_data_format),
+ &pixels,
+ )?;
+ }
+ }
+
+ gl_texture.generate_mipmap(curr_gl_ctx);
+
+ Ok(())
+}
+
+#[tracing::instrument(skip_all)]
+fn update_draw_properties(
+ backend_resources: &BackendResourceStore,
+ object_store: &ObjectStore,
+ gl_context: &MaybeCurrentContextWithFns,
+ properties: DrawProperties,
+ update_flags: DrawPropertiesUpdateFlags,
+)
+{
+ if update_flags.contains(DrawPropertiesUpdateFlags::POLYGON_MODE_CONFIG) {
+ opengl_bindings::misc::set_polygon_mode(
+ gl_context,
+ polygon_mode_face_to_gl(properties.polygon_mode_config.face),
+ polygon_mode_to_gl(properties.polygon_mode_config.mode),
+ );
+ }
+
+ if update_flags.contains(DrawPropertiesUpdateFlags::BLENDING_ENABLED) {
+ set_enabled(gl_context, Capability::Blend, properties.blending_enabled);
+ }
+
+ if update_flags.contains(DrawPropertiesUpdateFlags::BLENDING_CONFIG) {
+ gl_blending_configure(
+ gl_context,
+ GlBlendingConfig::default()
+ .with_source_factor(blending_factor_to_gl(
+ properties.blending_config.source_factor,
+ ))
+ .with_destination_factor(blending_factor_to_gl(
+ properties.blending_config.destination_factor,
+ ))
+ .with_equation(blending_equation_to_gl(
+ properties.blending_config.equation,
+ )),
+ );
+ }
+
+ if update_flags.contains(DrawPropertiesUpdateFlags::DEPTH_TEST_ENABLED) {
+ set_enabled(
+ gl_context,
+ Capability::DepthTest,
+ properties.depth_test_enabled,
+ );
+ }
+
+ if update_flags.contains(DrawPropertiesUpdateFlags::DEPTH_FUNCTION) {
+ gl_set_depth_function(
+ gl_context,
+ match properties.depth_function {
+ DepthFunction::Never => GlDepthFunction::Never,
+ DepthFunction::Less => GlDepthFunction::Less,
+ DepthFunction::Equal => GlDepthFunction::Equal,
+ DepthFunction::LessOrEqual => GlDepthFunction::LessOrEqual,
+ DepthFunction::Greater => GlDepthFunction::Greater,
+ DepthFunction::NotEqual => GlDepthFunction::NotEqual,
+ DepthFunction::GreaterOrEqual => GlDepthFunction::GreaterOrEqual,
+ DepthFunction::Always => GlDepthFunction::Always,
+ },
+ );
+ }
+
+ if update_flags.contains(DrawPropertiesUpdateFlags::SCISSOR_TEST_ENABLED) {
+ set_enabled(
+ gl_context,
+ Capability::ScissorTest,
+ properties.scissor_test_enabled,
+ );
+ }
+
+ if update_flags.contains(DrawPropertiesUpdateFlags::SCISSOR_BOX) {
+ gl_define_scissor_box(
+ gl_context,
+ properties.scissor_box.lower_left_corner_pos.into(),
+ properties
+ .scissor_box
+ .size
+ .unwrap_or_else(|| {
+ let (_, viewport_size) = gl_get_viewport(gl_context);
+
+ Dimens::<u16> {
+ width: viewport_size
+ .width
+ .try_into()
+ .expect("Viewport width too large"),
+ height: viewport_size
+ .height
+ .try_into()
+ .expect("Viewport height too large"),
+ }
+ })
+ .into(),
+ );
+ }
+
+ if update_flags.contains(DrawPropertiesUpdateFlags::FACE_CULLING_ENABLED) {
+ set_enabled(
+ gl_context,
+ Capability::CullFace,
+ properties.face_culling_enabled,
+ );
+ }
+
+ if update_flags.contains(DrawPropertiesUpdateFlags::FACE_CULLING_MODE) {
+ gl_set_face_culling_mode(
+ gl_context,
+ match properties.face_culling_mode {
+ FaceCullingMode::Front => GlFaceCullingMode::Front,
+ FaceCullingMode::Back => GlFaceCullingMode::Back,
+ FaceCullingMode::FrontAndBack => GlFaceCullingMode::FrontAndBack,
+ },
+ );
+ }
+
+ 'if_draw_fb: {
+ if update_flags.contains(DrawPropertiesUpdateFlags::DRAW_FRAMEBUFFER) {
+ let Some(framebuffer_obj_id) = properties.draw_framebuffer else {
+ gl_bind_framebuffer(gl_context, GlFramebufferTarget::Draw, None);
+ break 'if_draw_fb;
+ };
+
+ let Some(framebuffer_resource) = backend_resources.get_resource(
+ object_store,
+ framebuffer_obj_id,
+ ObjectKind::Framebuffer,
+ ) else {
+ break 'if_draw_fb;
+ };
+
+ let BackendResource::Framebuffer { framebuffer } = framebuffer_resource
+ else {
+ unreachable!();
+ };
+
+ gl_bind_framebuffer(gl_context, GlFramebufferTarget::Draw, Some(framebuffer));
+ }
+ }
+
+ 'if_read_fb: {
+ if update_flags.contains(DrawPropertiesUpdateFlags::READ_FRAMEBUFFER) {
+ let Some(framebuffer_obj_id) = properties.read_framebuffer else {
+ gl_bind_framebuffer(gl_context, GlFramebufferTarget::Read, None);
+ break 'if_read_fb;
+ };
+
+ let Some(framebuffer_resource) = backend_resources.get_resource(
+ object_store,
+ framebuffer_obj_id,
+ ObjectKind::Framebuffer,
+ ) else {
+ break 'if_read_fb;
+ };
+
+ let BackendResource::Framebuffer { framebuffer } = framebuffer_resource
+ else {
+ unreachable!();
+ };
+
+ gl_bind_framebuffer(gl_context, GlFramebufferTarget::Read, Some(framebuffer));
+ }
+ }
+}
+
+#[tracing::instrument(skip_all)]
+fn update_framebuffer_properties(
+ backend_resources: &BackendResourceStore,
+ object_store: &ObjectStore,
+ current_context: &MaybeCurrentContextWithFns,
+ framebuffer_object_id: ObjectId,
+ properties: FramebufferProperties,
+ update_flags: FramebufferPropertiesUpdateFlags,
+)
+{
+ let Some(resource) = backend_resources.get_resource(
+ object_store,
+ framebuffer_object_id,
+ ObjectKind::Framebuffer,
+ ) else {
+ return;
+ };
+
+ let BackendResource::Framebuffer { framebuffer } = resource else {
+ unreachable!();
+ };
+
+ 'if_dt: {
+ if update_flags.contains(FramebufferPropertiesUpdateFlags::DEPTH_TEXTURE) {
+ let Some(framebuffer_depth_texture) = &properties.depth_texture else {
+ framebuffer
+ .detach_texture(current_context, GlFramebufferAttachment::Depth);
+
+ break 'if_dt;
+ };
+
+ let Some(depth_texture_resource) = backend_resources.get_resource(
+ object_store,
+ framebuffer_depth_texture.object_id,
+ ObjectKind::Texture,
+ ) else {
+ break 'if_dt;
+ };
+
+ let BackendResource::Texture { texture: depth_texture, .. } =
+ depth_texture_resource
+ else {
+ unreachable!();
+ };
+
+ framebuffer.attach_texture(
+ current_context,
+ GlFramebufferAttachment::Depth,
+ depth_texture.clone(),
+ framebuffer_depth_texture.mipmap_level,
+ );
+ }
+ }
+
+ if update_flags.contains(FramebufferPropertiesUpdateFlags::COLOR_DRAW_BUFFER) {
+ framebuffer.set_draw_buffer(
+ current_context,
+ properties.color_draw_buffer.map(|color_texture_index| {
+ GlFramebufferColorAttachment(color_texture_index.0)
+ }),
+ );
+ }
+}
+
+fn set_shader_binding<'backend_resources>(
+ backend_resources: &'backend_resources mut BackendResourceStore,
+ object_store: &ObjectStore,
+ gl_context: &MaybeCurrentContextWithFns,
+ shader_object_id: ObjectId,
+ binding_location: &ShaderBindingLocation,
+ binding_value: ShaderBindingValue,
+) -> Option<&'backend_resources BackendShaderBinding>
+{
+ if let ShaderBindingValue::Texture(texture_object_id, _) = &binding_value {
+ let gl_texture = match backend_resources.get_resource(
+ object_store,
+ *texture_object_id,
+ ObjectKind::Texture,
+ ) {
+ Some(BackendResource::Texture { texture, .. }) => texture.clone(),
+ Some(_) => unreachable!(),
+ None => return None,
+ };
+
+ let shader_bindings = match backend_resources.get_resource_mut(
+ object_store,
+ shader_object_id,
+ ObjectKind::ShaderProgram,
+ ) {
+ Some(BackendResource::Shader { bindings, .. }) => bindings,
+ Some(_) => unreachable!(),
+ None => return None,
+ };
+
+ if let Some(prev_binding) =
+ shader_bindings.remove(ShaderBindingIndex(binding_location.binding_index))
+ {
+ // TODO: Textures should probably also be handled somehow here
+ if let BackendShaderBinding::Uniform(prev_binding_uniform_buf) = prev_binding
+ {
+ prev_binding_uniform_buf.delete(gl_context);
+ }
+ }
+
+ return Some(
+ shader_bindings
+ .entry(ShaderBindingIndex(binding_location.binding_index))
+ .set_or_insert_with(|| BackendShaderBinding::Texture(gl_texture.clone())),
+ );
+
+ // if Some(shader_object_id)
+ // == activated_gl_shader_program
+ // .as_ref()
+ // .map(|(obj_id, ..)| *obj_id)
+ // {
+ // BackendShaderBinding::Texture(gl_texture.clone()).bind(
+ // gl_context,
+ // ShaderBindingIndex(binding_location.binding_index),
+ // );
+ // }
+ }
+
+ let shader_bindings = match backend_resources.get_resource_mut(
+ object_store,
+ shader_object_id,
+ ObjectKind::ShaderProgram,
+ ) {
+ Some(BackendResource::Shader { bindings, .. }) => bindings,
+ Some(_) => unreachable!(),
+ None => return None,
+ };
+
+ let binding =
+ match shader_bindings.get(ShaderBindingIndex(binding_location.binding_index)) {
+ Some(binding @ BackendShaderBinding::Uniform(_)) => binding,
+ Some(_) | None => {
+ let uniform_buf = GlBuffer::<u8>::new(gl_context);
+
+ uniform_buf
+ .init(
+ gl_context,
+ binding_location.binding_size,
+ opengl_bindings::buffer::Usage::Dynamic,
+ )
+ .unwrap();
+
+ shader_bindings
+ .entry(ShaderBindingIndex(binding_location.binding_index))
+ .set_or_insert_with(|| BackendShaderBinding::Uniform(uniform_buf))
+ }
+ };
+
+ let BackendShaderBinding::Uniform(binding_uniform_buf) = binding else {
+ unreachable!();
+ };
+
+ let fvec3_value;
+ let mut color_value = [0.0; 4];
+
+ binding_uniform_buf
+ .store_at_byte_offset(
+ gl_context,
+ binding_location.byte_offset,
+ match binding_value {
+ ShaderBindingValue::Uint(ref value) => value.as_bytes(),
+ ShaderBindingValue::Int(ref value) => value.as_bytes(),
+ ShaderBindingValue::Float(ref value) => value.as_bytes(),
+ ShaderBindingValue::FVec3(value) => {
+ fvec3_value = [value.x, value.y, value.z];
+ fvec3_value.as_bytes()
+ }
+ ShaderBindingValue::Color(value) => match value {
+ Color::Rgb(value) => {
+ color_value[..3].copy_from_slice(&[value.r, value.g, value.b]);
+
+ color_value[..3].as_bytes()
+ }
+ Color::Rgba(value) => {
+ color_value[..4]
+ .copy_from_slice(&[value.r, value.g, value.b, value.a]);
+
+ color_value[..4].as_bytes()
+ }
+ },
+ ShaderBindingValue::FMat4x4(ref value) => value.items().as_bytes(),
+ ShaderBindingValue::Texture(_, _) => unreachable!(),
+ },
+ )
+ .unwrap();
+
+ Some(binding)
+
+ // if Some(shader_object_id)
+ // == activated_gl_shader_program
+ // .as_ref()
+ // .map(|(obj_id, ..)| *obj_id)
+ // {
+ // BackendShaderBinding::Uniform(binding_uniform_buf.clone()).bind(
+ // gl_context,
+ // ShaderBindingIndex(binding_location.binding_index),
+ // );
+ // }
+}
+
+fn draw_mesh(
+ current_context: &MaybeCurrentContextWithFns,
+ graphics_mesh: &GraphicsMesh,
+ opts: &DrawMeshOptions,
+) -> Result<(), GlDrawError>
+{
+ graphics_mesh.vertex_arr.bind(current_context);
+
+ if graphics_mesh.index_buffer.is_some() {
+ VertexArray::draw_elements(
+ current_context,
+ opengl_bindings::vertex_array::DrawElementsOptions {
+ primitive_kind: PrimitiveKind::Triangles,
+ element_offset: opts.element_offset,
+ element_cnt: opts.element_cnt.unwrap_or(graphics_mesh.element_cnt),
+ vertex_offset: opts.vertex_offset,
+ },
+ )?;
+ } else {
+ VertexArray::draw_arrays(
+ current_context,
+ PrimitiveKind::Triangles,
+ opts.vertex_offset,
+ opts.element_cnt.unwrap_or(graphics_mesh.element_cnt),
+ )?;
+ }
+
+ Ok(())
+}
+
+fn tex_pixel_data_format_into_gl(
+ tex_pixel_data_format: TexturePixelDataFormat,
+) -> GlTexturePixelDataFormat
+{
+ match tex_pixel_data_format {
+ TexturePixelDataFormat::Rgb(data_type) => {
+ GlTexturePixelDataFormat::Rgb(match data_type {
+ crate::rendering::RgbTextureDataType::UnsignedByte => {
+ opengl_bindings::texture::RgbDataType::UnsignedByte
+ }
+ })
+ }
+ TexturePixelDataFormat::Srgb(data_type) => {
+ GlTexturePixelDataFormat::Srgb(match data_type {
+ crate::rendering::SrgbTextureDataType::UnsignedByte => {
+ opengl_bindings::texture::SrgbDataType::UnsignedByte
+ }
+ })
+ }
+ TexturePixelDataFormat::Rgba(data_type) => {
+ GlTexturePixelDataFormat::Rgba(match data_type {
+ crate::rendering::RgbaTextureDataType::UnsignedByte => {
+ opengl_bindings::texture::RgbaDataType::UnsignedByte
+ }
+ })
+ }
+ TexturePixelDataFormat::Srgba(data_type) => {
+ GlTexturePixelDataFormat::Srgba(match data_type {
+ crate::rendering::SrgbaTextureDataType::UnsignedByte => {
+ opengl_bindings::texture::SrgbaDataType::UnsignedByte
+ }
+ })
+ }
+ TexturePixelDataFormat::Depth(data_type) => {
+ GlTexturePixelDataFormat::DepthComponent(match data_type {
+ crate::rendering::DepthTextureDataType::Float32 => {
+ opengl_bindings::texture::DepthComponentDataType::Float32
+ }
+ })
+ }
+ }
+}
+
+fn create_gl_texture(
+ curr_gl_context: &MaybeCurrentContextWithFns,
+ texture_creation: TextureCreation,
+ texture_pixel_data_format: TexturePixelDataFormat,
+ texture_properties: &TextureProperties,
+) -> Result<GlTexture, GlTextureError>
+{
+ let gl_texture = match texture_creation {
+ TextureCreation::Texture2D { size, image } => {
+ GlTexture::builder().size(size.into()).create_2d(
+ curr_gl_context,
+ image.as_deref(),
+ tex_pixel_data_format_into_gl(texture_pixel_data_format),
+ )
+ }
+ TextureCreation::CubeMap { size, images } => {
+ GlTexture::builder().size(size.into()).create_cube_map(
+ curr_gl_context,
+ images.as_ref().map(|images| {
+ images.each_ref().map(|(face, image)| {
+ (cube_map_texture_face_to_gl(*face), &**image)
+ })
+ }),
+ tex_pixel_data_format_into_gl(texture_pixel_data_format),
+ )
+ }
+ }?;
+
+ gl_texture.set_wrap(
+ curr_gl_context,
+ texture_wrapping_to_gl(texture_properties.wrap),
+ );
+
+ gl_texture.set_magnifying_filter(
+ curr_gl_context,
+ texture_filtering_to_gl(texture_properties.magnifying_filter),
+ );
+
+ gl_texture.set_minifying_filter(
+ curr_gl_context,
+ texture_filtering_to_gl(texture_properties.minifying_filter),
+ );
+
+ if let Some(border) = texture_properties.border.as_ref() {
+ gl_texture.set_float_border_values(
+ curr_gl_context,
+ match border {
+ TextureBorder::RgbaF32(color) => [color.r, color.g, color.b, color.a],
+ TextureBorder::Depth(depth) => [*depth, 0.0, 0.0, 0.0],
+ },
+ );
+ }
+
+ Ok(gl_texture)
+}
+
+fn cube_map_texture_face_to_gl(face: CubeMapTextureFace) -> GlCubeMapTextureFace
+{
+ match face {
+ CubeMapTextureFace::PositiveX => GlCubeMapTextureFace::PositiveX,
+ CubeMapTextureFace::NegativeX => GlCubeMapTextureFace::NegativeX,
+ CubeMapTextureFace::PositiveY => GlCubeMapTextureFace::PositiveY,
+ CubeMapTextureFace::NegativeY => GlCubeMapTextureFace::NegativeY,
+ CubeMapTextureFace::PositiveZ => GlCubeMapTextureFace::PositiveZ,
+ CubeMapTextureFace::NegativeZ => GlCubeMapTextureFace::NegativeZ,
+ }
+}
+
+fn create_shader_program(
+ current_context: &MaybeCurrentContextWithFns,
+ shader_program: &ShaderProgram,
+) -> Result<GlShaderProgram, CreateShaderError>
+{
+ let shader_program_reflection = shader_program.reflection(0).expect("Not possible");
+
+ let (vs_entry_point_index, vs_entry_point_reflection) = shader_program_reflection
+ .entry_points()
+ .enumerate()
+ .find(|(_, entry_point)| entry_point.stage() == ShaderStage::Vertex)
+ .ok_or_else(|| {
+ CreateShaderError::NoShaderStageEntrypointFound(ShaderStage::Vertex)
+ })?;
+
+ let vertex_shader_entry_point_code = shader_program
+ .get_entry_point_code(vs_entry_point_index.try_into().expect(
+ "Vertex shader entry point index does not fit in 32-bit unsigned int",
+ ))
+ .map_err(|err| CreateShaderError::GetShaderEntryPointCodeFailed {
+ err,
+ stage: ShaderStage::Vertex,
+ entrypoint: vs_entry_point_reflection
+ .name()
+ .map(|name| name.to_string().into())
+ .unwrap_or("(none)".into()),
+ })?;
+
+ let (fs_entry_point_index, fs_entry_point_reflection) = shader_program_reflection
+ .entry_points()
+ .enumerate()
+ .find(|(_, entry_point)| entry_point.stage() == ShaderStage::Fragment)
+ .ok_or_else(|| {
+ CreateShaderError::NoShaderStageEntrypointFound(ShaderStage::Fragment)
+ })?;
+
+ let fragment_shader_entry_point_code = shader_program
+ .get_entry_point_code(fs_entry_point_index.try_into().expect(
+ "Fragment shader entry point index does not fit in 32-bit unsigned int",
+ ))
+ .map_err(|err| CreateShaderError::GetShaderEntryPointCodeFailed {
+ err,
+ stage: ShaderStage::Fragment,
+ entrypoint: fs_entry_point_reflection
+ .name()
+ .map(|name| name.to_string().into())
+ .unwrap_or("(none)".into()),
+ })?;
+
+ let vertex_shader = GlShader::new(current_context, ShaderKind::Vertex);
+
+ vertex_shader.set_source(
+ current_context,
+ &vertex_shader_entry_point_code.as_str().unwrap(),
+ )?;
+
+ vertex_shader.compile(current_context)?;
+
+ let fragment_shader = GlShader::new(current_context, ShaderKind::Fragment);
+
+ fragment_shader.set_source(
+ current_context,
+ &fragment_shader_entry_point_code.as_str().unwrap(),
+ )?;
+
+ fragment_shader.compile(current_context)?;
+
+ let gl_shader_program = GlShaderProgram::new(current_context);
+
+ gl_shader_program.attach(current_context, &vertex_shader);
+ gl_shader_program.attach(current_context, &fragment_shader);
+
+ gl_shader_program.link(current_context)?;
+
+ Ok(gl_shader_program)
+}
+
+#[derive(Debug, thiserror::Error)]
+enum CreateShaderError
+{
+ #[error(
+ "Failed to get code of shader program entry point {entrypoint} of stage {stage:?}"
+ )]
+ GetShaderEntryPointCodeFailed
+ {
+ #[source]
+ err: ShaderError,
+ stage: ShaderStage,
+ entrypoint: Cow<'static, str>,
+ },
+
+ #[error("No entrypoint was found for shader stage {0:?}")]
+ NoShaderStageEntrypointFound(ShaderStage),
+
+ #[error(transparent)]
+ ShaderError(#[from] GlShaderError),
+}
+
+// This function not having tracing instrumentation is intentional. It would not provide
+// any value
+fn opengl_debug_message_cb(
+ source: MessageSource,
+ ty: MessageType,
+ id: u32,
+ severity: MessageSeverity,
+ message: &str,
+)
+{
+ macro_rules! emit_log_with_message {
+ ($level: expr) => {
+ tracing::event!($level, ?source, ?ty, id, ?severity, "{message}");
+ };
+ }
+
+ if matches!(severity, MessageSeverity::Notification) {
+ return;
+ }
+
+ match ty {
+ MessageType::Error => {
+ emit_log_with_message!(tracing::Level::ERROR);
+
+ let backtrace = std::backtrace::Backtrace::capture();
+
+ if backtrace.status() == std::backtrace::BacktraceStatus::Captured {
+ tracing::error!("{backtrace}");
+ }
+ }
+ MessageType::Other => {
+ emit_log_with_message!(tracing::Level::INFO);
+ }
+ _ => {
+ emit_log_with_message!(tracing::Level::WARN);
+ }
+ };
+}
+
+#[inline]
+fn texture_wrapping_to_gl(texture_wrapping: TextureWrapping) -> GlTextureWrapping
+{
+ match texture_wrapping {
+ TextureWrapping::Repeat => GlTextureWrapping::Repeat,
+ TextureWrapping::MirroredRepeat => GlTextureWrapping::MirroredRepeat,
+ TextureWrapping::ClampToEdge => GlTextureWrapping::ClampToEdge,
+ TextureWrapping::ClampToBorder => GlTextureWrapping::ClampToBorder,
+ }
+}
+
+#[inline]
+fn texture_filtering_to_gl(texture_filtering: TextureFiltering) -> GlTextureFiltering
+{
+ match texture_filtering {
+ TextureFiltering::Linear => GlTextureFiltering::Linear,
+ TextureFiltering::Nearest => GlTextureFiltering::Nearest,
+ }
+}
+
+impl<Value: ReprC + Copy> From<Vec2<Value>> for opengl_bindings::data_types::Vec2<Value>
+{
+ fn from(vec2: Vec2<Value>) -> Self
+ {
+ Self { x: vec2.x, y: vec2.y }
+ }
+}
+
+impl<Value: ReprC + IntoBytes + Copy> From<Vec3<Value>>
+ for opengl_bindings::data_types::Vec3<Value>
+{
+ fn from(vec3: Vec3<Value>) -> Self
+ {
+ Self { x: vec3.x, y: vec3.y, z: vec3.z }
+ }
+}
+
+impl<Value: Copy> From<Dimens<Value>> for opengl_bindings::data_types::Dimens<Value>
+{
+ fn from(dimens: Dimens<Value>) -> Self
+ {
+ Self {
+ width: dimens.width,
+ height: dimens.height,
+ }
+ }
+}
+
+fn polygon_mode_to_gl(mode: PolygonMode) -> GlPolygonMode
+{
+ match mode {
+ PolygonMode::Point => GlPolygonMode::Point,
+ PolygonMode::Fill => GlPolygonMode::Fill,
+ PolygonMode::Line => GlPolygonMode::Line,
+ }
+}
+
+fn polygon_mode_face_to_gl(face: PolygonModeFace) -> GlPolygonModeFace
+{
+ match face {
+ PolygonModeFace::Front => GlPolygonModeFace::Front,
+ PolygonModeFace::Back => GlPolygonModeFace::Back,
+ PolygonModeFace::FrontAndBack => GlPolygonModeFace::FrontAndBack,
+ }
+}
+
+fn blending_factor_to_gl(blending_factor: BlendingFactor) -> GlBlendingFactor
+{
+ match blending_factor {
+ BlendingFactor::Zero => GlBlendingFactor::Zero,
+ BlendingFactor::One => GlBlendingFactor::One,
+ BlendingFactor::SrcColor => GlBlendingFactor::SrcColor,
+ BlendingFactor::OneMinusSrcColor => GlBlendingFactor::OneMinusSrcColor,
+ BlendingFactor::DstColor => GlBlendingFactor::DstColor,
+ BlendingFactor::OneMinusDstColor => GlBlendingFactor::OneMinusDstColor,
+ BlendingFactor::SrcAlpha => GlBlendingFactor::SrcAlpha,
+ BlendingFactor::OneMinusSrcAlpha => GlBlendingFactor::OneMinusSrcAlpha,
+ BlendingFactor::DstAlpha => GlBlendingFactor::DstAlpha,
+ BlendingFactor::OneMinusDstAlpha => GlBlendingFactor::OneMinusDstAlpha,
+ BlendingFactor::ConstantColor => GlBlendingFactor::ConstantColor,
+ BlendingFactor::OneMinusConstantColor => GlBlendingFactor::OneMinusConstantColor,
+ BlendingFactor::ConstantAlpha => GlBlendingFactor::ConstantAlpha,
+ BlendingFactor::OneMinusConstantAlpha => GlBlendingFactor::OneMinusConstantAlpha,
+ }
+}
+
+fn blending_equation_to_gl(blending_equation: BlendingEquation) -> GlBlendingEquation
+{
+ match blending_equation {
+ BlendingEquation::Add => GlBlendingEquation::Add,
+ BlendingEquation::Subtract => GlBlendingEquation::Subtract,
+ BlendingEquation::ReverseSubtract => GlBlendingEquation::ReverseSubtract,
+ BlendingEquation::Min => GlBlendingEquation::Min,
+ BlendingEquation::Max => GlBlendingEquation::Max,
+ }
+}
+
+trait IntMapEntryExt<'map, Value>
+{
+ fn set_or_insert_with(self, func: impl FnOnce() -> Value) -> &'map mut Value;
+}
+
+impl<'map, Key, Value> IntMapEntryExt<'map, Value> for intmap::Entry<'map, Key, Value>
+where
+ Key: intmap::IntKey,
+ Value: 'map,
+{
+ fn set_or_insert_with(self, func: impl FnOnce() -> Value) -> &'map mut Value
+ {
+ match self {
+ Self::Occupied(occupied) => {
+ let value = occupied.into_mut();
+
+ *value = func();
+
+ value
+ }
+ Self::Vacant(vacant) => vacant.insert(func()),
+ }
+ }
+}
diff --git a/engine/src/rendering/backend/opengl/glutin_compat.rs b/engine/src/rendering/backend/opengl/glutin_compat.rs
new file mode 100644
index 0000000..27f82ad
--- /dev/null
+++ b/engine/src/rendering/backend/opengl/glutin_compat.rs
@@ -0,0 +1,264 @@
+// Original file:
+// https://github.com/rust-windowing/glutin/blob/
+// 0433af9018febe0696c485ed9d66c40dad41f2d4/glutin-winit/src/lib.rs
+//
+// Copyright © 2022 Kirill Chibisov
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the “Software”), to deal
+// in the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+//! This library provides helpers for cross-platform [`glutin`] bootstrapping
+//! with [`winit`].
+
+#![deny(rust_2018_idioms)]
+#![deny(rustdoc::broken_intra_doc_links)]
+#![deny(clippy::all)]
+#![deny(missing_debug_implementations)]
+#![deny(missing_docs)]
+#![cfg_attr(clippy, deny(warnings))]
+
+use glutin::config::{Config, ConfigTemplateBuilder};
+use glutin::display::{Display, DisplayApiPreference};
+use glutin::error::Error as GlutinError;
+use glutin::prelude::*;
+use raw_window_handle::{DisplayHandle, RawWindowHandle, WindowHandle};
+
+use crate::windowing::window::CreationAttributes as WindowCreationAttributes;
+
+#[cfg(all(not(windows), not(target_os = "macos"), not(target_os = "linux")))]
+compile_error!("Unsupported platform");
+
+#[cfg(target_family = "wasm")]
+compile_error!("Wasm targets are not supported");
+
+/// The helper to perform [`Display`] creation and OpenGL platform
+/// bootstrapping with the help of [`winit`] with little to no platform specific
+/// code.
+///
+/// This is only required for the initial setup. If you want to create
+/// additional windows just use the [`finalize_window`] function and the
+/// configuration you've used either for the original window or picked with the
+/// existing [`Display`].
+///
+/// [`winit`]: winit
+/// [`Display`]: glutin::display::Display
+#[derive(Default, Debug, Clone)]
+pub struct DisplayBuilder
+{
+ preference: ApiPreference,
+ window_attributes: WindowCreationAttributes,
+}
+
+impl DisplayBuilder
+{
+ /// Create new display builder.
+ pub fn new() -> Self
+ {
+ Default::default()
+ }
+
+ /// The preference in picking the configuration.
+ #[allow(dead_code)]
+ pub fn with_preference(mut self, preference: ApiPreference) -> Self
+ {
+ self.preference = preference;
+ self
+ }
+
+ /// The window attributes to use when building a window.
+ ///
+ /// By default no window is created.
+ pub fn with_window_attributes(
+ mut self,
+ window_creation_attrs: WindowCreationAttributes,
+ ) -> Self
+ {
+ self.window_attributes = window_creation_attrs;
+ self
+ }
+
+ /// Initialize the OpenGL platform and create a compatible window to use
+ /// with it when the [`WindowAttributes`] was passed with
+ /// [`Self::with_window_attributes()`]. It's optional, since on some
+ /// platforms like `Android` it is not available early on, so you want to
+ /// find configuration and later use it with the [`finalize_window`].
+ /// But if you don't care about such platform you can always pass
+ /// [`WindowAttributes`].
+ ///
+ /// # Api-specific
+ ///
+ /// **WGL:** - [`WindowAttributes`] **must** be passed in
+ /// [`Self::with_window_attributes()`] if modern OpenGL(ES) is desired,
+ /// otherwise only builtin functions like `glClear` will be available.
+ pub fn build<ConfigPickerFn>(
+ self,
+ window_handle: Option<WindowHandle<'_>>,
+ display_handle: &DisplayHandle<'_>,
+ template_builder: ConfigTemplateBuilder,
+ config_picker_fn: ConfigPickerFn,
+ ) -> Result<(WindowCreationAttributes, Config), Error>
+ where
+ ConfigPickerFn: FnOnce(Box<dyn Iterator<Item = Config> + '_>) -> Option<Config>,
+ {
+ // XXX with WGL backend window should be created first.
+ let raw_window_handle = if cfg!(windows) {
+ let Some(window_handle) = window_handle else {
+ return Err(Error::WindowRequired);
+ };
+
+ Some(window_handle.as_raw())
+ } else {
+ None
+ };
+
+ let gl_display =
+ create_display(display_handle, self.preference, raw_window_handle)
+ .map_err(Error::CreateDisplayFailed)?;
+
+ // XXX the native window must be passed to config picker when WGL is used
+ // otherwise very limited OpenGL features will be supported.
+ let template_builder = if let Some(raw_window_handle) =
+ raw_window_handle.filter(|_| cfg!(windows))
+ {
+ template_builder.compatible_with_native_window(raw_window_handle)
+ } else {
+ template_builder
+ };
+
+ let template = template_builder.build();
+
+ // SAFETY: The RawWindowHandle passed on the config template
+ // (when cfg(windows)) will always point to a valid object since it is
+ // derived from the window_handle argument which when Some is a WindowHandle and
+ // WindowHandles always point to a valid object
+ let gl_configs = unsafe { gl_display.find_configs(template) }
+ .map_err(Error::FindConfigsFailed)?;
+
+ let picked_gl_config =
+ config_picker_fn(gl_configs).ok_or(Error::NoConfigPicked)?;
+
+ let window_attrs = cfg_select! {
+ windows => { self.window_attributes }
+ _ => {
+ finalize_window_creation_attrs(self.window_attributes, &picked_gl_config)
+ }
+ };
+
+ Ok((window_attrs, picked_gl_config))
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error
+{
+ #[error("Failed to create display")]
+ CreateDisplayFailed(#[source] GlutinError),
+
+ #[error("Failed to find configs")]
+ FindConfigsFailed(#[source] GlutinError),
+
+ #[error("No config was picked by config picker function")]
+ NoConfigPicked,
+
+ #[error("Window required for building display on current platform")]
+ WindowRequired,
+}
+
+fn create_display(
+ display_handle: &DisplayHandle<'_>,
+ _api_preference: ApiPreference,
+ _raw_window_handle: Option<RawWindowHandle>,
+) -> Result<Display, GlutinError>
+{
+ let preference = cfg_select! {
+ windows => {
+ match _api_preference {
+ ApiPreference::PreferEgl => {
+ DisplayApiPreference::EglThenWgl(_raw_window_handle)
+ }
+ ApiPreference::FallbackEgl => {
+ DisplayApiPreference::WglThenEgl(_raw_window_handle)
+ }
+ }
+ }
+ target_os = "linux" => {
+ match _api_preference {
+ ApiPreference::PreferEgl => DisplayApiPreference::EglThenGlx(Box::new(
+ crate::windowing::window::platform::x11::register_xlib_error_hook,
+ )),
+ ApiPreference::FallbackEgl => DisplayApiPreference::GlxThenEgl(Box::new(
+ crate::windowing::window::platform::x11::register_xlib_error_hook,
+ )),
+ }
+ }
+ target_os = "macos" => { DisplayApiPreference::Cgl }
+ };
+
+ let handle = display_handle.as_raw();
+
+ unsafe { Ok(Display::new(handle, preference)?) }
+}
+
+/// Finalize [`Window`] creation by applying the options from the [`Config`], be
+/// aware that it could remove incompatible options from the window builder like
+/// `transparency`, when the provided config doesn't support it.
+///
+/// [`Window`]: winit::window::Window
+/// [`Config`]: glutin::config::Config
+#[cfg(not(windows))]
+fn finalize_window_creation_attrs(
+ mut attributes: WindowCreationAttributes,
+ gl_config: &Config,
+) -> WindowCreationAttributes
+{
+ // Disable transparency if the end config doesn't support it.
+ if gl_config.supports_transparency() == Some(false) {
+ attributes = attributes.with_transparent(false);
+ }
+
+ #[cfg(target_os = "linux")]
+ if let Some(x11_visual) = glutin::platform::x11::X11GlConfigExt::x11_visual(gl_config)
+ {
+ return attributes.with_x_visual_id(Some(
+ x11_visual.visual_id() as crate::windowing::window::XVisualID
+ ));
+ }
+
+ attributes
+}
+
+/// Simplified version of the [`DisplayApiPreference`] which is used to simplify
+/// cross platform window creation.
+///
+/// To learn about platform differences the [`DisplayApiPreference`] variants.
+///
+/// [`DisplayApiPreference`]: glutin::display::DisplayApiPreference
+#[allow(dead_code)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum ApiPreference
+{
+ /// Prefer `EGL` over system provider like `GLX` and `WGL`.
+ PreferEgl,
+
+ /// Fallback to `EGL` when failed to create the system profile.
+ ///
+ /// This behavior is used by default. However consider using
+ /// [`Self::PreferEgl`] if you don't care about missing EGL features.
+ #[default]
+ FallbackEgl,
+}
diff --git a/engine/src/rendering/backend/opengl/graphics_mesh.rs b/engine/src/rendering/backend/opengl/graphics_mesh.rs
new file mode 100644
index 0000000..a3f4197
--- /dev/null
+++ b/engine/src/rendering/backend/opengl/graphics_mesh.rs
@@ -0,0 +1,259 @@
+use std::hint::cold_path;
+
+use opengl_bindings::buffer::{Buffer as GlBuffer, Usage as GlBufferUsage};
+use opengl_bindings::vertex_array::{
+ AttributeFormat as GlVertexArrayAttributeFormat,
+ BindVertexBufferError as GlVertexArrayBindVertexBufferError,
+ DataType as GlVertexArrayDataType,
+ VertexArray as GlVertexArray,
+ VertexBufferSpec as GlVertexArrayVertexBufferSpec,
+};
+use opengl_bindings::MaybeCurrentContextWithFns as GlCurrentContextWithFns;
+
+use crate::mesh::vertex_buffer::VertexAttrProperties as MeshVertexAttrProperties;
+use crate::mesh::{Mesh, VertexAttrType};
+use crate::rendering::shader::{
+ VertexDescription as ShaderVertexDescription,
+ VertexInputSemName,
+};
+use crate::rendering::MeshUsage;
+
+const VERTEX_BUF_BINDING_INDEX: u32 = 0;
+
+#[derive(Debug)]
+pub struct GraphicsMesh
+{
+ /// Vertex and index buffer has to live as long as the vertex array
+ vertex_buffer: GlBuffer<u8>,
+ vertex_attr_props: Vec<MeshVertexAttrProperties>,
+ last_max_vertex_attr_index: u32,
+ pub index_buffer: Option<GlBuffer<u32>>,
+ pub element_cnt: u32,
+ pub vertex_arr: GlVertexArray,
+}
+
+impl GraphicsMesh
+{
+ #[tracing::instrument(skip_all)]
+ pub fn new(
+ current_context: &GlCurrentContextWithFns,
+ mesh: &Mesh,
+ mesh_usage: MeshUsage,
+ ) -> Result<Self, Error>
+ {
+ let buffer_usage = mesh_usage_to_gl_buffer_usage(mesh_usage);
+
+ let vertex_arr = GlVertexArray::new(current_context);
+ let vertex_buffer = GlBuffer::new(current_context);
+
+ vertex_buffer
+ .store(current_context, mesh.vertex_buf().as_bytes(), buffer_usage)
+ .map_err(Error::StoreVerticesFailed)?;
+
+ if let Err(err) = vertex_arr.bind_vertex_buffer(
+ current_context,
+ VERTEX_BUF_BINDING_INDEX,
+ &vertex_buffer,
+ GlVertexArrayVertexBufferSpec {
+ offset: 0,
+ vertex_size: mesh.vertex_buf().vertex_size(),
+ },
+ ) {
+ match err {
+ GlVertexArrayBindVertexBufferError::OffsetValueTooLarge {
+ value: _,
+ max_value: _,
+ } => unreachable!(),
+ GlVertexArrayBindVertexBufferError::VertexSizeValueTooLarge {
+ value,
+ max_value,
+ } => {
+ panic!(
+ "Size of vertex ({}) is too large. Must be less than {max_value}",
+ value
+ );
+ }
+ }
+ }
+
+ if let Some(indices) = mesh.indices() {
+ let index_buffer = GlBuffer::new(current_context);
+
+ index_buffer
+ .store(current_context, indices, buffer_usage)
+ .map_err(Error::StoreIndicesFailed)?;
+
+ vertex_arr.bind_element_buffer(current_context, &index_buffer);
+
+ return Ok(Self {
+ vertex_buffer: vertex_buffer,
+ vertex_attr_props: mesh.vertex_buf().vertex_attr_props().to_vec(),
+ last_max_vertex_attr_index: 0,
+ index_buffer: Some(index_buffer),
+ element_cnt: indices
+ .len()
+ .try_into()
+ .expect("Mesh index count does not fit into a 32-bit unsigned int"),
+ vertex_arr,
+ });
+ }
+
+ Ok(Self {
+ vertex_buffer: vertex_buffer,
+ vertex_attr_props: mesh.vertex_buf().vertex_attr_props().to_vec(),
+ last_max_vertex_attr_index: 0,
+ index_buffer: None,
+ element_cnt: mesh
+ .vertex_buf()
+ .len()
+ .try_into()
+ .expect("Mesh vertex count does not fit into a 32-bit unsigned int"),
+ vertex_arr,
+ })
+ }
+
+ pub fn update(
+ &mut self,
+ current_context: &GlCurrentContextWithFns,
+ mesh: &Mesh,
+ mesh_usage: MeshUsage,
+ ) -> Result<(), Error>
+ {
+ let buffer_usage = mesh_usage_to_gl_buffer_usage(mesh_usage);
+
+ self.vertex_buffer
+ .store(current_context, mesh.vertex_buf().as_bytes(), buffer_usage)
+ .map_err(Error::StoreVerticesFailed)?;
+
+ if let Some(indices) = mesh.indices() {
+ let index_buffer = self
+ .index_buffer
+ .get_or_insert_with(|| GlBuffer::new(current_context));
+
+ index_buffer
+ .store(current_context, indices, buffer_usage)
+ .map_err(Error::StoreIndicesFailed)?;
+
+ self.vertex_arr
+ .bind_element_buffer(current_context, &index_buffer);
+
+ self.element_cnt = indices
+ .len()
+ .try_into()
+ .expect("Mesh index count does not fit into a 32-bit unsigned int");
+
+ return Ok(());
+ }
+
+ self.element_cnt = mesh
+ .vertex_buf()
+ .len()
+ .try_into()
+ .expect("Mesh vertex count does not fit into a 32-bit unsigned int");
+
+ Ok(())
+ }
+
+ pub fn update_vertex_attrs_for_shader(
+ &mut self,
+ curr_gl_ctx: &GlCurrentContextWithFns,
+ shader_vertex_desc: &ShaderVertexDescription,
+ ) -> Result<(), VertexAttrsUpdatingError>
+ {
+ for index in 0..self.last_max_vertex_attr_index {
+ self.vertex_arr.disable_attrib(curr_gl_ctx, index);
+ }
+
+ let mut max_vertex_attr_index = 0u32;
+
+ for vertex_input_desc in &*shader_vertex_desc.inputs {
+ let Some(vertex_attr_props) =
+ self.vertex_attr_props.iter().find(|vertex_attr_props| {
+ vertex_input_desc
+ .semantic_name
+ .matches_vertex_label(&vertex_attr_props.label)
+ })
+ else {
+ cold_path();
+ return Err(VertexAttrsUpdatingError::MissingVertexAttr(
+ vertex_input_desc.semantic_name.clone(),
+ ));
+ };
+
+ let attrib_index: u32 = vertex_input_desc.index.try_into().unwrap();
+
+ self.vertex_arr.enable_attrib(curr_gl_ctx, attrib_index);
+
+ self.vertex_arr.set_attrib_format(
+ curr_gl_ctx,
+ attrib_index,
+ match &vertex_attr_props.ty {
+ VertexAttrType::Float32 => GlVertexArrayAttributeFormat {
+ data_type: GlVertexArrayDataType::Float,
+ count: 1,
+ normalized: false,
+ offset: vertex_attr_props.byte_offset.try_into().unwrap(),
+ },
+ VertexAttrType::Float32Array { length } => {
+ GlVertexArrayAttributeFormat {
+ data_type: GlVertexArrayDataType::Float,
+ count: (*length).try_into().unwrap(),
+ normalized: false,
+ offset: vertex_attr_props.byte_offset.try_into().unwrap(),
+ }
+ }
+ },
+ );
+
+ self.vertex_arr.set_attrib_vertex_buf_binding(
+ curr_gl_ctx,
+ attrib_index,
+ VERTEX_BUF_BINDING_INDEX,
+ );
+
+ max_vertex_attr_index = max_vertex_attr_index.max(attrib_index);
+ }
+
+ self.last_max_vertex_attr_index = max_vertex_attr_index;
+
+ Ok(())
+ }
+
+ pub fn destroy(&self, curr_gl_ctx: &GlCurrentContextWithFns)
+ {
+ self.vertex_arr.delete(curr_gl_ctx);
+ self.vertex_buffer.delete(curr_gl_ctx);
+
+ if let Some(index_buffer) = &self.index_buffer {
+ index_buffer.delete(curr_gl_ctx);
+ }
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error
+{
+ #[error("Failed to store vertices in vertex buffer")]
+ StoreVerticesFailed(#[source] opengl_bindings::buffer::Error),
+
+ #[error("Failed to store indices in index buffer")]
+ StoreIndicesFailed(#[source] opengl_bindings::buffer::Error),
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum VertexAttrsUpdatingError
+{
+ #[error(
+ "Mesh is missing equivalent vertex attribute for shader's vertex input: {0}"
+ )]
+ MissingVertexAttr(VertexInputSemName),
+}
+
+fn mesh_usage_to_gl_buffer_usage(mesh_usage: MeshUsage) -> GlBufferUsage
+{
+ match mesh_usage {
+ MeshUsage::Stream => GlBufferUsage::Stream,
+ MeshUsage::Static => GlBufferUsage::Static,
+ MeshUsage::Dynamic => GlBufferUsage::Dynamic,
+ }
+}
diff --git a/engine/src/rendering/blending.rs b/engine/src/rendering/blending.rs
new file mode 100644
index 0000000..9ae2f82
--- /dev/null
+++ b/engine/src/rendering/blending.rs
@@ -0,0 +1,89 @@
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Config
+{
+ pub source_factor: Factor,
+ pub destination_factor: Factor,
+ pub equation: Equation,
+}
+
+impl Default for Config
+{
+ fn default() -> Self
+ {
+ Self {
+ source_factor: Factor::One,
+ destination_factor: Factor::Zero,
+ equation: Equation::default(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum Factor
+{
+ /// Factor will be the RGBA color `(0,0,0,0)`
+ Zero,
+
+ /// Factor will be the RGBA color `(1,1,1,1)`
+ One,
+
+ /// Factor will be the source color
+ SrcColor,
+
+ /// Factor will be the RGBA color `(1,1,1,1) - source color`
+ OneMinusSrcColor,
+
+ /// Factor will be the destination color
+ DstColor,
+
+ /// Factor will be the RGBA color `(1,1,1,1) - destination color`
+ OneMinusDstColor,
+
+ /// Factor will be the alpha component of the source color.
+ SrcAlpha,
+
+ /// Factor will be the RGBA color `(1,1,1,1) - source color alpha`
+ OneMinusSrcAlpha,
+
+ /// Factor will be the alpha component of the destination color.
+ DstAlpha,
+
+ /// Factor will be the RGBA color `(1,1,1,1) - destination color alpha`
+ OneMinusDstAlpha,
+
+ /// Factor will be the constant color
+ ConstantColor,
+
+ /// Factor will be the RGBA color `(1,1,1,1) - constant color`
+ OneMinusConstantColor,
+
+ /// Factor will be the alpha component of the constant color.
+ ConstantAlpha,
+
+ /// Factor will be the RGBA color `(1,1,1,1) - constant color alpha`
+ OneMinusConstantAlpha,
+}
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub enum Equation
+{
+ /// The destination color and source color is added to each other in the blend
+ /// function
+ #[default]
+ Add,
+
+ /// The destination color is subtracted from the source color in the blend function
+ Subtract,
+
+ /// The source color is subtracted from the destination color in the blend function
+ ReverseSubtract,
+
+ /// The blend function will take the component-wise minimum of the destination color
+ /// and the source color
+ Min,
+
+ /// The blend function will take the component-wise maximum of the destination color
+ /// and the source color
+ Max,
+}
diff --git a/engine/src/rendering/main_render_pass.rs b/engine/src/rendering/main_render_pass.rs
new file mode 100644
index 0000000..c762231
--- /dev/null
+++ b/engine/src/rendering/main_render_pass.rs
@@ -0,0 +1,1006 @@
+use std::path::Path;
+use std::sync::LazyLock;
+
+use ecs::actions::Actions;
+use ecs::component::local::Local;
+use ecs::error::Context as _;
+use ecs::pair::ChildOf;
+use ecs::query::term::{Traverse, TraverseUp};
+use ecs::uid::Uid;
+use ecs::Component;
+
+use crate::asset::{Assets, Label as AssetLabel};
+use crate::camera::{Active as ActiveCamera, Camera};
+use crate::color::Rgba;
+use crate::data_types::dimens::{Dimens, Dimens3};
+use crate::draw_flags::{DrawFlags, NoDraw, PolygonModeConfig};
+use crate::ecs::query::term::{With, Without};
+use crate::ecs::sole::Single;
+use crate::ecs::Query;
+use crate::error;
+use crate::image::Image;
+use crate::lighting::{
+ DirectionalLight,
+ Environmental as EnvironmentalLighting,
+ PointLight,
+};
+use crate::material::{Flags as MaterialFlags, Material};
+use crate::mesh::Mesh;
+use crate::model::{MaterialSearchResult, Model};
+use crate::projection::{ClipVolume as ProjectionClipVolume, Perspective, Projection};
+use crate::rendering::object::{Id as ObjectId, Store as ObjectStore};
+use crate::rendering::shader::cursor::{
+ BindingTextureKind as ShaderBindingTextureKind,
+ BindingValue as ShaderBindingValue,
+ Cursor as ShaderCursor,
+};
+use crate::rendering::shader::{
+ Context as ShaderContext,
+ EntrypointFlags as ShaderEntrypointFlags,
+ ModuleSource as ShaderModuleSource,
+ Program as ShaderProgram,
+};
+use crate::rendering::{
+ AssetOrValue,
+ BufferClearMask,
+ Command,
+ DepthFunction,
+ DrawMeshOptions,
+ DrawProperties,
+ DrawPropertiesUpdateFlags,
+ MeshUsage,
+ RenderPass,
+ RenderPasses,
+ RgbaTextureDataType,
+ Surface,
+ TargetWindow,
+ TextureCreation,
+ TexturePixelDataFormat,
+};
+use crate::scene::{Active as ActiveScene, Scene};
+use crate::sky_box::SkyBox;
+use crate::texture::{
+ Filtering as TextureFiltering,
+ Properties as TextureProperties,
+ Texture,
+ Wrapping as TextureWrapping,
+};
+use crate::transform::Transform;
+use crate::vector::Vec3;
+use crate::windowing::window::Window;
+
+type RenderableEntity<'a> = (
+ &'a Model,
+ Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>,
+ Option<&'a MaterialFlags>,
+ Option<&'a Transform>,
+ Option<&'a DrawFlags>,
+ Without<NoDraw>,
+);
+
+pub static SKY_BOX_SHADER_ASSET_LABEL: LazyLock<AssetLabel> =
+ LazyLock::new(|| AssetLabel {
+ path: Path::new("").into(),
+ name: Some("sky_box_shader".into()),
+ });
+
+pub static MAIN_3D_SHADER_ASSET_LABEL: LazyLock<AssetLabel> =
+ LazyLock::new(|| AssetLabel {
+ path: Path::new("").into(),
+ name: Some("main_3d_shader".into()),
+ });
+
+struct SkyBoxIds
+{
+ texture_object: ObjectId,
+ mesh_object: ObjectId,
+}
+
+#[derive(Debug, Default, Component)]
+pub struct MiscObjectIds
+{
+ white_1x1_tex_obj_id: Option<ObjectId>,
+}
+
+#[tracing::instrument(skip_all)]
+pub fn add_main_render_pass(
+ renderable_query: Query<RenderableEntity<'_>>,
+ camera_query: Query<(
+ &Camera,
+ &Transform,
+ &ActiveCamera,
+ Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>,
+ )>,
+ window_query: Query<(&Window, &Surface, With<TargetWindow>)>,
+ scene_query: Query<(
+ Option<&EnvironmentalLighting>,
+ Option<&SkyBox>,
+ Option<&SkyBoxState>,
+ With<Scene>,
+ With<ActiveScene>,
+ )>,
+ point_light_query: Query<(
+ &PointLight,
+ &Transform,
+ Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>,
+ )>,
+ directional_light_query: Query<(
+ &DirectionalLight,
+ Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>,
+ )>,
+ shader_context: Single<ShaderContext>,
+ mut assets: Single<Assets>,
+ mut render_passes: Single<RenderPasses>,
+ mut object_store: Single<ObjectStore>,
+ mut actions: Actions,
+ mut misc_object_ids: Local<MiscObjectIds>,
+) -> Result<(), crate::Error>
+{
+ let assets = assets.get_mut()?;
+ let shader_context = shader_context.get()?;
+ let render_passes = render_passes.get_mut()?;
+ let object_store = object_store.get_mut()?;
+
+ let Some((scene_ent_id, (scene_env_lighting, scene_skybox, scene_skybox_state))) =
+ scene_query.iter_with_euids().next()
+ else {
+ return Ok(());
+ };
+
+ let Some((camera, camera_transform, _)) = camera_query.iter().next() else {
+ tracing::trace!("No active camera");
+ return Ok(());
+ };
+
+ let render_pass = render_passes.passes.push_front_mut(RenderPass {
+ commands: Vec::with_capacity(30),
+ draw_properties: DrawProperties::default(),
+ });
+
+ for (model, ..) in &renderable_query {
+ add_renderable_creation_commands(assets, object_store, render_pass, &model);
+ }
+
+ let shaders =
+ match get_or_create_shaders(assets, shader_context, object_store, render_pass) {
+ Ok(ShadersCreationStatus::Ready(ready_shaders)) => ready_shaders,
+ Ok(ShadersCreationStatus::ProcessingRequired) => return Ok(()),
+ Err(err) => {
+ tracing::error!("Failed to create shaders: {err}");
+ return Ok(());
+ }
+ };
+
+ let white_1x1_tex_obj_id =
+ *misc_object_ids.white_1x1_tex_obj_id.get_or_insert_with(|| {
+ create_white_1x1_texture_object(object_store, render_pass)
+ });
+
+ let main_3d_shader_cursor = ShaderCursor::new(
+ shaders
+ .main_3d_shader_program
+ .reflection(0)
+ .context("Unable to get reflection for main 3D shader")?
+ .global_params_var_layout()
+ .ok_or_else(|| {
+ crate::Error::message(
+ "Unable to get reflection for main 3D shader's global parameters",
+ )
+ })?,
+ );
+
+ add_set_3d_shader_point_light_bindings(
+ render_pass,
+ &point_light_query,
+ shaders.main_3d_shader_obj_id,
+ &main_3d_shader_cursor,
+ )?;
+
+ add_set_3d_shader_dir_lights_bindings(
+ render_pass,
+ &directional_light_query,
+ shaders.main_3d_shader_obj_id,
+ &main_3d_shader_cursor,
+ )?;
+
+ for (window, window_surface) in &window_query {
+ render_pass
+ .commands
+ .push(Command::MakeCurrent(window_surface.id));
+
+ add_set_3d_shader_camera_bindings(
+ render_pass,
+ (&camera, &camera_transform),
+ &window,
+ shaders.main_3d_shader_obj_id,
+ &main_3d_shader_cursor,
+ )?;
+
+ let mut buf_clear_mask = BufferClearMask::DEPTH;
+
+ buf_clear_mask.set(BufferClearMask::COLOR, scene_skybox.is_none());
+
+ render_pass
+ .commands
+ .push(Command::ClearBuffers(buf_clear_mask));
+
+ for (model, material_flags, transform, draw_flags) in &renderable_query {
+ let Some(model_spec) = assets.get(&model.spec_asset) else {
+ continue;
+ };
+
+ let Some(mesh_asset) = &model_spec.mesh_asset else {
+ continue;
+ };
+
+ if assets.get(mesh_asset).is_none() {
+ continue;
+ }
+
+ let model_material = match model_spec.find_first_material(&assets) {
+ MaterialSearchResult::Found(model_material_asset)
+ if let Some(model_material) = assets.get(&model_material_asset) =>
+ {
+ model_material
+ }
+ MaterialSearchResult::Found(_) | MaterialSearchResult::NotFound => {
+ continue;
+ }
+ MaterialSearchResult::NoMaterials => &Material::builder().build(),
+ };
+
+ if model_material
+ .textures()
+ .any(|texture_asset| !assets.is_loaded_and_has_type(&texture_asset))
+ {
+ continue;
+ }
+
+ add_set_3d_shader_renderable_bindings(
+ render_pass,
+ (
+ model_material,
+ material_flags.as_deref(),
+ transform.as_deref(),
+ ),
+ scene_env_lighting.as_deref(),
+ shaders.main_3d_shader_obj_id,
+ &main_3d_shader_cursor,
+ white_1x1_tex_obj_id,
+ )?;
+
+ render_pass
+ .commands
+ .push(Command::ActivateShader(shaders.main_3d_shader_obj_id));
+
+ if let Some(draw_flags) = draw_flags.as_deref().and_then(|draw_flags| {
+ if draw_flags.polygon_mode_config != PolygonModeConfig::default() {
+ Some(draw_flags)
+ } else {
+ None
+ }
+ }) {
+ render_pass.commands.push(Command::UpdateDrawProperties(
+ DrawProperties {
+ polygon_mode_config: draw_flags.polygon_mode_config.clone(),
+ ..Default::default()
+ },
+ DrawPropertiesUpdateFlags::POLYGON_MODE_CONFIG,
+ ));
+ }
+
+ render_pass.commands.push(Command::DrawMesh(
+ ObjectId::Asset(mesh_asset.id()),
+ DrawMeshOptions::default(),
+ ));
+
+ if draw_flags.as_deref().is_some_and(|draw_flags| {
+ draw_flags.polygon_mode_config != PolygonModeConfig::default()
+ }) {
+ render_pass.commands.push(Command::UpdateDrawProperties(
+ DrawProperties {
+ polygon_mode_config: PolygonModeConfig::default(),
+ ..Default::default()
+ },
+ DrawPropertiesUpdateFlags::POLYGON_MODE_CONFIG,
+ ));
+ }
+ }
+
+ if let Some(scene_skybox) = &scene_skybox {
+ let Some(sky_box_ids) = load_sky_box(
+ scene_ent_id,
+ &scene_skybox,
+ scene_skybox_state.as_deref(),
+ assets,
+ object_store,
+ render_pass,
+ &mut actions,
+ )?
+ else {
+ continue;
+ };
+
+ add_sky_box_commands(
+ render_pass,
+ &sky_box_ids,
+ &camera,
+ &camera_transform,
+ &window,
+ shaders.sky_box_shader_obj_id,
+ shaders.sky_box_shader_program,
+ )?;
+ }
+ }
+
+ Ok(())
+}
+
+fn create_white_1x1_texture_object(
+ object_store: &mut ObjectStore,
+ render_pass: &mut RenderPass,
+) -> ObjectId
+{
+ let white_1x1_tex_obj_id = ObjectId::new_sequential();
+
+ object_store.insert_pending(white_1x1_tex_obj_id);
+
+ render_pass.commands.push(Command::CreateTexture {
+ obj_id: white_1x1_tex_obj_id,
+ pixel_data_format: TexturePixelDataFormat::Rgba(
+ RgbaTextureDataType::UnsignedByte,
+ ),
+ creation: TextureCreation::Texture2D {
+ size: Dimens { width: 1, height: 1 },
+ image: Some(
+ Image::from_color(Rgba::<u8>::white(), Dimens { width: 1, height: 1 })
+ .into_bytes()
+ .into_boxed_slice(),
+ ),
+ },
+ properties: TextureProperties::default(),
+ });
+
+ white_1x1_tex_obj_id
+}
+
+#[derive(Debug)]
+enum ShadersCreationStatus<'shader_ctx>
+{
+ ProcessingRequired,
+ Ready(ReadyShaders<'shader_ctx>),
+}
+
+#[derive(Debug)]
+struct ReadyShaders<'shader_ctx>
+{
+ main_3d_shader_obj_id: ObjectId,
+ main_3d_shader_program: &'shader_ctx ShaderProgram,
+ sky_box_shader_obj_id: ObjectId,
+ sky_box_shader_program: &'shader_ctx ShaderProgram,
+}
+
+fn get_or_create_shaders<'shader_ctx>(
+ assets: &mut Assets,
+ shader_context: &'shader_ctx ShaderContext,
+ object_store: &mut ObjectStore,
+ render_pass: &mut RenderPass,
+) -> Result<ShadersCreationStatus<'shader_ctx>, crate::Error>
+{
+ let main_3d_shader_asset = assets
+ .get_handle_to_loaded::<ShaderModuleSource>(MAIN_3D_SHADER_ASSET_LABEL.clone())
+ .or_else(|| {
+ assets.store_with_label(
+ MAIN_3D_SHADER_ASSET_LABEL.clone(),
+ ShaderModuleSource {
+ name: "main_3d_shader.slang".into(),
+ file_path: Path::new("@engine/main_3d_shader").into(),
+ source: include_str!("../../res/main_3d_shader.slang").into(),
+ link_entrypoints: ShaderEntrypointFlags::VERTEX
+ | ShaderEntrypointFlags::FRAGMENT,
+ },
+ );
+
+ None
+ });
+
+ let sky_box_shader_asset = assets
+ .get_handle_to_loaded::<ShaderModuleSource>(SKY_BOX_SHADER_ASSET_LABEL.clone())
+ .or_else(|| {
+ assets.store_with_label(
+ SKY_BOX_SHADER_ASSET_LABEL.clone(),
+ ShaderModuleSource {
+ name: "sky_box_shader.slang".into(),
+ file_path: Path::new("@engine/sky_box_shader").into(),
+ source: include_str!("../../res/sky_box_shader.slang").into(),
+ link_entrypoints: ShaderEntrypointFlags::VERTEX
+ | ShaderEntrypointFlags::FRAGMENT,
+ },
+ );
+ None
+ });
+
+ let (Some(main_3d_shader_asset), Some(sky_box_shader_asset)) =
+ (main_3d_shader_asset, sky_box_shader_asset)
+ else {
+ return Ok(ShadersCreationStatus::ProcessingRequired);
+ };
+
+ let main_3d_shader_program = shader_context.get_program(&main_3d_shader_asset.id());
+
+ let main_3d_shader_program = main_3d_shader_program.ok_or_else(|| {
+ error!("Shader context doesn't have a program for main 3D shader")
+ })?;
+
+ let main_3d_shader_obj_id = ObjectId::Asset(main_3d_shader_asset.id());
+
+ if !object_store.contains_maybe_pending_with_id(&main_3d_shader_obj_id) {
+ object_store.insert_pending(main_3d_shader_obj_id);
+
+ render_pass.commands.push(Command::CreateShaderProgram(
+ main_3d_shader_obj_id,
+ main_3d_shader_program.clone(),
+ ));
+ }
+
+ let sky_box_shader_program = shader_context.get_program(&sky_box_shader_asset.id());
+
+ let sky_box_shader_program = sky_box_shader_program.ok_or_else(|| {
+ error!("Shader context doesn't have a program for sky box shader")
+ })?;
+
+ let sky_box_shader_obj_id = ObjectId::Asset(sky_box_shader_asset.id());
+
+ if !object_store.contains_maybe_pending_with_id(&sky_box_shader_obj_id) {
+ object_store.insert_pending(sky_box_shader_obj_id);
+
+ render_pass.commands.push(Command::CreateShaderProgram(
+ sky_box_shader_obj_id,
+ sky_box_shader_program.clone(),
+ ));
+ }
+
+ Ok(ShadersCreationStatus::Ready(ReadyShaders {
+ main_3d_shader_obj_id,
+ main_3d_shader_program,
+ sky_box_shader_obj_id,
+ sky_box_shader_program,
+ }))
+}
+
+fn add_renderable_creation_commands(
+ assets: &Assets,
+ object_store: &mut ObjectStore,
+ render_pass: &mut RenderPass,
+ model: &Model,
+)
+{
+ let Some(model_spec) = assets.get(&model.spec_asset) else {
+ return;
+ };
+
+ let Some(mesh_asset) = &model_spec.mesh_asset else {
+ return;
+ };
+
+ if assets.get(mesh_asset).is_none() {
+ return;
+ }
+
+ debug_assert!(model_spec.materials.len() <= 1);
+
+ let model_material = match model_spec.find_first_material(&assets) {
+ MaterialSearchResult::Found(model_material_asset) => {
+ let Some(model_material) = assets.get(&model_material_asset) else {
+ return;
+ };
+
+ model_material
+ }
+ MaterialSearchResult::NotFound => {
+ return;
+ }
+ MaterialSearchResult::NoMaterials => &Material::builder().build(),
+ };
+
+ for texture_asset in model_material.textures() {
+ let Some(texture) = assets.get(texture_asset) else {
+ return;
+ };
+
+ let Texture::Texture2D(texture) = texture else {
+ tracing::error!(
+ texture_asset_id = ?texture_asset.id(),
+ texture_asset_label = ?assets.get_label(texture_asset),
+ "Material texture map is not 2D"
+ );
+ return;
+ };
+
+ let texture_object_id = ObjectId::Asset(texture_asset.id());
+
+ if object_store.contains_maybe_pending_with_id(&texture_object_id) {
+ return;
+ }
+
+ let Some(tex_pixel_data_format) =
+ TexturePixelDataFormat::for_image(&texture.image)
+ else {
+ tracing::error!("No texture pixel data format is available for image");
+ return;
+ };
+
+ object_store.insert_pending(texture_object_id);
+
+ render_pass.commands.push(Command::CreateTexture {
+ obj_id: texture_object_id,
+ pixel_data_format: tex_pixel_data_format,
+ creation: TextureCreation::Texture2D {
+ size: texture.image.dimensions(),
+ image: Some(texture.image.as_bytes().to_vec().into_boxed_slice()),
+ },
+ properties: texture.properties.clone(),
+ });
+ }
+
+ if !object_store.contains_maybe_pending_with_id(&ObjectId::Asset(mesh_asset.id())) {
+ object_store.insert_pending(ObjectId::Asset(mesh_asset.id()));
+
+ render_pass.commands.push(Command::CreateMesh {
+ obj_id: ObjectId::Asset(mesh_asset.id()),
+ mesh: AssetOrValue::Asset(mesh_asset.clone()),
+ usage: MeshUsage::Static,
+ });
+ }
+}
+
+fn add_set_3d_shader_point_light_bindings(
+ render_pass: &mut RenderPass,
+ point_light_query: &Query<(
+ &PointLight,
+ &Transform,
+ Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>,
+ )>,
+ main_3d_shader_obj_id: ObjectId,
+ main_3d_shader_cursor: &ShaderCursor<'_>,
+) -> Result<(), crate::Error>
+{
+ let lighting_shader_cursor =
+ main_3d_shader_cursor.field("Uniforms").field("lighting");
+
+ render_pass
+ .commands
+ .reserve(1 + point_light_query.iter().count() * 6);
+
+ render_pass.commands.push(Command::SetShaderBinding(
+ main_3d_shader_obj_id,
+ lighting_shader_cursor.field("point_light_cnt").binding(
+ u32::try_from(point_light_query.iter().count())
+ .expect("Point light count does not fit in 32-bit unsigned integer")
+ .into(),
+ )?,
+ ));
+
+ for (point_light_index, (point_light, point_light_transform)) in
+ point_light_query.iter().enumerate()
+ {
+ let point_light_shader_cursor = lighting_shader_cursor
+ .field("point_lights")
+ .element(point_light_index);
+
+ let phong_shader_cursor = point_light_shader_cursor.field("phong");
+
+ let attenuation_props_shader_cursor =
+ point_light_shader_cursor.field("attenuation_props");
+
+ render_pass.commands.extend(
+ [
+ phong_shader_cursor
+ .field("diffuse")
+ .binding(point_light.diffuse.to_rgb_lossy().into())?,
+ phong_shader_cursor
+ .field("specular")
+ .binding(point_light.specular.to_rgb_lossy().into())?,
+ point_light_shader_cursor.field("position").binding(
+ (point_light_transform.position + point_light.local_position).into(),
+ )?,
+ attenuation_props_shader_cursor
+ .field("constant")
+ .binding(point_light.attenuation_params.constant.into())?,
+ attenuation_props_shader_cursor
+ .field("linear")
+ .binding(point_light.attenuation_params.linear.into())?,
+ attenuation_props_shader_cursor
+ .field("quadratic")
+ .binding(point_light.attenuation_params.quadratic.into())?,
+ ]
+ .map(|binding| Command::SetShaderBinding(main_3d_shader_obj_id, binding)),
+ );
+ }
+
+ Ok(())
+}
+
+fn add_set_3d_shader_dir_lights_bindings(
+ render_pass: &mut RenderPass,
+ directional_light_query: &Query<(
+ &DirectionalLight,
+ Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>,
+ )>,
+ main_3d_shader_obj_id: ObjectId,
+ main_3d_shader_cursor: &ShaderCursor<'_>,
+) -> Result<(), crate::Error>
+{
+ let lighting_shader_cursor =
+ main_3d_shader_cursor.field("Uniforms").field("lighting");
+
+ render_pass
+ .commands
+ .reserve(1 + directional_light_query.iter().count() * 3);
+
+ render_pass.commands.push(Command::SetShaderBinding(
+ main_3d_shader_obj_id,
+ lighting_shader_cursor
+ .field("directional_light_cnt")
+ .binding(
+ u32::try_from(directional_light_query.iter().count())
+ .expect(
+ "Directional light count does not fit in 32-bit unsigned integer",
+ )
+ .into(),
+ )?,
+ ));
+
+ for (directional_light_index, (directional_light,)) in
+ directional_light_query.iter().enumerate()
+ {
+ let directional_light_shader_cursor = lighting_shader_cursor
+ .field("directional_lights")
+ .element(directional_light_index);
+
+ let phong_shader_cursor = directional_light_shader_cursor.field("phong");
+
+ render_pass.commands.extend(
+ [
+ phong_shader_cursor
+ .field("diffuse")
+ .binding(directional_light.diffuse.to_rgb_lossy().into())?,
+ phong_shader_cursor
+ .field("specular")
+ .binding(directional_light.specular.to_rgb_lossy().into())?,
+ directional_light_shader_cursor
+ .field("direction")
+ .binding(directional_light.direction.into())?,
+ ]
+ .map(|binding| Command::SetShaderBinding(main_3d_shader_obj_id, binding)),
+ );
+ }
+
+ Ok(())
+}
+
+fn add_set_3d_shader_camera_bindings(
+ render_pass: &mut RenderPass,
+ (camera, camera_transform): (&Camera, &Transform),
+ window: &Window,
+ main_3d_shader_obj_id: ObjectId,
+ main_3d_shader_cursor: &ShaderCursor<'_>,
+) -> Result<(), crate::Error>
+{
+ let model_3d_shader_cursor =
+ main_3d_shader_cursor.field("Uniforms").field("model_3d");
+
+ let lighting_shader_cursor =
+ main_3d_shader_cursor.field("Uniforms").field("lighting");
+
+ render_pass.commands.extend(
+ [
+ model_3d_shader_cursor
+ .field("view")
+ .binding(camera.to_view_matrix(camera_transform.position).into())?,
+ model_3d_shader_cursor.field("projection").binding(
+ camera
+ .projection
+ .to_matrix_rh(window.inner_size, ProjectionClipVolume::NegOneToOne)
+ .into(),
+ )?,
+ lighting_shader_cursor
+ .field("view_pos")
+ .binding(camera_transform.position.into())?,
+ ]
+ .into_iter()
+ .map(|binding| Command::SetShaderBinding(main_3d_shader_obj_id, binding)),
+ );
+
+ Ok(())
+}
+
+fn add_set_3d_shader_renderable_bindings(
+ render_pass: &mut RenderPass,
+ renderable: (&Material, Option<&MaterialFlags>, Option<&Transform>),
+ scene_env_lighting: Option<&EnvironmentalLighting>,
+ main_3d_shader_obj_id: ObjectId,
+ main_3d_shader_cursor: &ShaderCursor<'_>,
+ white_1x1_tex_obj_id: ObjectId,
+) -> Result<(), crate::Error>
+{
+ let (material, material_flags, transform) = renderable;
+
+ let transform = match transform.as_deref() {
+ Some(transform) => transform,
+ None => &Transform::default(),
+ };
+
+ let model_matrix = transform.to_matrix();
+ let inverted_model_matrix = model_matrix.inverse();
+
+ let material_flags = material_flags
+ .as_deref()
+ .unwrap_or(&const { MaterialFlags::builder().build() });
+
+ let env_lighting = match &scene_env_lighting {
+ Some(env_lighting) => &env_lighting,
+ None => &EnvironmentalLighting::default(),
+ };
+
+ let model_3d_shader_cursor =
+ main_3d_shader_cursor.field("Uniforms").field("model_3d");
+
+ let material_shader_cursor = main_3d_shader_cursor
+ .field("Uniforms")
+ .field("lighting")
+ .field("material");
+
+ let diffuse_map_obj_id = material
+ .diffuse_map
+ .as_ref()
+ .map(|diffuse_map| ObjectId::Asset(diffuse_map.id()))
+ .unwrap_or(white_1x1_tex_obj_id);
+
+ render_pass.commands.extend(
+ [
+ model_3d_shader_cursor
+ .field("model")
+ .binding(model_matrix.into())?,
+ model_3d_shader_cursor
+ .field("model_inverted")
+ .binding(inverted_model_matrix.into())?,
+ material_shader_cursor.field("ambient").binding(
+ material_flags
+ .use_ambient_color
+ .then_some(&material.ambient)
+ .unwrap_or(&env_lighting.ambient_color)
+ .to_rgb_lossy()
+ .into(),
+ )?,
+ material_shader_cursor
+ .field("diffuse")
+ .binding(material.diffuse.to_rgb_lossy().into())?,
+ material_shader_cursor
+ .field("specular")
+ .binding(material.specular.to_rgb_lossy().into())?,
+ main_3d_shader_cursor.field("ambient_map").binding(
+ ShaderBindingValue::Texture(
+ material
+ .ambient_map
+ .as_ref()
+ .map(|ambient_map| ObjectId::Asset(ambient_map.id()))
+ .unwrap_or(diffuse_map_obj_id),
+ ShaderBindingTextureKind::Texture2D,
+ ),
+ )?,
+ main_3d_shader_cursor.field("diffuse_map").binding(
+ ShaderBindingValue::Texture(
+ diffuse_map_obj_id,
+ ShaderBindingTextureKind::Texture2D,
+ ),
+ )?,
+ main_3d_shader_cursor.field("specular_map").binding(
+ ShaderBindingValue::Texture(
+ material
+ .specular_map
+ .as_ref()
+ .map(|specular_map| ObjectId::Asset(specular_map.id()))
+ .unwrap_or(white_1x1_tex_obj_id),
+ ShaderBindingTextureKind::Texture2D,
+ ),
+ )?,
+ material_shader_cursor
+ .field("shininess")
+ .binding(material.shininess.into())?,
+ ]
+ .into_iter()
+ .map(|binding| Command::SetShaderBinding(main_3d_shader_obj_id, binding)),
+ );
+
+ Ok(())
+}
+
+fn add_sky_box_commands(
+ render_pass: &mut RenderPass,
+ sky_box_ids: &SkyBoxIds,
+ camera: &Camera,
+ camera_transform: &Transform,
+ window: &Window,
+ sky_box_shader_obj_id: ObjectId,
+ sky_box_shader_program: &ShaderProgram,
+) -> Result<(), crate::Error>
+{
+ render_pass
+ .commands
+ .push(Command::ActivateShader(sky_box_shader_obj_id));
+
+ let sky_box_shader_cursor = ShaderCursor::new(
+ sky_box_shader_program
+ .reflection(0)
+ .unwrap()
+ .global_params_var_layout()
+ .unwrap(),
+ );
+
+ let mut view = camera.to_view_matrix(camera_transform.position);
+
+ view.translate(&Vec3 { x: 0.0, y: 0.0, z: 0.0 });
+
+ let sky_box_shader_bindings = [
+ sky_box_shader_cursor
+ .field("Uniforms")
+ .field("projection")
+ .binding(
+ (match camera.projection {
+ Projection::Perspective(_) => camera.projection.to_matrix_rh(
+ window.inner_size,
+ ProjectionClipVolume::NegOneToOne,
+ ),
+ Projection::Orthographic(_) => {
+ // A orthographic projection cannot be used for a sky box
+ Perspective::default().to_matrix_rh(
+ window.inner_size.width as f32
+ / window.inner_size.height as f32,
+ ProjectionClipVolume::NegOneToOne,
+ )
+ }
+ })
+ .into(),
+ )?,
+ sky_box_shader_cursor
+ .field("Uniforms")
+ .field("view")
+ .binding(view.into())?,
+ sky_box_shader_cursor.field("cube_texture").binding(
+ ShaderBindingValue::Texture(
+ sky_box_ids.texture_object,
+ ShaderBindingTextureKind::Cube,
+ ),
+ )?,
+ ];
+
+ for binding in sky_box_shader_bindings {
+ render_pass
+ .commands
+ .push(Command::SetShaderBinding(sky_box_shader_obj_id, binding));
+ }
+
+ render_pass.commands.push(Command::UpdateDrawProperties(
+ DrawProperties {
+ depth_function: DepthFunction::LessOrEqual,
+ ..Default::default()
+ },
+ DrawPropertiesUpdateFlags::DEPTH_FUNCTION,
+ ));
+
+ render_pass.commands.push(Command::DrawMesh(
+ sky_box_ids.mesh_object,
+ DrawMeshOptions::default(),
+ ));
+
+ Ok(())
+}
+
+fn load_sky_box(
+ scene_ent_id: Uid,
+ sky_box: &SkyBox,
+ sky_box_state: Option<&SkyBoxState>,
+ assets: &mut Assets,
+ object_store: &mut ObjectStore,
+ render_pass: &mut RenderPass,
+ actions: &mut Actions,
+) -> Result<Option<SkyBoxIds>, crate::Error>
+{
+ let mesh_object_id = match &sky_box_state {
+ Some(sky_box_state) => sky_box_state.mesh_object_id,
+ None => {
+ let sky_box_mesh =
+ Mesh::cube(Dimens3 { width: 1.0, height: 1.0, depth: 1.0 });
+
+ let mesh_object_id = ObjectId::new_sequential();
+
+ object_store.insert_pending(mesh_object_id);
+
+ render_pass.commands.push(Command::CreateMesh {
+ obj_id: mesh_object_id,
+ mesh: AssetOrValue::Value(sky_box_mesh),
+ usage: MeshUsage::Static,
+ });
+
+ mesh_object_id
+ }
+ };
+
+ let texture_object_id = match &sky_box {
+ SkyBox::AssetPerCubeMapFace(cube_map_part_assets) => {
+ if let Some(sky_box_state) = sky_box_state {
+ sky_box_state.texture_object_id
+ } else {
+ let Some(part_images) = cube_map_part_assets
+ .iter()
+ .map_while(|(face, part_texture_asset)| {
+ let part_texture = assets.get(part_texture_asset)?;
+
+ match part_texture {
+ Texture::Texture2D(part_texture) => {
+ Some((*face, &part_texture.image))
+ }
+ Texture::CubeMap(_) => {
+ tracing::warn!(
+ "Cube map texture cannot be used as cube map part"
+ );
+
+ None
+ }
+ }
+ })
+ .collect::<Vec<_>>()
+ .as_array::<6>()
+ .cloned()
+ else {
+ return Ok(None);
+ };
+
+ let texture_object_id = ObjectId::new_sequential();
+
+ object_store.insert_pending(texture_object_id);
+
+ render_pass.commands.push(Command::CreateTexture {
+ obj_id: texture_object_id,
+ pixel_data_format: TexturePixelDataFormat::for_image(
+ part_images[0].1,
+ )
+ .expect("No texture pixel data format is available for image"),
+ creation: TextureCreation::CubeMap {
+ size: part_images[0].1.dimensions(),
+ images: Some(part_images.map(|(face, image)| {
+ (face, image.as_bytes().to_vec().into_boxed_slice())
+ })),
+ },
+ properties: TextureProperties::builder()
+ .minifying_filter(TextureFiltering::Linear)
+ .magnifying_filter(TextureFiltering::Linear)
+ .wrap(TextureWrapping::ClampToEdge)
+ .build(),
+ });
+
+ texture_object_id
+ }
+ }
+ };
+
+ if sky_box_state.is_none() {
+ actions.add_components(
+ scene_ent_id,
+ (SkyBoxState { texture_object_id, mesh_object_id },),
+ );
+ }
+
+ Ok(Some(SkyBoxIds {
+ texture_object: texture_object_id,
+ mesh_object: mesh_object_id,
+ }))
+}
+
+#[derive(Debug, Component)]
+pub struct SkyBoxState
+{
+ texture_object_id: ObjectId,
+ mesh_object_id: ObjectId,
+}
diff --git a/engine/src/rendering/object.rs b/engine/src/rendering/object.rs
new file mode 100644
index 0000000..61e7ed5
--- /dev/null
+++ b/engine/src/rendering/object.rs
@@ -0,0 +1,137 @@
+use std::collections::HashMap;
+use std::fmt::Display;
+use std::sync::atomic::{AtomicU64, Ordering};
+
+use crate::asset::Id as AssetId;
+use crate::ecs::Sole;
+
+pub type RawValue = u32;
+
+/// Rendering object ID.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum Id
+{
+ Asset(AssetId),
+ Sequential(SequentialId),
+}
+
+impl Id
+{
+ pub fn new_sequential() -> Self
+ {
+ static NEXT_SEQUENTIAL_ID: AtomicU64 = AtomicU64::new(0);
+
+ Self::Sequential(SequentialId(
+ NEXT_SEQUENTIAL_ID.fetch_add(1, Ordering::Relaxed),
+ ))
+ }
+
+ pub fn into_asset_id(self) -> Option<AssetId>
+ {
+ match self {
+ Self::Asset(asset_id) => Some(asset_id),
+ Self::Sequential(_) => None,
+ }
+ }
+}
+
+impl From<AssetId> for Id
+{
+ fn from(asset_id: AssetId) -> Self
+ {
+ Self::Asset(asset_id)
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct SequentialId(u64);
+
+/// Rendering object store.
+#[derive(Debug, Default, Sole)]
+pub struct Store
+{
+ objects: HashMap<Id, Option<Object>>,
+}
+
+impl Store
+{
+ pub fn get_obj(&self, id: &Id) -> Option<&Object>
+ {
+ self.objects.get(id).and_then(|obj| obj.as_ref())
+ }
+
+ pub fn contains_maybe_pending_with_id(&self, id: &Id) -> bool
+ {
+ self.objects.contains_key(id)
+ }
+
+ pub fn contains_non_pending_with_id(&self, id: &Id) -> bool
+ {
+ self.objects.get(id).and_then(|obj| obj.as_ref()).is_some()
+ }
+
+ pub fn insert(&mut self, id: Id, object: Object)
+ {
+ self.objects.insert(id, Some(object));
+ }
+
+ pub fn insert_pending(&mut self, id: Id)
+ {
+ self.objects.insert(id, None);
+ }
+
+ pub fn remove(&mut self, id: &Id) -> Option<Option<Object>>
+ {
+ self.objects.remove(id)
+ }
+}
+
+/// Rendering object.
+#[derive(Debug, Clone)]
+pub struct Object
+{
+ raw: RawValue,
+ kind: Kind,
+}
+
+impl Object
+{
+ pub fn from_raw(raw: RawValue, kind: Kind) -> Self
+ {
+ Self { raw, kind }
+ }
+
+ pub fn as_raw(&self) -> RawValue
+ {
+ self.raw
+ }
+
+ pub fn kind(&self) -> Kind
+ {
+ self.kind
+ }
+}
+
+/// Rendering object kind.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum Kind
+{
+ Texture,
+ ShaderProgram,
+ Mesh,
+ Framebuffer,
+}
+
+impl Display for Kind
+{
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
+ {
+ formatter.write_str(match self {
+ Self::Texture => "texture",
+ Self::ShaderProgram => "shader program",
+ Self::Mesh => "mesh",
+ Self::Framebuffer => "framebuffer",
+ })
+ }
+}
diff --git a/engine/src/rendering/shader.rs b/engine/src/rendering/shader.rs
new file mode 100644
index 0000000..3f07381
--- /dev/null
+++ b/engine/src/rendering/shader.rs
@@ -0,0 +1,1404 @@
+use std::any::type_name;
+use std::borrow::Cow;
+use std::collections::{HashMap, HashSet};
+use std::fmt::{Debug, Display, Write};
+use std::path::Path;
+use std::str::Utf8Error;
+use std::sync::Arc;
+
+use bitflags::{bitflags, bitflags_match};
+use ecs::phase::INIT as INIT_PHASE;
+use shader_slang::{
+ Blob as SlangBlob,
+ ComponentType as SlangComponentType,
+ DebugInfoLevel as SlangDebugInfoLevel,
+ EntryPoint as SlangEntryPoint,
+ GlobalSession as SlangGlobalSession,
+ Module as SlangModule,
+ ParameterCategory as SlangParameterCategory,
+ ScalarType as SlangScalarType,
+ Session as SlangSession,
+ TypeKind as SlangTypeKind,
+};
+
+use crate::asset::{
+ Assets,
+ Event as AssetEvent,
+ Handle as AssetHandle,
+ Id as AssetId,
+ Submitter as AssetSubmitter,
+ HANDLE_ASSETS_PHASE,
+};
+use crate::builder;
+use crate::ecs::pair::ChildOf;
+use crate::ecs::phase::Phase;
+use crate::ecs::sole::Single;
+use crate::ecs::{declare_entity, pair, Sole};
+use crate::mesh::vertex_buffer::VertexLabel;
+
+pub mod cursor;
+
+pub const STD_VERT_IN_SEM_NAME_POSITION: &str = "STD_POSITION";
+pub const STD_VERT_IN_SEM_NAME_NORMAL: &str = "STD_NORMAL";
+pub const STD_VERT_IN_SEM_NAME_UV_FROM_TOP_LEFT: &str = "STD_UV_FROM_TOP_LEFT";
+pub const STD_VERT_IN_SEM_NAME_COLOR: &str = "STD_COLOR";
+
+/// Shader module.
+#[derive(Debug)]
+pub struct ModuleSource
+{
+ pub name: Cow<'static, str>,
+ pub file_path: Cow<'static, Path>,
+ pub source: Cow<'static, str>,
+ pub link_entrypoints: EntrypointFlags,
+}
+
+bitflags! {
+ #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
+ pub struct EntrypointFlags: usize
+ {
+ const FRAGMENT = 1 << 0;
+ const VERTEX = 1 << 1;
+ }
+}
+
+#[derive(Clone)]
+pub struct Module
+{
+ inner: SlangModule,
+}
+
+impl Module
+{
+ pub fn entry_points(&self) -> impl ExactSizeIterator<Item = EntryPoint> + use<'_>
+ {
+ self.inner
+ .entry_points()
+ .map(|entry_point| EntryPoint { inner: entry_point })
+ }
+
+ pub fn get_entry_point(&self, entry_point: &str) -> Option<EntryPoint>
+ {
+ let entry_point = self.inner.find_entry_point_by_name(entry_point)?;
+
+ Some(EntryPoint { inner: entry_point })
+ }
+
+ pub fn file_path(&self) -> &str
+ {
+ self.inner.file_path()
+ }
+}
+
+pub struct EntryPoint
+{
+ inner: SlangEntryPoint,
+}
+
+impl EntryPoint
+{
+ pub fn function(&self) -> FunctionReflection<'_>
+ {
+ FunctionReflection {
+ inner: self.inner.function_reflection(),
+ }
+ }
+}
+
+pub struct FunctionReflection<'a>
+{
+ inner: &'a shader_slang::reflection::Function,
+}
+
+impl<'a> FunctionReflection<'a>
+{
+ pub fn name(&self) -> Option<&str>
+ {
+ self.inner.name()
+ }
+}
+
+pub struct EntryPointReflection<'a>
+{
+ inner: &'a shader_slang::reflection::EntryPoint,
+}
+
+impl<'a> EntryPointReflection<'a>
+{
+ pub fn name(&self) -> Option<&str>
+ {
+ self.inner.name()
+ }
+
+ pub fn name_override(&self) -> Option<&str>
+ {
+ self.inner.name_override()
+ }
+
+ pub fn stage(&self) -> Stage
+ {
+ Stage::from_slang_stage(self.inner.stage())
+ }
+
+ pub fn parameters(&self) -> impl ExactSizeIterator<Item = VariableLayout<'a>>
+ {
+ self.inner
+ .parameters()
+ .map(|param| VariableLayout { inner: param })
+ }
+
+ pub fn var_layout(&self) -> Option<VariableLayout<'a>>
+ {
+ Some(VariableLayout { inner: self.inner.var_layout()? })
+ }
+}
+
+#[derive(Clone)]
+pub struct Program
+{
+ inner: SlangComponentType,
+ metadata: ProgramMetadata,
+}
+
+impl Program
+{
+ pub fn into_linked(self) -> Result<Program, Error>
+ {
+ let linked_program = self.inner.link()?;
+
+ Ok(Program {
+ inner: linked_program,
+ metadata: self.metadata,
+ })
+ }
+
+ pub fn metadata(&self) -> &ProgramMetadata
+ {
+ &self.metadata
+ }
+
+ pub fn get_entry_point_code(&self, entry_point_index: u32) -> Result<Blob, Error>
+ {
+ let blob = self.inner.entry_point_code(entry_point_index.into(), 0)?;
+
+ Ok(Blob { inner: blob })
+ }
+
+ pub fn reflection(&self, target: u32) -> Result<ProgramReflection<'_>, Error>
+ {
+ let reflection = self.inner.layout(target as i64)?;
+
+ Ok(ProgramReflection { inner: reflection })
+ }
+}
+
+impl Debug for Program
+{
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
+ {
+ formatter
+ .debug_struct(type_name::<Self>())
+ .finish_non_exhaustive()
+ }
+}
+
+pub struct ProgramReflection<'a>
+{
+ inner: &'a shader_slang::reflection::Shader,
+}
+
+impl<'a> ProgramReflection<'a>
+{
+ pub fn get_entry_point_by_index(&self, index: u32)
+ -> Option<EntryPointReflection<'a>>
+ {
+ Some(EntryPointReflection {
+ inner: self.inner.entry_point_by_index(index)?,
+ })
+ }
+
+ pub fn get_entry_point_by_name(&self, name: &str)
+ -> Option<EntryPointReflection<'a>>
+ {
+ Some(EntryPointReflection {
+ inner: self.inner.find_entry_point_by_name(name)?,
+ })
+ }
+
+ pub fn entry_points(
+ &self,
+ ) -> impl ExactSizeIterator<Item = EntryPointReflection<'a>> + use<'a>
+ {
+ self.inner
+ .entry_points()
+ .map(|entry_point| EntryPointReflection { inner: entry_point })
+ }
+
+ pub fn global_params_type_layout(&self) -> Option<TypeLayout<'a>>
+ {
+ Some(TypeLayout {
+ inner: self.inner.global_params_type_layout()?,
+ })
+ }
+
+ pub fn global_params_var_layout(&self) -> Option<VariableLayout<'a>>
+ {
+ Some(VariableLayout {
+ inner: self.inner.global_params_var_layout()?,
+ })
+ }
+
+ pub fn get_type(&self, name: &str) -> Option<TypeReflection<'a>>
+ {
+ Some(TypeReflection {
+ inner: self.inner.find_type_by_name(name)?,
+ })
+ }
+
+ pub fn get_type_layout(&self, ty: &TypeReflection<'a>) -> Option<TypeLayout<'a>>
+ {
+ Some(TypeLayout {
+ inner: self
+ .inner
+ .type_layout(&ty.inner, shader_slang::LayoutRules::Default)?,
+ })
+ }
+}
+
+#[derive(Clone, Copy)]
+pub struct VariableLayout<'a>
+{
+ inner: &'a shader_slang::reflection::VariableLayout,
+}
+
+impl<'a> VariableLayout<'a>
+{
+ pub fn name(&self) -> Option<&'a str>
+ {
+ self.inner.name()
+ }
+
+ pub fn semantic_name(&self) -> Option<&str>
+ {
+ self.inner.semantic_name()
+ }
+
+ pub fn binding_index(&self) -> u32
+ {
+ self.inner
+ .offset(shader_slang::ParameterCategory::DescriptorTableSlot) as u32
+
+ // self.inner.binding_index()
+ }
+
+ pub fn varying_input_offset(&self) -> Option<usize>
+ {
+ if !self
+ .inner
+ .categories()
+ .any(|category| category == SlangParameterCategory::VaryingInput)
+ {
+ return None;
+ }
+
+ Some(self.inner.offset(SlangParameterCategory::VaryingInput))
+ }
+
+ pub fn binding_space(&self) -> u32
+ {
+ self.inner.binding_space()
+ }
+
+ pub fn semantic_index(&self) -> usize
+ {
+ self.inner.semantic_index()
+ }
+
+ pub fn offset(&self) -> usize
+ {
+ self.inner.offset(shader_slang::ParameterCategory::Uniform)
+ }
+
+ pub fn ty(&self) -> Option<TypeReflection<'a>>
+ {
+ self.inner.ty().map(|ty| TypeReflection { inner: ty })
+ }
+
+ pub fn type_layout(&self) -> Option<TypeLayout<'a>>
+ {
+ Some(TypeLayout { inner: self.inner.type_layout()? })
+ }
+}
+
+#[derive(Clone, Copy)]
+pub struct TypeLayout<'a>
+{
+ inner: &'a shader_slang::reflection::TypeLayout,
+}
+
+impl<'a> TypeLayout<'a>
+{
+ pub fn kind(&self) -> TypeKind
+ {
+ TypeKind::from_slang_type_kind(self.inner.kind())
+ }
+
+ pub fn scalar_type(&self) -> Option<ScalarType>
+ {
+ Some(ScalarType::from_slang_scalar_type(
+ self.inner.scalar_type()?,
+ ))
+ }
+
+ pub fn resource_shape(&self) -> Option<ResourceShape>
+ {
+ Some(ResourceShape::from_bits_retain(
+ self.inner.resource_shape()? as u32,
+ ))
+ }
+
+ pub fn get_field_by_name(&self, name: &str) -> Option<VariableLayout<'a>>
+ {
+ let index = self.inner.find_field_index_by_name(name);
+
+ if index < 0 {
+ return None;
+ }
+
+ let index = u32::try_from(index.cast_unsigned()).expect("Should not happend");
+
+ let field = self.inner.field_by_index(index)?;
+
+ Some(VariableLayout { inner: field })
+ }
+
+ pub fn parameter_category(&self) -> ParameterCategory
+ {
+ ParameterCategory::from_slang_parameter_category(self.inner.parameter_category())
+ }
+
+ pub fn binding_range_descriptor_set_index(&self, index: i64) -> i64
+ {
+ self.inner.binding_range_descriptor_set_index(index)
+ }
+
+ pub fn get_field_binding_range_offset_by_name(&self, name: &str) -> Option<u64>
+ {
+ let field_index = self.inner.find_field_index_by_name(name);
+
+ if field_index < 0 {
+ return None;
+ }
+
+ let field_binding_range_offset =
+ self.inner.field_binding_range_offset(field_index);
+
+ if field_binding_range_offset < 0 {
+ return None;
+ }
+
+ Some(field_binding_range_offset.cast_unsigned())
+ }
+
+ pub fn ty(&self) -> Option<TypeReflection<'a>>
+ {
+ self.inner.ty().map(|ty| TypeReflection { inner: ty })
+ }
+
+ pub fn fields(&self) -> FieldIter<'a>
+ {
+ FieldIter {
+ type_layout: self.clone(),
+ cnt: self.field_cnt(),
+ index: 0,
+ }
+ }
+
+ pub fn field_cnt(&self) -> u32
+ {
+ self.inner.field_count()
+ }
+
+ pub fn element_cnt(&self) -> Option<usize>
+ {
+ self.inner.element_count()
+ }
+
+ pub fn row_cnt(&self) -> Option<u32>
+ {
+ self.inner.row_count()
+ }
+
+ pub fn column_cnt(&self) -> Option<u32>
+ {
+ self.inner.column_count()
+ }
+
+ pub fn element_type_layout(&self) -> Option<TypeLayout<'a>>
+ {
+ self.inner
+ .element_type_layout()
+ .map(|type_layout| TypeLayout { inner: type_layout })
+ }
+
+ pub fn element_var_layout(&self) -> Option<VariableLayout<'a>>
+ {
+ self.inner
+ .element_var_layout()
+ .map(|var_layout| VariableLayout { inner: var_layout })
+ }
+
+ pub fn container_var_layout(&self) -> Option<VariableLayout<'a>>
+ {
+ self.inner
+ .container_var_layout()
+ .map(|var_layout| VariableLayout { inner: var_layout })
+ }
+
+ pub fn uniform_size(&self) -> Option<usize>
+ {
+ // tracing::debug!(
+ // "uniform_size: {:?} categories: {:?}",
+ // self.inner.name(),
+ // self.inner.categories().collect::<Vec<_>>(),
+ // );
+
+ if !self
+ .inner
+ .categories()
+ .any(|category| category == SlangParameterCategory::Uniform)
+ {
+ return None;
+ }
+
+ // let category = self.inner.categories().next().unwrap();
+
+ // println!(
+ // "AARGH size Category: {category:?} Category count: {}",
+ // self.inner.category_count()
+ // );
+
+ // Some(self.inner.size(category))
+
+ Some(self.inner.size(SlangParameterCategory::Uniform))
+ }
+
+ pub fn stride(&self) -> usize
+ {
+ self.inner.stride(self.inner.categories().next().unwrap())
+ }
+}
+
+pub struct FieldIter<'a>
+{
+ type_layout: TypeLayout<'a>,
+ cnt: u32,
+ index: u32,
+}
+
+impl<'a> Iterator for FieldIter<'a>
+{
+ type Item = VariableLayout<'a>;
+
+ fn next(&mut self) -> Option<Self::Item>
+ {
+ if self.index == self.cnt {
+ return None;
+ }
+
+ let Some(field) = self.type_layout.inner.field_by_index(self.index) else {
+ unreachable!();
+ };
+
+ self.index += 1;
+
+ Some(VariableLayout { inner: field })
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>)
+ {
+ let len = (self.cnt - self.index) as usize;
+
+ (len, Some(len))
+ }
+}
+
+impl ExactSizeIterator for FieldIter<'_>
+{
+ fn len(&self) -> usize
+ {
+ (self.cnt - self.index) as usize
+ }
+}
+
+impl DoubleEndedIterator for FieldIter<'_>
+{
+ fn next_back(&mut self) -> Option<Self::Item>
+ {
+ if self.cnt == 0 || self.index == self.cnt - 1 {
+ return None;
+ }
+
+ let Some(field) = self.type_layout.inner.field_by_index(self.cnt - 1) else {
+ unreachable!();
+ };
+
+ self.cnt -= 1;
+
+ Some(VariableLayout { inner: field })
+ }
+}
+
+pub struct TypeReflection<'a>
+{
+ inner: &'a shader_slang::reflection::Type,
+}
+
+impl TypeReflection<'_>
+{
+ pub fn kind(&self) -> TypeKind
+ {
+ TypeKind::from_slang_type_kind(self.inner.kind())
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[non_exhaustive]
+pub enum TypeKind
+{
+ None,
+ Struct,
+ Enum,
+ Array,
+ Matrix,
+ Vector,
+ Scalar,
+ ConstantBuffer,
+ Resource,
+ SamplerState,
+ TextureBuffer,
+ ShaderStorageBuffer,
+ ParameterBlock,
+ GenericTypeParameter,
+ Interface,
+ OutputStream,
+ MeshOutput,
+ Specialized,
+ Feedback,
+ Pointer,
+ DynamicResource,
+ Count,
+}
+
+impl TypeKind
+{
+ fn from_slang_type_kind(type_kind: SlangTypeKind) -> Self
+ {
+ match type_kind {
+ SlangTypeKind::None => Self::None,
+ SlangTypeKind::Struct => Self::Struct,
+ SlangTypeKind::Enum => Self::Enum,
+ SlangTypeKind::Array => Self::Array,
+ SlangTypeKind::Matrix => Self::Matrix,
+ SlangTypeKind::Vector => Self::Vector,
+ SlangTypeKind::Scalar => Self::Scalar,
+ SlangTypeKind::ConstantBuffer => Self::ConstantBuffer,
+ SlangTypeKind::Resource => Self::Resource,
+ SlangTypeKind::SamplerState => Self::SamplerState,
+ SlangTypeKind::TextureBuffer => Self::TextureBuffer,
+ SlangTypeKind::ShaderStorageBuffer => Self::ShaderStorageBuffer,
+ SlangTypeKind::ParameterBlock => Self::ParameterBlock,
+ SlangTypeKind::GenericTypeParameter => Self::GenericTypeParameter,
+ SlangTypeKind::Interface => Self::Interface,
+ SlangTypeKind::OutputStream => Self::OutputStream,
+ SlangTypeKind::MeshOutput => Self::MeshOutput,
+ SlangTypeKind::Specialized => Self::Specialized,
+ SlangTypeKind::Feedback => Self::Feedback,
+ SlangTypeKind::Pointer => Self::Pointer,
+ SlangTypeKind::DynamicResource => Self::DynamicResource,
+ SlangTypeKind::Count => Self::Count,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+#[non_exhaustive]
+pub enum ScalarType
+{
+ None,
+ Void,
+ Bool,
+ Int32,
+ Uint32,
+ Int64,
+ Uint64,
+ Float16,
+ Float32,
+ Float64,
+ Int8,
+ Uint8,
+ Int16,
+ Uint16,
+ Intptr,
+ Uintptr,
+ Bfloat16,
+ FloatE4m3,
+ FloatE5m2,
+}
+
+impl ScalarType
+{
+ fn from_slang_scalar_type(scalar_type: SlangScalarType) -> Self
+ {
+ match scalar_type {
+ SlangScalarType::None => Self::None,
+ SlangScalarType::Void => Self::Void,
+ SlangScalarType::Bool => Self::Bool,
+ SlangScalarType::Int32 => Self::Int32,
+ SlangScalarType::Uint32 => Self::Uint32,
+ SlangScalarType::Int64 => Self::Int64,
+ SlangScalarType::Uint64 => Self::Uint64,
+ SlangScalarType::Float16 => Self::Float16,
+ SlangScalarType::Float32 => Self::Float32,
+ SlangScalarType::Float64 => Self::Float64,
+ SlangScalarType::Int8 => Self::Int8,
+ SlangScalarType::Uint8 => Self::Uint8,
+ SlangScalarType::Int16 => Self::Int16,
+ SlangScalarType::Uint16 => Self::Uint16,
+ SlangScalarType::Intptr => Self::Intptr,
+ SlangScalarType::Uintptr => Self::Uintptr,
+ SlangScalarType::Bfloat16 => Self::Bfloat16,
+ SlangScalarType::FloatE4m3 => Self::FloatE4m3,
+ SlangScalarType::FloatE5m2 => Self::FloatE5m2,
+ #[allow(unreachable_patterns)]
+ _ => unimplemented!("conversion from slang scalar type"),
+ }
+ }
+}
+
+bitflags! {
+#[derive(Debug, Copy, Clone)]
+pub struct ResourceShape: u32 {
+ const BASE = shader_slang::ResourceShape::SlangResourceBaseShapeMask as u32;
+ const NONE = shader_slang::ResourceShape::SlangResourceNone as u32;
+ const TEXTURE_1D = shader_slang::ResourceShape::SlangTexture1d as u32;
+ const TEXTURE_2D = shader_slang::ResourceShape::SlangTexture2d as u32;
+ const TEXTURE_3D = shader_slang::ResourceShape::SlangTexture3d as u32;
+ const TEXTURE_CUBE = shader_slang::ResourceShape::SlangTextureCube as u32;
+ const TEXTURE_BUFFER = shader_slang::ResourceShape::SlangTextureBuffer as u32;
+ const STRUCTURED_BUFFER = shader_slang::ResourceShape::SlangStructuredBuffer as u32;
+ const BYTE_ADDRESS_BUFFER = shader_slang::ResourceShape::SlangByteAddressBuffer as u32;
+ const UNKNOWN = shader_slang::ResourceShape::SlangResourceUnknown as u32;
+ const ACCELERATION_STRUCTURE = shader_slang::ResourceShape::SlangAccelerationStructure as u32;
+ const TEXTURE_SUBPASS = shader_slang::ResourceShape::SlangTextureSubpass as u32;
+ const EXT_SHAPE = shader_slang::ResourceShape::SlangResourceExtShapeMask as u32;
+ const TEXTURE_FEEDBACK_FLAG = shader_slang::ResourceShape::SlangTextureFeedbackFlag as u32;
+ const TEXTURE_SHADOW_FLAG = shader_slang::ResourceShape::SlangTextureShadowFlag as u32;
+ const TEXTURE_ARRAY_FLAG = shader_slang::ResourceShape::SlangTextureArrayFlag as u32;
+ const TEXTURE_MULTISAMPLE_FLAG = shader_slang::ResourceShape::SlangTextureMultisampleFlag as u32;
+ const TEXTURE_COMBINED_FLAG = shader_slang::ResourceShape::SlangTextureCombinedFlag as u32;
+ const TEXTURE_1D_ARRAY = shader_slang::ResourceShape::SlangTexture1dArray as u32;
+ const TEXTURE_2D_ARRAY = shader_slang::ResourceShape::SlangTexture2dArray as u32;
+ const TEXTURE_CUBE_ARRAY = shader_slang::ResourceShape::SlangTextureCubeArray as u32;
+ const TEXTURE_2D_MULTISAMPLE = shader_slang::ResourceShape::SlangTexture2dMultisample as u32;
+ const TEXTURE_2D_MULTISAMPLE_ARRAY = shader_slang::ResourceShape::SlangTexture2dMultisampleArray as u32;
+ const TEXTURE_SUBPASS_MULTISAMPLE = shader_slang::ResourceShape::SlangTextureSubpassMultisample as u32;
+}
+}
+
+#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
+pub enum ParameterCategory
+{
+ None,
+ Mixed,
+ ConstantBuffer,
+ ShaderResource,
+ UnorderedAccess,
+ VaryingInput,
+ VaryingOutput,
+ SamplerState,
+ Uniform,
+ DescriptorTableSlot,
+ SpecializationConstant,
+ PushConstantBuffer,
+ RegisterSpace,
+ Generic,
+ RayPayload,
+ HitAttributes,
+ CallablePayload,
+ ShaderRecord,
+ ExistentialTypeParam,
+ ExistentialObjectParam,
+ SubElementRegisterSpace,
+ Subpass,
+ MetalArgumentBufferElement,
+ MetalAttribute,
+ MetalPayload,
+ Count,
+}
+
+impl ParameterCategory
+{
+ fn from_slang_parameter_category(parameter_category: SlangParameterCategory) -> Self
+ {
+ match parameter_category {
+ SlangParameterCategory::None => Self::None,
+ SlangParameterCategory::Mixed => Self::Mixed,
+ SlangParameterCategory::ConstantBuffer => Self::ConstantBuffer,
+ SlangParameterCategory::ShaderResource => Self::ShaderResource,
+ SlangParameterCategory::UnorderedAccess => Self::UnorderedAccess,
+ SlangParameterCategory::VaryingInput => Self::VaryingInput,
+ SlangParameterCategory::VaryingOutput => Self::VaryingOutput,
+ SlangParameterCategory::SamplerState => Self::SamplerState,
+ SlangParameterCategory::Uniform => Self::Uniform,
+ SlangParameterCategory::DescriptorTableSlot => Self::DescriptorTableSlot,
+ SlangParameterCategory::SpecializationConstant => {
+ Self::SpecializationConstant
+ }
+ SlangParameterCategory::PushConstantBuffer => Self::PushConstantBuffer,
+ SlangParameterCategory::RegisterSpace => Self::RegisterSpace,
+ SlangParameterCategory::Generic => Self::Generic,
+ SlangParameterCategory::RayPayload => Self::RayPayload,
+ SlangParameterCategory::HitAttributes => Self::HitAttributes,
+ SlangParameterCategory::CallablePayload => Self::CallablePayload,
+ SlangParameterCategory::ShaderRecord => Self::ShaderRecord,
+ SlangParameterCategory::ExistentialTypeParam => Self::ExistentialTypeParam,
+ SlangParameterCategory::ExistentialObjectParam => {
+ Self::ExistentialObjectParam
+ }
+ SlangParameterCategory::SubElementRegisterSpace => {
+ Self::SubElementRegisterSpace
+ }
+ SlangParameterCategory::Subpass => Self::Subpass,
+ SlangParameterCategory::MetalArgumentBufferElement => {
+ Self::MetalArgumentBufferElement
+ }
+ SlangParameterCategory::MetalAttribute => Self::MetalAttribute,
+ SlangParameterCategory::MetalPayload => Self::MetalPayload,
+ SlangParameterCategory::Count => Self::Count,
+ }
+ }
+}
+
+pub struct Blob
+{
+ inner: SlangBlob,
+}
+
+impl Blob
+{
+ pub fn as_bytes(&self) -> &[u8]
+ {
+ self.inner.as_slice()
+ }
+
+ pub fn as_str(&self) -> Result<&str, Utf8Error>
+ {
+ self.inner.as_str()
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[non_exhaustive]
+pub enum Stage
+{
+ None,
+ Vertex,
+ Hull,
+ Domain,
+ Geometry,
+ Fragment,
+ Compute,
+ RayGeneration,
+ Intersection,
+ AnyHit,
+ ClosestHit,
+ Miss,
+ Callable,
+ Mesh,
+ Amplification,
+ Dispatch,
+ Count,
+}
+
+impl Stage
+{
+ fn from_slang_stage(stage: shader_slang::Stage) -> Self
+ {
+ match stage {
+ shader_slang::Stage::None => Self::None,
+ shader_slang::Stage::Vertex => Self::Vertex,
+ shader_slang::Stage::Hull => Self::Hull,
+ shader_slang::Stage::Domain => Self::Domain,
+ shader_slang::Stage::Geometry => Self::Geometry,
+ shader_slang::Stage::Fragment => Self::Fragment,
+ shader_slang::Stage::Compute => Self::Compute,
+ shader_slang::Stage::RayGeneration => Self::RayGeneration,
+ shader_slang::Stage::Intersection => Self::Intersection,
+ shader_slang::Stage::AnyHit => Self::AnyHit,
+ shader_slang::Stage::ClosestHit => Self::ClosestHit,
+ shader_slang::Stage::Miss => Self::Miss,
+ shader_slang::Stage::Callable => Self::Callable,
+ shader_slang::Stage::Mesh => Self::Mesh,
+ shader_slang::Stage::Amplification => Self::Amplification,
+ shader_slang::Stage::Dispatch => Self::Dispatch,
+ shader_slang::Stage::Count => Self::Count,
+ }
+ }
+}
+
+builder! {
+#[builder(name = SettingsBuilder, derives=(Debug))]
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct Settings
+{
+ link_entrypoints: EntrypointFlags,
+}
+}
+
+#[derive(Sole)]
+pub struct Context
+{
+ _global_session: SlangGlobalSession,
+ session: SlangSession,
+ modules: HashMap<AssetId, Module>,
+ programs: HashMap<AssetId, Program>,
+}
+
+impl Context
+{
+ pub fn get_module(&self, asset_id: &AssetId) -> Option<&Module>
+ {
+ self.modules.get(asset_id)
+ }
+
+ pub fn get_program(&self, asset_id: &AssetId) -> Option<&Program>
+ {
+ self.programs.get(asset_id)
+ }
+
+ #[tracing::instrument(skip_all, fields(module_file = module.file_path()))]
+ pub fn compose_into_program(
+ &self,
+ module: Module,
+ link_entrypoints: EntrypointFlags,
+ ) -> Result<Program, ComposeProgramError>
+ {
+ let entry_points = link_entrypoints
+ .iter()
+ .filter_map(|entrypoint_flag| {
+ let entrypoint_name = bitflags_match!(entrypoint_flag, {
+ EntrypointFlags::VERTEX => Some("vertex_main"),
+ EntrypointFlags::FRAGMENT => Some("fragment_main"),
+ _ => None
+ })?;
+
+ let Some(entry_point) = module.get_entry_point(entrypoint_name) else {
+ return Some(Err(ComposeProgramError::EntrypointNotFoundError {
+ entrypoint_name,
+ }));
+ };
+
+ Some(Ok(entry_point))
+ })
+ .collect::<Result<Vec<_>, _>>()?;
+
+ let components = entry_points
+ .into_iter()
+ .map(|entry_point| SlangComponentType::from(entry_point.inner))
+ .chain([SlangComponentType::from(module.inner)])
+ .collect::<Vec<_>>();
+
+ let program = self
+ .session
+ .create_composite_component_type(&components)
+ .map_err(|err| ComposeProgramError::Other(Error(err)))?;
+
+ let vertex_desc = if link_entrypoints.contains(EntrypointFlags::VERTEX) {
+ // TODO: Do not have hard coded target here
+ let Ok(program_reflection) = program.layout(0) else {
+ unreachable!();
+ };
+
+ let program_reflection = ProgramReflection { inner: program_reflection };
+
+ Some(VertexDescription::new(
+ &program_reflection
+ .get_entry_point_by_name("vertex_main")
+ .expect("Not possible"),
+ )?)
+ } else {
+ None
+ };
+
+ Ok(Program {
+ inner: program,
+ metadata: ProgramMetadata { vertex_desc },
+ })
+ }
+}
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct ProgramMetadata
+{
+ /// If the program has a entry point in the vertex stage, this field will contain a
+ /// description of the vertex type passed to the entry point.
+ pub vertex_desc: Option<VertexDescription>,
+}
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct VertexDescription
+{
+ pub inputs: Arc<[VertexInputDescription]>,
+}
+
+impl VertexDescription
+{
+ #[tracing::instrument(skip_all, fields(vs_entry_point_name = vs_entrypoint.name()))]
+ pub fn new(
+ vs_entrypoint: &EntryPointReflection<'_>,
+ ) -> Result<Self, VertexDescriptionError>
+ {
+ if vs_entrypoint.stage() != Stage::Vertex {
+ return Err(VertexDescriptionError::EntrypointNotInVertexStage);
+ }
+
+ let inputs = VarInputDfsIter::new(vs_entrypoint).inspect(|var_input| {
+ if var_input.type_layout.kind() != TypeKind::Struct {
+ return;
+ }
+
+ let Some(semantic_name) = var_input.var_layout.semantic_name() else {
+ return;
+ };
+
+ tracing::warn!(
+ "Semantic name '{}' of '{}' will be inherited by it's fields",
+ semantic_name,
+ var_input.var_layout.name().unwrap_or("<unnamed>")
+ );
+ });
+
+ let mut seen_inputs = HashSet::<VertexInputSemName>::new();
+
+ Ok(Self {
+ inputs: inputs
+ .filter(|var_input| var_input.type_layout.kind() != TypeKind::Struct)
+ .map(|var_input| {
+ let name = var_input.var_layout.name().unwrap_or("<unnamed>");
+
+ let semantic_name =
+ var_input.var_layout.semantic_name().ok_or_else(|| {
+ VertexDescriptionError::VertexInputMissingSemanticName {
+ name: name.to_owned(),
+ }
+ })?;
+
+ let semantic_name =
+ VertexInputSemName::from_semantic_name(semantic_name);
+
+ if seen_inputs.contains(&semantic_name) {
+ return Err(
+ VertexDescriptionError::VertexInputHasOccupiedSemanticName {
+ name: name.to_owned(),
+ },
+ );
+ }
+
+ let scalar_type = match (
+ var_input.type_layout.kind(),
+ var_input.type_layout.scalar_type(),
+ ) {
+ (TypeKind::Scalar, Some(scalar_type)) => scalar_type,
+ (TypeKind::Vector, Some(scalar_type)) => scalar_type,
+ _ => {
+ return Err(
+ VertexDescriptionError::UnsupportedVertexInputType {
+ name: name.to_owned(),
+ },
+ );
+ }
+ };
+
+ seen_inputs.insert(semantic_name.clone());
+
+ Ok(VertexInputDescription {
+ semantic_name,
+ index: var_input.index,
+ type_kind: var_input.type_layout.kind(),
+ scalar_type,
+ })
+ })
+ .collect::<Result<Vec<_>, _>>()?
+ .into(),
+ })
+ }
+}
+
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct VertexInputDescription
+{
+ pub semantic_name: VertexInputSemName,
+ pub index: usize,
+ pub type_kind: TypeKind,
+ pub scalar_type: ScalarType,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[non_exhaustive]
+pub enum VertexInputSemName
+{
+ Position,
+ Normal,
+ UvFromTopLeft,
+ Color,
+ Other(Box<str>),
+}
+
+impl VertexInputSemName
+{
+ fn from_semantic_name(semantic_name: &str) -> Self
+ {
+ match semantic_name {
+ STD_VERT_IN_SEM_NAME_POSITION => Self::Position,
+ STD_VERT_IN_SEM_NAME_NORMAL => Self::Normal,
+ STD_VERT_IN_SEM_NAME_UV_FROM_TOP_LEFT => Self::UvFromTopLeft,
+ STD_VERT_IN_SEM_NAME_COLOR => Self::Color,
+ _ => Self::Other(semantic_name.to_lowercase().into_boxed_str()),
+ }
+ }
+
+ pub fn matches_vertex_label(&self, vertex_label: &VertexLabel) -> bool
+ {
+ match (self, vertex_label) {
+ (Self::Position, VertexLabel::Position)
+ | (Self::Normal, VertexLabel::Normal)
+ | (Self::UvFromTopLeft, VertexLabel::UvFromTopLeft)
+ | (Self::Color, VertexLabel::Color) => true,
+ (Self::Other(other), VertexLabel::Other(other_vertex_label)) => {
+ **other == *other_vertex_label
+ }
+ _ => false,
+ }
+ }
+}
+
+impl Display for VertexInputSemName
+{
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
+ {
+ match self {
+ Self::Position => formatter.write_str(STD_VERT_IN_SEM_NAME_POSITION),
+ Self::Normal => formatter.write_str(STD_VERT_IN_SEM_NAME_NORMAL),
+ Self::UvFromTopLeft => {
+ formatter.write_str(STD_VERT_IN_SEM_NAME_UV_FROM_TOP_LEFT)
+ }
+ Self::Color => formatter.write_str(STD_VERT_IN_SEM_NAME_COLOR),
+ Self::Other(other) => {
+ for character in other.chars() {
+ formatter.write_char(character.to_ascii_uppercase())?;
+ }
+
+ Ok(())
+ }
+ }
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum VertexDescriptionError
+{
+ #[error("Entrypoint is not in vertex stage")]
+ EntrypointNotInVertexStage,
+
+ #[error("Type of vertex input '{name}' is not supported")]
+ UnsupportedVertexInputType
+ {
+ name: String
+ },
+
+ #[error("Vertex input '{name}' is missing a semantic name")]
+ VertexInputMissingSemanticName
+ {
+ name: String
+ },
+
+ #[error(
+ "Vertex input '{name}' has a semantic name already used by another vertex input"
+ )]
+ VertexInputHasOccupiedSemanticName
+ {
+ name: String
+ },
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum ComposeProgramError
+{
+ #[error("Shader module does not have a '{entrypoint_name}' entry point")]
+ EntrypointNotFoundError
+ {
+ entrypoint_name: &'static str
+ },
+
+ #[error("Failed to create vertex description")]
+ VertexDescriptionCreationFailed(#[from] VertexDescriptionError),
+
+ #[error(transparent)]
+ Other(Error),
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error(transparent)]
+pub struct Error(#[from] shader_slang::Error);
+
+struct VarInputDfsIter<'a>
+{
+ stack: Vec<(VariableLayout<'a>, TypeLayout<'a>, usize)>,
+}
+
+impl<'a> VarInputDfsIter<'a>
+{
+ fn new(entry_point: &EntryPointReflection<'a>) -> Self
+ {
+ Self {
+ stack: entry_point
+ .parameters()
+ .map(|param| {
+ let Some(param_type_layout) = param.type_layout() else {
+ // I do not know in which cases type_layout can return None
+ unimplemented!();
+ };
+
+ (param, param_type_layout, 0)
+ })
+ .collect(),
+ }
+ }
+}
+
+impl<'a> Iterator for VarInputDfsIter<'a>
+{
+ type Item = VarInput<'a>;
+
+ fn next(&mut self) -> Option<Self::Item>
+ {
+ let (var_layout, type_layout, index) = loop {
+ let (var_layout, type_layout, acc_offset) = self.stack.pop()?;
+
+ if let Some(offset) = var_layout.varying_input_offset() {
+ break (var_layout, type_layout, acc_offset + offset);
+ }
+ };
+
+ if type_layout.kind() == TypeKind::Struct {
+ self.stack.extend(type_layout.fields().map(|field| {
+ let Some(field_type_layout) = field.type_layout() else {
+ // I do not know in which cases type_layout can return
+ // None
+ unimplemented!();
+ };
+
+ (field, field_type_layout, index)
+ }));
+ }
+
+ Some(VarInput { var_layout, type_layout, index })
+ }
+}
+
+struct VarInput<'a>
+{
+ var_layout: VariableLayout<'a>,
+ type_layout: TypeLayout<'a>,
+ index: usize,
+}
+
+fn import_slang_asset(
+ asset_submitter: &mut AssetSubmitter<'_>,
+ file_path: &Path,
+ settings: Option<&'_ Settings>,
+) -> Result<(), ImportError>
+{
+ let file_name = file_path
+ .file_name()
+ .ok_or(ImportError::NoPathFileName)?
+ .to_str()
+ .ok_or(ImportError::PathFileNameNotUtf8)?;
+
+ let file_path_canonicalized = file_path
+ .canonicalize()
+ .map_err(ImportError::CanonicalizePathFailed)?;
+
+ asset_submitter.submit_store(ModuleSource {
+ name: file_name.to_owned().into(),
+ file_path: file_path_canonicalized.into(),
+ source: std::fs::read_to_string(file_path)
+ .map_err(ImportError::ReadFileFailed)?
+ .into(),
+ link_entrypoints: settings
+ .map(|settings| settings.link_entrypoints)
+ .unwrap_or_default(),
+ });
+
+ Ok(())
+}
+
+#[derive(Debug, thiserror::Error)]
+enum ImportError
+{
+ #[error("Failed to read file")]
+ ReadFileFailed(#[source] std::io::Error),
+
+ #[error("Asset path does not have a file name")]
+ NoPathFileName,
+
+ #[error("Asset path file name is not valid UTF8")]
+ PathFileNameNotUtf8,
+
+ #[error("Failed to canonicalize asset path")]
+ CanonicalizePathFailed(#[source] std::io::Error),
+}
+
+declare_entity! {
+pub IMPORT_SHADERS_PHASE: (Phase, pair!(ChildOf, { *HANDLE_ASSETS_PHASE }));
+}
+
+pub(super) fn prepare(collector: &mut crate::ecs::extension::Collector<'_>)
+{
+ let Some(global_session) = SlangGlobalSession::new() else {
+ tracing::error!("Unable to create global shader-slang session");
+ return;
+ };
+
+ let session_options = shader_slang::CompilerOptions::default()
+ .optimization(shader_slang::OptimizationLevel::None)
+ .matrix_layout_row(true)
+ .debug_information(SlangDebugInfoLevel::Maximal)
+ .no_mangle(true);
+
+ let target_desc = shader_slang::TargetDesc::default()
+ .format(shader_slang::CompileTarget::Glsl)
+ // .format(shader_slang::CompileTarget::Spirv)
+ .profile(global_session.find_profile("glsl_330"));
+ // .profile(global_session.find_profile("spirv_1_5"));
+
+ let targets = [target_desc];
+
+ let session_desc = shader_slang::SessionDesc::default()
+ .targets(&targets)
+ .search_paths(&[""])
+ .options(&session_options);
+
+ let Some(session) = global_session.create_session(&session_desc) else {
+ tracing::error!("Failed to create shader-slang session");
+ return;
+ };
+
+ collector
+ .add_sole(Context {
+ _global_session: global_session,
+ session,
+ modules: HashMap::new(),
+ programs: HashMap::new(),
+ })
+ .ok();
+
+ collector.spawn_declared_entity(&IMPORT_SHADERS_PHASE);
+
+ collector.add_system(*INIT_PHASE, initialize);
+ collector.add_system(*IMPORT_SHADERS_PHASE, load_modules);
+}
+
+fn initialize(mut assets: Single<Assets>) -> Result<(), crate::Error>
+{
+ let assets = assets.get_mut()?;
+
+ assets.set_importer::<_, _>(["slang"], import_slang_asset);
+
+ Ok(())
+}
+
+#[tracing::instrument(skip_all)]
+fn load_modules(
+ mut context: Single<Context>,
+ assets: Single<Assets>,
+) -> Result<(), crate::Error>
+{
+ let Ok(context) = context.get_mut() else {
+ unreachable!();
+ };
+
+ let assets = assets.get()?;
+
+ for AssetEvent::Stored(asset_id, asset_label) in assets.events().last_tick_events() {
+ let asset_handle = AssetHandle::<ModuleSource>::from_id(*asset_id);
+
+ if !assets.is_loaded_and_has_type(&asset_handle) {
+ continue;
+ }
+
+ let Some(module_source) = assets.get(&asset_handle) else {
+ unreachable!();
+ };
+
+ tracing::debug!(asset_label=?asset_label, "Loading shader module");
+
+ let module = match load_module(&context.session, module_source) {
+ Ok(module) => module,
+ Err(err) => {
+ tracing::error!("Failed to load shader module: {err}");
+ continue;
+ }
+ };
+
+ context.modules.insert(*asset_id, module.clone());
+
+ if !module_source.link_entrypoints.is_empty() {
+ assert!(context.programs.get(asset_id).is_none());
+
+ let shader_program = match context
+ .compose_into_program(module, module_source.link_entrypoints)
+ {
+ Ok(shader_program) => shader_program,
+ Err(err) => {
+ tracing::error!(
+ "Failed to compose shader into program: {:#}",
+ crate::Error::new(err)
+ );
+ continue;
+ }
+ };
+
+ let linked_shader_program = match shader_program.into_linked() {
+ Ok(linked_shader_program) => linked_shader_program,
+ Err(err) => {
+ tracing::error!("Failed to link shader: {err}");
+ continue;
+ }
+ };
+
+ context.programs.insert(*asset_id, linked_shader_program);
+ }
+ }
+
+ Ok(())
+}
+
+fn load_module(
+ session: &SlangSession,
+ module_source: &ModuleSource,
+) -> Result<Module, Error>
+{
+ let module = session.load_module_from_source_string(
+ &module_source.name,
+ &module_source.file_path.to_string_lossy(),
+ &module_source.source,
+ )?;
+
+ Ok(Module { inner: module })
+}
diff --git a/engine/src/rendering/shader/cursor.rs b/engine/src/rendering/shader/cursor.rs
new file mode 100644
index 0000000..49f6b47
--- /dev/null
+++ b/engine/src/rendering/shader/cursor.rs
@@ -0,0 +1,372 @@
+use std::borrow::Cow;
+use std::fmt::Display;
+use std::hint::cold_path;
+
+use circular_buffer::FixedCircularBuffer;
+
+use crate::color::{Color, Rgb, Rgba};
+use crate::data_types::matrix::Matrix;
+use crate::data_types::vector::Vec3;
+use crate::rendering::object::Id as RenderingObjectId;
+use crate::rendering::shader::{
+ ResourceShape,
+ ScalarType,
+ TypeKind,
+ TypeLayout,
+ VariableLayout,
+};
+
+/// Shader cursor
+#[derive(Clone)]
+pub struct Cursor<'a>
+{
+ type_layout: TypeLayout<'a>,
+ binding_location: BindingLocation,
+ location_path: LocationPath,
+}
+
+impl<'a> Cursor<'a>
+{
+ pub fn new(var_layout: VariableLayout<'a>) -> Self
+ {
+ let binding_location = BindingLocation {
+ binding_index: var_layout.binding_index(),
+ binding_size: 0,
+ byte_offset: var_layout.offset(),
+ };
+
+ Self {
+ type_layout: var_layout.type_layout().unwrap(),
+ binding_location,
+ location_path: LocationPath::default(),
+ }
+ }
+
+ pub fn field(&self, name: impl Into<Cow<'static, str>>) -> Self
+ {
+ let name = name.into();
+
+ let Some(field_var_layout) = self.type_layout.get_field_by_name(name.as_ref())
+ else {
+ panic!("Field '{name}' does not exist");
+ };
+
+ let field_type_kind = field_var_layout.ty().unwrap().kind();
+
+ let (new_var_layout, binding_index_offset) = match field_type_kind {
+ TypeKind::ConstantBuffer => {
+ let elem_var_layout = field_var_layout
+ .type_layout()
+ .expect("Constant buffer field has no type layout")
+ .element_var_layout()
+ .expect(
+ "Constant buffer field type layout has no element var layout",
+ );
+
+ (
+ elem_var_layout,
+ field_var_layout.binding_index() + elem_var_layout.binding_index(),
+ )
+ }
+ TypeKind::Array
+ | TypeKind::Matrix
+ | TypeKind::Scalar
+ | TypeKind::Vector
+ | TypeKind::Struct
+ | TypeKind::Resource => (field_var_layout, field_var_layout.binding_index()),
+ type_kind => unimplemented!("Type kind {type_kind:?} is not yet supported"),
+ };
+
+ let mut location_path = self.location_path.clone();
+
+ location_path.push(Location::Field(name));
+
+ Self {
+ type_layout: new_var_layout.type_layout().unwrap(),
+ binding_location: BindingLocation {
+ binding_index: self.binding_location.binding_index + binding_index_offset,
+ binding_size: if field_type_kind == TypeKind::ConstantBuffer {
+ new_var_layout
+ .type_layout()
+ .unwrap()
+ .uniform_size()
+ .unwrap()
+ } else {
+ self.binding_location.binding_size
+ },
+ byte_offset: self.binding_location.byte_offset + new_var_layout.offset(),
+ },
+ location_path,
+ }
+ }
+
+ pub fn element(mut self, index: usize) -> Self
+ {
+ let element_type_layout = self.type_layout.element_type_layout().unwrap();
+
+ self.binding_location.byte_offset += index * element_type_layout.stride();
+
+ self.type_layout = element_type_layout;
+
+ self.location_path.push(Location::Element(index));
+
+ self
+ }
+
+ pub fn binding(self, value: BindingValue) -> Result<Binding, BindingError>
+ {
+ value.validate_for_shader_type(self.type_layout, self.location_path)?;
+
+ Ok(Binding {
+ location: self.binding_location,
+ value,
+ })
+ }
+}
+
+/// Shader cursor location.
+#[derive(Debug, Clone, Default)]
+pub struct LocationPath
+{
+ locations: FixedCircularBuffer<Location, 16>,
+ is_truncated: bool,
+}
+
+impl LocationPath
+{
+ fn push(&mut self, location: Location)
+ {
+ if self.locations.push_back(location).is_some() {
+ self.is_truncated = true;
+ }
+ }
+}
+
+impl Display for LocationPath
+{
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
+ {
+ if self.is_truncated {
+ write!(formatter, "(...)")?;
+ }
+
+ for location in &self.locations {
+ match location {
+ Location::Field(field) => {
+ write!(formatter, ".{field}")?;
+ }
+ Location::Element(element) => {
+ write!(formatter, "[{element}]")?;
+ }
+ }
+ }
+
+ Ok(())
+ }
+}
+
+/// Shader cursor location.
+#[derive(Debug, Clone)]
+pub enum Location
+{
+ Field(Cow<'static, str>),
+ Element(usize),
+}
+
+impl Display for Location
+{
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
+ {
+ match self {
+ Self::Field(field) => <Cow<'static, str> as Display>::fmt(field, formatter),
+ Self::Element(element) => <usize as Display>::fmt(element, formatter),
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct BindingLocation
+{
+ pub binding_index: u32,
+ pub binding_size: usize,
+ pub byte_offset: usize,
+}
+
+#[derive(Debug, Clone)]
+pub enum BindingValue
+{
+ Uint(u32),
+ Int(i32),
+ Float(f32),
+ FVec3(Vec3<f32>),
+ Color(Color<f32>),
+ FMat4x4(Matrix<f32, 4, 4>),
+ Texture(RenderingObjectId, BindingTextureKind),
+}
+
+impl BindingValue
+{
+ fn validate_for_shader_type(
+ &self,
+ ty: TypeLayout<'_>,
+ location_path: LocationPath,
+ ) -> Result<(), BindingError>
+ {
+ let ty_kind = ty.kind();
+ let scalar_ty = ty.scalar_type();
+
+ let element_scalar_ty = ty
+ .element_type_layout()
+ .and_then(|elem_ty| elem_ty.scalar_type());
+
+ let element_cnt = ty.element_cnt();
+
+ let is_valid = match self {
+ Self::Uint(_) => {
+ ty_kind == TypeKind::Scalar && scalar_ty == Some(ScalarType::Uint32)
+ }
+ Self::Int(_) => {
+ ty_kind == TypeKind::Scalar && scalar_ty == Some(ScalarType::Int32)
+ }
+ Self::Float(_) => {
+ ty_kind == TypeKind::Scalar && scalar_ty == Some(ScalarType::Float32)
+ }
+ Self::FVec3(_) => {
+ ty_kind == TypeKind::Vector
+ && element_scalar_ty == Some(ScalarType::Float32)
+ && element_cnt == Some(3)
+ }
+ Self::Color(Color::Rgb(_)) => {
+ ty_kind == TypeKind::Vector
+ && element_scalar_ty == Some(ScalarType::Float32)
+ && element_cnt == Some(3)
+ }
+ Self::Color(Color::Rgba(_)) => {
+ ty_kind == TypeKind::Vector
+ && element_scalar_ty == Some(ScalarType::Float32)
+ && element_cnt == Some(4)
+ }
+ Self::FMat4x4(_) => {
+ ty_kind == TypeKind::Matrix
+ && element_scalar_ty == Some(ScalarType::Float32)
+ && ty.row_cnt() == Some(4)
+ && ty.column_cnt() == Some(4)
+ }
+ Self::Texture(_, BindingTextureKind::Texture2D) => {
+ ty_kind == TypeKind::Resource
+ && ty.resource_shape().is_some_and(|res_shape| {
+ (res_shape & ResourceShape::BASE)
+ .contains(ResourceShape::TEXTURE_2D)
+ })
+ }
+ Self::Texture(_, BindingTextureKind::Cube) => {
+ ty_kind == TypeKind::Resource
+ && ty.resource_shape().is_some_and(|res_shape| {
+ (res_shape & ResourceShape::BASE)
+ .contains(ResourceShape::TEXTURE_CUBE)
+ })
+ }
+ };
+
+ if !is_valid {
+ cold_path();
+ return Err(BindingError::IncorrectValueType {
+ value: self.clone(),
+ location_path,
+ });
+ }
+
+ Ok(())
+ }
+}
+
+impl From<u32> for BindingValue
+{
+ fn from(value: u32) -> Self
+ {
+ BindingValue::Uint(value)
+ }
+}
+
+impl From<i32> for BindingValue
+{
+ fn from(value: i32) -> Self
+ {
+ BindingValue::Int(value)
+ }
+}
+
+impl From<f32> for BindingValue
+{
+ fn from(value: f32) -> Self
+ {
+ BindingValue::Float(value)
+ }
+}
+
+impl From<Vec3<f32>> for BindingValue
+{
+ fn from(vec: Vec3<f32>) -> Self
+ {
+ BindingValue::FVec3(vec)
+ }
+}
+
+impl From<Color<f32>> for BindingValue
+{
+ fn from(color: Color<f32>) -> Self
+ {
+ BindingValue::Color(color)
+ }
+}
+impl From<Rgb<f32>> for BindingValue
+{
+ fn from(color: Rgb<f32>) -> Self
+ {
+ BindingValue::Color(color.into())
+ }
+}
+
+impl From<Rgba<f32>> for BindingValue
+{
+ fn from(color: Rgba<f32>) -> Self
+ {
+ BindingValue::Color(color.into())
+ }
+}
+
+impl From<Matrix<f32, 4, 4>> for BindingValue
+{
+ fn from(matrix: Matrix<f32, 4, 4>) -> Self
+ {
+ BindingValue::FMat4x4(matrix)
+ }
+}
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct Binding
+{
+ pub location: BindingLocation,
+ pub value: BindingValue,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum BindingTextureKind
+{
+ Texture2D,
+ Cube,
+}
+
+#[derive(Debug, thiserror::Error)]
+#[non_exhaustive]
+pub enum BindingError
+{
+ #[error("Value {value:?} has incorrect type for value at {location_path} in shader")]
+ IncorrectValueType
+ {
+ value: BindingValue,
+ location_path: LocationPath,
+ },
+}
diff --git a/engine/src/scene.rs b/engine/src/scene.rs
new file mode 100644
index 0000000..2a66c3f
--- /dev/null
+++ b/engine/src/scene.rs
@@ -0,0 +1,8 @@
+use crate::ecs::Component;
+use crate::reflection::Reflection;
+
+#[derive(Debug, Default, Clone, Copy, Component, Reflection)]
+pub struct Scene;
+
+#[derive(Debug, Default, Clone, Copy, Component, Reflection)]
+pub struct Active;
diff --git a/engine/src/shader.rs b/engine/src/shader.rs
deleted file mode 100644
index 89f7b7c..0000000
--- a/engine/src/shader.rs
+++ /dev/null
@@ -1,186 +0,0 @@
-use std::collections::hash_map::DefaultHasher;
-use std::fs::read_to_string;
-use std::hash::{Hash, Hasher};
-use std::path::{Path, PathBuf};
-
-use ecs::Component;
-
-use crate::shader_preprocessor::{Error as ShaderPreprocessorError, ShaderPreprocessor};
-
-const VERTEX_SHADER_FILE: &str = "vertex.glsl";
-const FRAGMENT_SHADER_FILE: &str = "fragment.glsl";
-
-const SHADER_DIR: &str = "engine";
-
-/// Shader program
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Component)]
-pub struct Program
-{
- shaders: Vec<Shader>,
-}
-
-impl Program
-{
- /// Creates a new shader program with the default shaders.
- ///
- /// # Errors
- /// Returns `Err` if:
- /// - Reading a default shader file Fails
- /// - Preprocessing a shader fails.
- pub fn new() -> Result<Self, Error>
- {
- let mut program = Self { shaders: Vec::new() };
-
- program.push_shader(
- Shader::read_shader_file(
- Kind::Vertex,
- &Path::new(SHADER_DIR).join(VERTEX_SHADER_FILE),
- )?
- .preprocess()?,
- );
-
- program.push_shader(
- Shader::read_shader_file(
- Kind::Fragment,
- &Path::new(SHADER_DIR).join(FRAGMENT_SHADER_FILE),
- )?
- .preprocess()?,
- );
-
- Ok(program)
- }
-
- pub fn push_shader(&mut self, shader: Shader)
- {
- self.shaders.push(shader);
- }
-
- pub fn append_shaders(&mut self, shaders: impl IntoIterator<Item = Shader>)
- {
- self.shaders.extend(shaders);
- }
-
- #[must_use]
- pub fn shaders(&self) -> &[Shader]
- {
- &self.shaders
- }
-
- pub(crate) fn u64_hash(&self) -> u64
- {
- let mut hasher = DefaultHasher::new();
-
- self.hash(&mut hasher);
-
- hasher.finish()
- }
-}
-
-#[derive(Debug, Clone, Hash, PartialEq, Eq)]
-pub struct Shader
-{
- kind: Kind,
- source: String,
- file: PathBuf,
-}
-
-impl Shader
-{
- /// Reads a shader from the specified source file.
- ///
- /// # Errors
- /// Will return `Err` if:
- /// - Reading the file fails
- /// - The shader source is not ASCII
- pub fn read_shader_file(kind: Kind, shader_file: &Path) -> Result<Self, Error>
- {
- let source = read_to_string(shader_file).map_err(|err| Error::ReadFailed {
- source: err,
- shader_file: shader_file.to_path_buf(),
- })?;
-
- if !source.is_ascii() {
- return Err(Error::SourceNotAscii);
- }
-
- Ok(Self {
- kind,
- source,
- file: shader_file.to_path_buf(),
- })
- }
-
- /// Preprocesses the shaders.
- ///
- /// # Errors
- /// Returns `Err` if preprocessing fails.
- pub fn preprocess(self) -> Result<Self, Error>
- {
- let shader_preprocessor = ShaderPreprocessor::new(
- self.file
- .parent()
- .ok_or(Error::SourcePathHasNoParent)?
- .to_path_buf(),
- );
-
- let source_preprocessed = shader_preprocessor
- .preprocess(self.source, &self.file)
- .map_err(|err| Error::PreprocessFailed {
- source: err,
- shader_file: self.file.clone(),
- })?;
-
- Ok(Self {
- kind: self.kind,
- source: source_preprocessed,
- file: self.file.clone(),
- })
- }
-
- #[must_use]
- pub fn kind(&self) -> Kind
- {
- self.kind
- }
-
- #[must_use]
- pub fn source(&self) -> &str
- {
- &self.source
- }
-}
-
-/// Shader kind.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum Kind
-{
- Vertex,
- Fragment,
-}
-
-/// Shader error
-#[derive(Debug, thiserror::Error)]
-pub enum Error
-{
- #[error("Failed to read shader {}", shader_file.display())]
- ReadFailed
- {
- #[source]
- source: std::io::Error,
- shader_file: PathBuf,
- },
-
- #[error("Shader source is not ASCII")]
- SourceNotAscii,
-
- #[error("Failed to preprocess shader {}", shader_file.display())]
- PreprocessFailed
- {
- #[source]
- source: ShaderPreprocessorError,
- shader_file: PathBuf,
- },
-
- #[error("Shader source path has no parent")]
- SourcePathHasNoParent,
-}
diff --git a/engine/src/shader_preprocessor.rs b/engine/src/shader_preprocessor.rs
deleted file mode 100644
index 70696ac..0000000
--- a/engine/src/shader_preprocessor.rs
+++ /dev/null
@@ -1,607 +0,0 @@
-use std::path::{Path, PathBuf};
-use std::string::FromUtf8Error;
-
-const PREINCLUDE_DIRECTIVE: &str = "#preinclude";
-
-/// Preprocessor for shaders written in the OpenGL Shading Language.
-pub struct ShaderPreprocessor
-{
- read_file: fn(&Path) -> Result<Vec<u8>, std::io::Error>,
- base_dir: PathBuf,
-}
-
-impl ShaderPreprocessor
-{
- pub fn new(base_dir: PathBuf) -> Self
- {
- Self {
- read_file: |path| std::fs::read(path),
- base_dir,
- }
- }
-
- pub fn preprocess(
- &self,
- shader_content: String,
- shader_file_path: &Path,
- ) -> Result<String, Error>
- {
- let mut preincludes = shader_content
- .match_indices(PREINCLUDE_DIRECTIVE)
- .peekable();
-
- if preincludes.peek().is_none() {
- // Shader content contains no preincludes
- return Ok(shader_content);
- };
-
- let mut preprocessed = shader_content.clone();
-
- let mut curr = shader_content.find(PREINCLUDE_DIRECTIVE);
-
- let mut last_start = 0;
- let mut span_line_offset = 0;
-
- while let Some(preinclude_start) = curr {
- let replacement_job = self.handle_preinclude(
- &preprocessed,
- shader_file_path,
- preinclude_start,
- span_line_offset,
- )?;
-
- let path = replacement_job.path.clone();
-
- let mut included = String::from_utf8(
- (self.read_file)(&self.base_dir.join(replacement_job.path.clone()))
- .map_err(|err| Error::ReadIncludedShaderFailed {
- source: err,
- path: replacement_job.path.clone(),
- })?,
- )
- .map_err(|err| Error::IncludedShaderInvalidUtf8 {
- source: err,
- path: path.clone(),
- })?;
-
- if let Some(first_line) = included.lines().next() {
- if first_line.starts_with("#version") {
- included = included
- .chars()
- .skip_while(|character| *character != '\n')
- .collect();
- }
- }
-
- let included_preprocessed =
- self.preprocess(included, &replacement_job.path)?;
-
- let start = replacement_job.start_index;
- let end = replacement_job.end_index;
-
- preprocessed.replace_range(start..end, &included_preprocessed);
-
- curr = preprocessed[last_start + 1..]
- .find(PREINCLUDE_DIRECTIVE)
- .map(|index| index + 1);
-
- last_start = preinclude_start + included_preprocessed.len();
-
- span_line_offset += included_preprocessed.lines().count();
- }
-
- Ok(preprocessed)
- }
-
- fn handle_preinclude(
- &self,
- shader_content: &str,
- shader_file_path: &Path,
- preinclude_start_index: usize,
- span_line_offset: usize,
- ) -> Result<ReplacementJob, Error>
- {
- let expect_token = |token: char, index: usize| {
- let token_found = shader_content.chars().nth(index).ok_or_else(|| {
- Error::ExpectedToken {
- expected: token,
- span: Span::new(
- shader_content,
- &self.base_dir.join(shader_file_path),
- index,
- span_line_offset,
- preinclude_start_index,
- ),
- }
- })?;
-
- if token_found != token {
- return Err(Error::InvalidToken {
- expected: token,
- found: token_found,
- span: Span::new(
- shader_content,
- &self.base_dir.join(shader_file_path),
- index,
- span_line_offset,
- preinclude_start_index,
- ),
- });
- }
-
- Ok(())
- };
-
- let space_index = preinclude_start_index + PREINCLUDE_DIRECTIVE.len();
- let quote_open_index = space_index + 1;
-
- expect_token(' ', space_index)?;
- expect_token('"', quote_open_index)?;
-
- let buf = shader_content[quote_open_index + 1..]
- .chars()
- .take_while(|character| *character != '"')
- .map(|character| character as u8)
- .collect::<Vec<_>>();
-
- if buf.is_empty() {
- return Err(Error::ExpectedToken {
- expected: '"',
- span: Span::new(
- shader_content,
- &self.base_dir.join(shader_file_path),
- shader_content.len() - 1,
- span_line_offset,
- preinclude_start_index,
- ),
- });
- }
-
- let path_len = buf.len();
-
- let path = PathBuf::from(String::from_utf8(buf).map_err(|err| {
- Error::PreincludePathInvalidUtf8 {
- source: err,
- span: Span::new(
- shader_content,
- &self.base_dir.join(shader_file_path),
- quote_open_index + 1,
- span_line_offset,
- preinclude_start_index,
- ),
- }
- })?);
-
- Ok(ReplacementJob {
- start_index: preinclude_start_index,
- end_index: quote_open_index + 1 + path_len + 1,
- path,
- })
- }
-}
-
-struct ReplacementJob
-{
- start_index: usize,
- end_index: usize,
- path: PathBuf,
-}
-
-/// Shader preprocessing error.
-#[derive(Debug, thiserror::Error)]
-pub enum Error
-{
- #[error(
- "Invalid token at line {}, column {} of {}. Expected '{}', found '{}'",
- span.line,
- span.column,
- span.file.display(),
- expected,
- found
- )]
- InvalidToken
- {
- expected: char,
- found: char,
- span: Span,
- },
-
- #[error(
- "Expected token '{}' at line {}, column {} of {}. Found eof",
- expected,
- span.line,
- span.column,
- span.file.display(),
- )]
- ExpectedToken
- {
- expected: char, span: Span
- },
-
- #[error(
- "Preinclude path at line {}, column {} of {} is invalid UTF-8",
- span.line,
- span.column,
- span.file.display(),
- )]
- PreincludePathInvalidUtf8
- {
- #[source]
- source: FromUtf8Error,
- span: Span,
- },
-
- #[error("Failed to read included shader")]
- ReadIncludedShaderFailed
- {
- #[source]
- source: std::io::Error,
- path: PathBuf,
- },
-
- #[error("Included shader is not valid UTF-8")]
- IncludedShaderInvalidUtf8
- {
- #[source]
- source: FromUtf8Error,
- path: PathBuf,
- },
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct Span
-{
- line: usize,
- column: usize,
- file: PathBuf,
-}
-
-impl Span
-{
- fn new(
- file_content: &str,
- file_path: &Path,
- char_index: usize,
- line_offset: usize,
- line_start_index: usize,
- ) -> Self
- {
- let line = find_line_of_index(file_content, char_index) + 1
- - line_offset.saturating_sub(1);
-
- Self {
- line,
- column: char_index - line_start_index + 1,
- file: file_path.to_path_buf(),
- }
- }
-}
-
-fn find_line_of_index(text: &str, index: usize) -> usize
-{
- text.chars()
- .take(index + 1)
- .enumerate()
- .filter(|(_, character)| *character == '\n')
- .count()
-}
-
-#[cfg(test)]
-mod tests
-{
- use std::ffi::OsStr;
- use std::path::{Path, PathBuf};
-
- use super::{Error, ShaderPreprocessor};
-
- #[test]
- fn preprocess_no_directives_is_same()
- {
- assert_eq!(
- ShaderPreprocessor {
- read_file: |_| { unreachable!() },
- base_dir: PathBuf::new()
- }
- .preprocess("#version 330 core\n".to_string(), Path::new("foo.glsl"),)
- .unwrap(),
- "#version 330 core\n".to_string()
- );
- }
-
- #[test]
- fn preprocess_with_directives_works()
- {
- assert_eq!(
- ShaderPreprocessor {
- read_file: |_| { Ok(b"out vec4 FragColor;".to_vec()) },
- base_dir: PathBuf::new()
- }
- .preprocess(
- concat!(
- "#version 330 core\n",
- "\n",
- "#preinclude \"foo.glsl\"\n",
- "\n",
- "void main() {}",
- )
- .to_string(),
- Path::new("foo.glsl"),
- )
- .unwrap(),
- concat!(
- "#version 330 core\n",
- "\n",
- "out vec4 FragColor;\n",
- "\n",
- "void main() {}",
- )
- );
-
- assert_eq!(
- ShaderPreprocessor {
- read_file: |_| { Ok(b"out vec4 FragColor;".to_vec()) },
- base_dir: PathBuf::new()
- }
- .preprocess(
- concat!(
- "#version 330 core\n",
- "\n",
- "#preinclude \"bar.glsl\"\n",
- "\n",
- "in vec3 in_frag_color;\n",
- "\n",
- "void main() {}",
- )
- .to_string(),
- Path::new("foo.glsl"),
- )
- .unwrap(),
- concat!(
- "#version 330 core\n",
- "\n",
- "out vec4 FragColor;\n",
- "\n",
- "in vec3 in_frag_color;\n",
- "\n",
- "void main() {}",
- )
- );
-
- assert_eq!(
- ShaderPreprocessor {
- read_file: |path| {
- if path == OsStr::new("bar.glsl") {
- Ok(b"out vec4 FragColor;".to_vec())
- } else {
- Ok(concat!(
- "uniform sampler2D input_texture;\n",
- "in vec2 in_texture_coords;"
- )
- .as_bytes()
- .to_vec())
- }
- },
- base_dir: PathBuf::new()
- }
- .preprocess(
- concat!(
- "#version 330 core\n",
- "\n",
- "#preinclude \"bar.glsl\"\n",
- "\n",
- "in vec3 in_frag_color;\n",
- "\n",
- "#preinclude \"foo.glsl\"\n",
- "\n",
- "void main() {}",
- )
- .to_string(),
- Path::new("foo.glsl"),
- )
- .unwrap(),
- concat!(
- "#version 330 core\n",
- "\n",
- "out vec4 FragColor;\n",
- "\n",
- "in vec3 in_frag_color;\n",
- "\n",
- "uniform sampler2D input_texture;\n",
- "in vec2 in_texture_coords;\n",
- "\n",
- "void main() {}",
- )
- );
- }
-
- #[test]
- fn preprocess_invalid_shader_does_not_work()
- {
- let path = Path::new("foo.glsl");
-
- let res = ShaderPreprocessor {
- read_file: |_| Ok(b"out vec4 FragColor;".to_vec()),
- base_dir: PathBuf::new(),
- }
- .preprocess(
- concat!(
- "#version 330 core\n",
- "\n",
- // Missing "
- "#preinclude foo.glsl\"\n",
- "\n",
- "void main() {}",
- )
- .to_string(),
- path,
- );
-
- let Err(Error::InvalidToken { expected, found, span }) = res else {
- panic!(
- "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}"
- );
- };
-
- assert_eq!(expected, '"');
- assert_eq!(found, 'f');
- assert_eq!(span.line, 3);
- assert_eq!(span.column, 13);
- assert_eq!(span.file, path);
- }
-
- #[test]
- fn preprocess_error_has_correct_span()
- {
- let path = Path::new("foo.glsl");
-
- let res = ShaderPreprocessor {
- read_file: |path| {
- if path == OsStr::new("bar.glsl") {
- Ok(concat!(
- "out vec4 FragColor;\n",
- "in vec2 in_texture_coords;\n",
- "in float foo;"
- )
- .as_bytes()
- .to_vec())
- } else {
- Ok(b"uniform sampler2D input_texture;".to_vec())
- }
- },
- base_dir: PathBuf::new(),
- }
- .preprocess(
- concat!(
- "#version 330 core\n",
- "\n",
- "#preinclude \"bar.glsl\"\n",
- "\n",
- "in vec3 in_frag_color;\n",
- "\n",
- "#preinclude\"foo.glsl\"\n",
- "\n",
- "void main() {}",
- )
- .to_string(),
- Path::new("foo.glsl"),
- );
-
- let Err(Error::InvalidToken { expected, found, span }) = res else {
- panic!(
- "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}"
- );
- };
-
- assert_eq!(expected, ' ');
- assert_eq!(found, '"');
- assert_eq!(span.line, 7);
- assert_eq!(span.column, 12);
- assert_eq!(span.file, path);
- }
-
- #[test]
- fn preprocess_included_shader_with_include_works()
- {
- assert_eq!(
- ShaderPreprocessor {
- read_file: |path| {
- if path == OsStr::new("bar.glsl") {
- Ok(concat!(
- "#preinclude \"foo.glsl\"\n",
- "\n",
- "out vec4 FragColor;"
- )
- .as_bytes()
- .to_vec())
- } else {
- Ok(concat!(
- "uniform sampler2D input_texture;\n",
- "in vec2 in_texture_coords;"
- )
- .as_bytes()
- .to_vec())
- }
- },
- base_dir: PathBuf::new()
- }
- .preprocess(
- concat!(
- "#version 330 core\n",
- "\n",
- "#preinclude \"bar.glsl\"\n",
- "\n",
- "in vec3 in_frag_color;\n",
- "\n",
- "void main() {}",
- )
- .to_string(),
- Path::new("foo.glsl"),
- )
- .unwrap(),
- concat!(
- "#version 330 core\n",
- "\n",
- "uniform sampler2D input_texture;\n",
- "in vec2 in_texture_coords;\n",
- "\n",
- "out vec4 FragColor;\n",
- "\n",
- "in vec3 in_frag_color;\n",
- "\n",
- "void main() {}",
- )
- );
- }
-
- #[test]
- fn preprocess_included_shader_with_include_error_span_is_correct()
- {
- let res = ShaderPreprocessor {
- read_file: |path| {
- if path == OsStr::new("bar.glsl") {
- Ok(concat!(
- // ' instead of "
- "#preinclude 'foo.glsl\"\n",
- "\n",
- "out vec4 FragColor;"
- )
- .as_bytes()
- .to_vec())
- } else {
- Ok(concat!(
- "uniform sampler2D input_texture;\n",
- "in vec2 in_texture_coords;"
- )
- .as_bytes()
- .to_vec())
- }
- },
- base_dir: PathBuf::new(),
- }
- .preprocess(
- concat!(
- "#version 330 core\n",
- "\n",
- "#preinclude \"bar.glsl\"\n",
- "\n",
- "in vec3 in_frag_color;\n",
- "\n",
- "void main() {}",
- )
- .to_string(),
- Path::new("foo.glsl"),
- );
-
- let Err(Error::InvalidToken { expected, found, span }) = res else {
- panic!(
- "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}"
- );
- };
-
- assert_eq!(expected, '"');
- assert_eq!(found, '\'');
- assert_eq!(span.line, 1);
- assert_eq!(span.column, 13);
- assert_eq!(span.file, Path::new("bar.glsl"));
- }
-}
diff --git a/engine/src/sky_box.rs b/engine/src/sky_box.rs
new file mode 100644
index 0000000..ceb82df
--- /dev/null
+++ b/engine/src/sky_box.rs
@@ -0,0 +1,11 @@
+use crate::asset::Handle as AssetHandle;
+use crate::ecs::Component;
+use crate::reflection::Reflection;
+use crate::texture::{CubeMapFace, Texture};
+
+#[derive(Debug, Clone, Component, Reflection)]
+#[non_exhaustive]
+pub enum SkyBox
+{
+ AssetPerCubeMapFace([(CubeMapFace, AssetHandle<Texture>); 6]),
+}
diff --git a/engine/src/texture.rs b/engine/src/texture.rs
index f82b59d..8361f12 100644
--- a/engine/src/texture.rs
+++ b/engine/src/texture.rs
@@ -1,152 +1,45 @@
-use std::fmt::Display;
use std::path::Path;
-use std::sync::atomic::{AtomicU32, Ordering};
-use image::io::Reader as ImageReader;
-use image::{DynamicImage, ImageError, Rgb, RgbImage};
-
-use crate::color::Color;
-use crate::data_types::dimens::Dimens;
-use crate::opengl::texture::PixelDataFormat;
-
-static NEXT_ID: AtomicU32 = AtomicU32::new(0);
-
-mod reexports
-{
- pub use crate::opengl::texture::{Filtering, Wrapping};
-}
-
-pub use reexports::*;
+use crate::asset::{Assets, Submitter as AssetSubmitter};
+use crate::builder;
+use crate::color::Rgba;
+use crate::image::{Error as ImageError, Image};
#[derive(Debug, Clone)]
-pub struct Texture
+pub enum Texture
{
- id: Id,
- image: DynamicImage,
- pixel_data_format: PixelDataFormat,
- dimensions: Dimens<u32>,
- properties: Properties,
+ Texture2D(Tex2D),
+ CubeMap(TexCubeMap),
}
-impl Texture
+#[derive(Debug, Clone)]
+pub struct Tex2D
{
- /// Opens a texture image.
- ///
- /// # Errors
- /// Will return `Err` if:
- /// - Opening the image fails
- /// - The image data is not 8-bit/color RGB
- #[allow(clippy::new_without_default)]
- pub fn open(path: &Path) -> Result<Self, Error>
- {
- let image = ImageReader::open(path)
- .map_err(Error::OpenImageFailed)?
- .decode()
- .map_err(Error::DecodeImageFailed)?;
-
- let pixel_data_format = match &image {
- DynamicImage::ImageRgb8(_) => PixelDataFormat::Rgb8,
- DynamicImage::ImageRgba8(_) => PixelDataFormat::Rgba8,
- _ => {
- return Err(Error::UnsupportedImageDataKind);
- }
- };
-
- let dimensions = Dimens {
- width: image.width(),
- height: image.height(),
- };
-
- let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
-
- Ok(Self {
- id: Id::new(id),
- image,
- pixel_data_format,
- dimensions,
- properties: Properties::default(),
- })
- }
-
- #[must_use]
- pub fn new_from_color(dimensions: &Dimens<u32>, color: &Color<u8>) -> Self
- {
- let image = RgbImage::from_pixel(
- dimensions.width,
- dimensions.height,
- Rgb([color.red, color.green, color.blue]),
- );
-
- let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
-
- Self {
- id: Id::new(id),
- image: image.into(),
- pixel_data_format: PixelDataFormat::Rgb8,
- dimensions: *dimensions,
- properties: Properties::default(),
- }
- }
-
- #[must_use]
- pub fn id(&self) -> Id
- {
- self.id
- }
-
- #[must_use]
- pub fn properties(&self) -> &Properties
- {
- &self.properties
- }
-
- pub fn properties_mut(&mut self) -> &mut Properties
- {
- &mut self.properties
- }
-
- #[must_use]
- pub fn dimensions(&self) -> &Dimens<u32>
- {
- &self.dimensions
- }
-
- #[must_use]
- pub fn pixel_data_format(&self) -> PixelDataFormat
- {
- self.pixel_data_format
- }
-
- #[must_use]
- pub fn image(&self) -> &DynamicImage
- {
- &self.image
- }
+ pub image: Image,
+ pub properties: Properties,
}
-impl Drop for Texture
+#[derive(Debug, Clone)]
+pub struct TexCubeMap
{
- fn drop(&mut self)
- {
- NEXT_ID.fetch_sub(1, Ordering::Relaxed);
- }
+ pub images: [(CubeMapFace, Image); 6],
+ pub properties: Properties,
}
-/// Texture error.
-#[derive(Debug, thiserror::Error)]
-pub enum Error
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum CubeMapFace
{
- #[error("Failed to open texture image")]
- OpenImageFailed(#[source] std::io::Error),
-
- #[error("Failed to decode texture image")]
- DecodeImageFailed(#[source] ImageError),
-
- #[error("Unsupported image data kind")]
- UnsupportedImageDataKind,
+ PositiveX,
+ NegativeX,
+ PositiveY,
+ NegativeY,
+ PositiveZ,
+ NegativeZ,
}
+builder! {
/// Texture properties
+#[builder(name = PropertiesBuilder, derives=(Debug, Clone))]
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Properties
@@ -154,6 +47,16 @@ pub struct Properties
pub wrap: Wrapping,
pub magnifying_filter: Filtering,
pub minifying_filter: Filtering,
+ pub border: Option<Border>,
+}
+}
+
+impl Properties
+{
+ pub fn builder() -> PropertiesBuilder
+ {
+ PropertiesBuilder::default()
+ }
}
impl Default for Properties
@@ -164,34 +67,88 @@ impl Default for Properties
wrap: Wrapping::Repeat,
magnifying_filter: Filtering::Linear,
minifying_filter: Filtering::Nearest,
+ border: None,
}
}
}
-/// Texture ID.
+impl Default for PropertiesBuilder
+{
+ fn default() -> Self
+ {
+ Properties::default().into()
+ }
+}
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct Id
+#[non_exhaustive]
+pub enum Filtering
{
- id: u32,
+ Nearest,
+ Linear,
}
-impl Id
+/// Texture wrapping.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[non_exhaustive]
+pub enum Wrapping
{
- fn new(id: u32) -> Self
- {
- Self { id }
- }
+ Repeat,
+ MirroredRepeat,
+ ClampToEdge,
+ ClampToBorder,
+}
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub enum Border
+{
+ RgbaF32(Rgba<f32>),
+ Depth(f32),
+}
- pub(crate) fn into_inner(self) -> u32
+builder! {
+#[builder(name = ImportSettingsBuilder, derives=(Debug, Clone))]
+#[derive(Debug, Default, Clone)]
+#[non_exhaustive]
+pub struct ImportSettings {
+ properties: Properties,
+}
+}
+
+impl ImportSettings
+{
+ pub fn builder() -> ImportSettingsBuilder
{
- self.id
+ ImportSettingsBuilder::default()
}
}
-impl Display for Id
+impl Default for ImportSettingsBuilder
{
- fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
+ fn default() -> Self
{
- self.id.fmt(formatter)
+ ImportSettings::default().into()
}
}
+
+pub(crate) fn initialize(assets: &mut Assets)
+{
+ assets.set_importer::<_, _>(["png", "jpg"], import);
+}
+
+fn import(
+ asset_submitter: &mut AssetSubmitter<'_>,
+ path: &Path,
+ settings: Option<&'_ ImportSettings>,
+) -> Result<(), ImageError>
+{
+ asset_submitter.submit_store(Texture::Texture2D(Tex2D {
+ image: Image::open(path)?,
+ properties: settings
+ .map(|settings| settings.properties.clone())
+ .unwrap_or_default(),
+ }));
+
+ Ok(())
+}
diff --git a/engine/src/transform.rs b/engine/src/transform.rs
index 5e5e296..8e768a6 100644
--- a/engine/src/transform.rs
+++ b/engine/src/transform.rs
@@ -1,43 +1,49 @@
-use ecs::Component;
-
+use crate::data_types::dimens::Dimens3;
+use crate::ecs::Component;
+use crate::matrix::Matrix;
+use crate::reflection::Reflection;
use crate::vector::Vec3;
-/// A position in 3D space.
-#[derive(Debug, Default, Clone, Copy, Component)]
-pub struct Position
+#[derive(Debug, Clone, Component, Reflection)]
+#[non_exhaustive]
+pub struct Transform
{
pub position: Vec3<f32>,
+ pub scale: Dimens3<f32>,
}
-impl From<Vec3<f32>> for Position
+impl Transform
{
- fn from(position: Vec3<f32>) -> Self
+ pub fn position(mut self, position: Vec3<f32>) -> Self
{
- Self { position }
+ self.position = position;
+ self
}
-}
-/// Scaling of a 3D object.
-#[derive(Debug, Clone, Copy, Component)]
-pub struct Scale
-{
- pub scale: Vec3<f32>,
-}
+ pub fn scale(mut self, scale: Dimens3<f32>) -> Self
+ {
+ self.scale = scale;
+ self
+ }
-impl From<Vec3<f32>> for Scale
-{
- fn from(scale: Vec3<f32>) -> Self
+ pub fn to_matrix(&self) -> Matrix<f32, 4, 4>
{
- Self { scale }
+ let mut matrix = Matrix::new_identity();
+
+ matrix.translate(&self.position);
+ matrix.scale(&self.scale);
+
+ matrix
}
}
-impl Default for Scale
+impl Default for Transform
{
fn default() -> Self
{
Self {
- scale: Vec3 { x: 1.0, y: 1.0, z: 1.0 },
+ position: Vec3 { x: 0.0, y: 0.0, z: 0.0 },
+ scale: Dimens3 { width: 1.0, height: 1.0, depth: 1.0 },
}
}
}
diff --git a/engine/src/ui.rs b/engine/src/ui.rs
new file mode 100644
index 0000000..626c810
--- /dev/null
+++ b/engine/src/ui.rs
@@ -0,0 +1,2 @@
+pub mod dear_imgui;
+pub mod view;
diff --git a/engine/src/ui/dear_imgui.rs b/engine/src/ui/dear_imgui.rs
new file mode 100644
index 0000000..0a50ff7
--- /dev/null
+++ b/engine/src/ui/dear_imgui.rs
@@ -0,0 +1,1081 @@
+use std::collections::HashMap;
+use std::path::{Path, PathBuf};
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::LazyLock;
+
+use dear_imgui_rs::{
+ DrawCmd as ImguiDrawCmd,
+ Key as ImguiKey,
+ MouseButton as ImguiMouseButton,
+};
+use ecs::component::local::Local;
+use ecs::component::Component;
+use ecs::error::Context as _;
+use ecs::event::component::{Changed, EventMatchExt};
+use ecs::pair::Pair;
+use ecs::query::term::With;
+use ecs::sole::Single;
+use ecs::system::initializable::Initializable;
+use ecs::system::observer::Observe;
+use ecs::system::Into;
+use ecs::time::Time;
+use ecs::{Component, Query, Sole};
+use intmap::IntMap;
+
+use crate::asset::{Assets, Handle as AssetHandle, Label as AssetLabel};
+use crate::data_types::dimens::Dimens;
+use crate::input::keyboard::{Key, Keyboard};
+use crate::input::mouse::{Button as MouseButton, Buttons as MouseButtons, Mouse};
+use crate::mesh::vertex_buffer::{
+ NamedVertexAttr as MeshNamedVertexAttr,
+ VertexAttrInfo as MeshVertexAttrInfo,
+ VertexBuffer as MeshVertexBuffer,
+ VertexLabel,
+};
+use crate::mesh::{Mesh, VertexAttrType as MeshVertexAttrType};
+use crate::projection::{
+ ClipVolume as ProjectionClipVolume,
+ Orthographic as OrthographicProjection,
+};
+use crate::reflection::Reflection;
+use crate::rendering::blending::{
+ Config as RenderingBlendingConfiguration,
+ Equation as RenderingBlendingEquation,
+ Factor as RenderingBlendingFactor,
+};
+use crate::rendering::object::Id as RenderingObjectId;
+use crate::rendering::shader::cursor::{
+ BindingTextureKind as ShaderBindingTextureKind,
+ BindingValue as ShaderBindingValue,
+ Cursor as ShaderCursor,
+};
+use crate::rendering::shader::{
+ Context as ShaderContext,
+ EntrypointFlags as ShaderEntrypointFlags,
+ ModuleSource as ShaderModuleSource,
+};
+use crate::rendering::{
+ AssetOrValue,
+ Command as RenderingCommand,
+ DrawMeshOptions,
+ DrawProperties,
+ DrawPropertiesUpdateFlags,
+ MeshUsage,
+ RenderPass,
+ RenderPasses,
+ ScissorBox,
+ SrgbaTextureDataType,
+ Surface,
+ SurfaceId,
+ TextureCreation,
+ TexturePixelDataFormat,
+ TextureUpdate,
+ PRE_RENDER_PHASE,
+};
+use crate::texture::Properties as TextureProperties;
+use crate::vector::Vec2;
+use crate::windowing::dpi::PhysicalSize;
+use crate::windowing::window::Window;
+
+mod reexports
+{
+ pub use dear_imgui_rs as bindings;
+}
+
+pub use reexports::*;
+
+pub static SHADER_ASSET_LABEL: LazyLock<AssetLabel> = LazyLock::new(|| AssetLabel {
+ path: Path::new("").into(),
+ name: Some("imgui_shader".into()),
+});
+
+/// Dear Imgui context
+#[derive(Sole)]
+pub struct Context
+{
+ pub enabled: bool,
+ ctx: inner_context_wrapper::InnerContextWrapper,
+ texture_lookup: IntMap<TextureLookupId, RenderingObjectId>,
+ texture_id_lookup:
+ HashMap<dear_imgui_rs::SnapshotTextureId, dear_imgui_rs::TextureId>,
+}
+
+impl Context
+{
+ pub fn frame(&mut self) -> Option<&mut bindings::Ui>
+ {
+ if !self.enabled {
+ return None;
+ }
+
+ self.ctx.get_frame()
+ }
+
+ pub fn register_texture(
+ &mut self,
+ texture_data: bindings::OwnedTextureData,
+ ) -> bindings::ManagedTextureId
+ {
+ self.ctx.register_texture(texture_data)
+ }
+}
+
+#[derive(Debug, Default)]
+#[non_exhaustive]
+pub struct Extension
+{
+ start_enabled: bool,
+ settings_ini_file_path: Option<PathBuf>,
+}
+
+impl Extension
+{
+ pub fn with_start_enabled(mut self, start_enabled: bool) -> Self
+ {
+ self.start_enabled = start_enabled;
+ self
+ }
+
+ pub fn with_settings_ini_file(
+ mut self,
+ settings_ini_file_path: Option<PathBuf>,
+ ) -> Self
+ {
+ self.settings_ini_file_path = settings_ini_file_path;
+ self
+ }
+}
+
+impl ecs::extension::Extension for Extension
+{
+ fn collect(self, mut collector: ecs::extension::Collector<'_>)
+ {
+ let mut context = Context {
+ enabled: self.start_enabled,
+ ctx: inner_context_wrapper::InnerContextWrapper::new(),
+ texture_lookup: IntMap::with_capacity(8),
+ texture_id_lookup: HashMap::with_capacity(8),
+ };
+
+ if let Err(err) = context
+ .ctx
+ .set_settings_ini_file_path(self.settings_ini_file_path)
+ {
+ tracing::error!("Failed to set path of imgui settings ini file: {err}");
+ }
+
+ assert!(
+ dear_imgui_rs::HAS_FREETYPE,
+ "Freetype font rasterizer support is not enabled"
+ );
+
+ unsafe {
+ context.ctx.font_atlas().add_font_from_memory_ttf(
+ include_bytes!("../../res/FiraMono-Regular.ttf"),
+ 14.0,
+ Some(
+ &dear_imgui_rs::FontConfig::default()
+ .name("FiraMono-Regular.ttf")
+ .font_loader_flags(
+ dear_imgui_rs::FontLoaderFlags::BITMAP
+ | dear_imgui_rs::FontLoaderFlags::FORCE_AUTOHINT,
+ ),
+ ),
+ None,
+ );
+ }
+
+ collector.add_sole(context).ok();
+
+ collector.add_system(
+ *PRE_RENDER_PHASE,
+ update.into_system().initialize((State::default(),)),
+ );
+
+ collector.add_observer(handle_window_changed);
+ }
+}
+
+#[tracing::instrument(skip_all)]
+fn handle_window_changed(
+ observe: Observe<Pair<Changed, Window>>,
+ mut context: Single<Context>,
+)
+{
+ let Ok(context) = context.get_mut() else {
+ unreachable!();
+ };
+
+ let Some(event_match) = observe
+ .iter()
+ .find(|event_match| event_match.get_entity().has_component(TargetWindow::id()))
+ else {
+ return;
+ };
+
+ let window = event_match.get_ent_target_comp();
+
+ let hidpi_factor = window.scale_factor().round();
+
+ context
+ .ctx
+ .get_io_mut()
+ .set_display_framebuffer_scale([hidpi_factor as f32, hidpi_factor as f32]);
+
+ let window_size_logical = PhysicalSize {
+ width: window.inner_size.width as f64,
+ height: window.inner_size.height as f64,
+ }
+ .to_logical::<f64>(hidpi_factor);
+
+ context.ctx.get_io_mut().set_display_size([
+ window_size_logical.width as f32,
+ window_size_logical.height as f32,
+ ]);
+}
+
+fn update(
+ target_window_query: Query<(&Window, &Surface, With<TargetWindow>)>,
+ mut context: Single<Context>,
+ time: Single<Time>,
+ mut assets: Single<Assets>,
+ mut render_passes: Single<RenderPasses>,
+ shader_context: Single<ShaderContext>,
+ keyboard: Single<Keyboard>,
+ mouse: Single<Mouse>,
+ mouse_buttons: Single<MouseButtons>,
+ mut state: Local<State>,
+) -> Result<(), crate::Error>
+{
+ let Ok(context) = context.get_mut() else {
+ unreachable!();
+ };
+
+ let assets = assets.get_mut()?;
+ let render_passes = render_passes.get_mut()?;
+ let shader_context = shader_context.get()?;
+ let keyboard = keyboard.get()?;
+ let mouse = mouse.get()?;
+ let mouse_buttons = mouse_buttons.get()?;
+
+ let Some((window, window_surface)) = target_window_query.iter().next() else {
+ return Ok(());
+ };
+
+ let curr_state = std::mem::replace(&mut *state, State::Unreachable);
+
+ match curr_state {
+ State::NotInitialized => {
+ let Some((shader_asset, mesh_obj_id, mesh)) = initialize_context(
+ context,
+ &window,
+ window_surface.id,
+ assets,
+ render_passes,
+ &shader_context,
+ ) else {
+ *state = State::NotInitialized;
+ return Ok(());
+ };
+
+ context.ctx.new_frame();
+
+ *state = State::Ready { shader_asset, mesh_obj_id, mesh };
+ }
+ State::Ready { shader_asset, mesh_obj_id, mut mesh } => {
+ if !context.enabled {
+ *state = State::Ready { shader_asset, mesh_obj_id, mesh };
+ return Ok(());
+ }
+
+ context
+ .ctx
+ .get_io_mut()
+ .set_delta_time(time.delta_time.as_secs_f32());
+
+ update_inputs(context, &window, keyboard, mouse, mouse_buttons);
+
+ add_drawing_render_pass(
+ context,
+ render_passes,
+ shader_context,
+ window_surface.id,
+ shader_asset.clone(),
+ mesh_obj_id,
+ &mut mesh,
+ )?;
+
+ context.ctx.new_frame();
+
+ *state = State::Ready { shader_asset, mesh_obj_id, mesh };
+ }
+ State::Unreachable => unreachable!(),
+ }
+
+ Ok(())
+}
+
+#[derive(Debug, Default, Clone, Copy, Component, Reflection)]
+pub struct TargetWindow;
+
+#[derive(Debug, Default, Component)]
+enum State
+{
+ #[default]
+ NotInitialized,
+ Ready
+ {
+ shader_asset: AssetHandle<ShaderModuleSource>,
+ mesh_obj_id: RenderingObjectId,
+ mesh: Mesh,
+ },
+ Unreachable,
+}
+
+fn initialize_context(
+ context: &mut Context,
+ window: &Window,
+ window_surface_id: SurfaceId,
+ assets: &mut Assets,
+ render_passes: &mut RenderPasses,
+ shader_context: &ShaderContext,
+) -> Option<(AssetHandle<ShaderModuleSource>, RenderingObjectId, Mesh)>
+{
+ let shader_asset = if let Some(shader_asset) =
+ assets.get_handle_to_loaded::<ShaderModuleSource>(SHADER_ASSET_LABEL.clone())
+ {
+ shader_asset
+ } else {
+ assets.store_with_label(
+ SHADER_ASSET_LABEL.clone(),
+ ShaderModuleSource {
+ name: "imgui_shader.slang".into(),
+ file_path: Path::new("@engine/imgui_shader").into(),
+ source: include_str!("../../res/imgui_shader.slang").into(),
+ link_entrypoints: ShaderEntrypointFlags::VERTEX
+ | ShaderEntrypointFlags::FRAGMENT,
+ },
+ );
+
+ // We want to wait with initializing until the shader have been processed by the
+ // shader context
+ return None;
+ };
+
+ let hidpi_factor = window.scale_factor().round();
+
+ context
+ .ctx
+ .get_io_mut()
+ .set_display_framebuffer_scale([hidpi_factor as f32, hidpi_factor as f32]);
+
+ let window_size = Dimens {
+ width: window.inner_size.width as f32,
+ height: window.inner_size.height as f32,
+ };
+
+ let window_logical_size = window_size / (hidpi_factor as f32);
+
+ context
+ .ctx
+ .get_io_mut()
+ .set_display_size([window_logical_size.width, window_logical_size.height]);
+
+ let mesh_obj_id = RenderingObjectId::new_sequential();
+
+ let mesh = Mesh::builder()
+ .vertices(MeshVertexBuffer::with_capacity(
+ &[
+ MeshVertexAttrInfo {
+ label: VertexLabel::Position,
+ ty: MeshVertexAttrType::Float32Array { length: 3 },
+ },
+ MeshVertexAttrInfo {
+ label: VertexLabel::Color,
+ ty: MeshVertexAttrType::Float32Array { length: 4 },
+ },
+ MeshVertexAttrInfo {
+ label: VertexLabel::UvFromTopLeft,
+ ty: MeshVertexAttrType::Float32Array { length: 2 },
+ },
+ ],
+ 128,
+ ))
+ .indices([])
+ .build();
+
+ render_passes.passes.push_back(RenderPass {
+ commands: vec![
+ RenderingCommand::MakeCurrent(window_surface_id),
+ RenderingCommand::CreateShaderProgram(
+ RenderingObjectId::Asset(shader_asset.id()),
+ shader_context
+ .get_program(&shader_asset.id())
+ .expect("Not possible")
+ .clone(),
+ ),
+ RenderingCommand::ActivateShader(RenderingObjectId::Asset(shader_asset.id())),
+ RenderingCommand::CreateMesh {
+ obj_id: mesh_obj_id,
+ mesh: AssetOrValue::Value(mesh.clone()),
+ usage: MeshUsage::Stream,
+ },
+ ],
+ draw_properties: DrawProperties::default(),
+ });
+
+ Some((shader_asset, mesh_obj_id, mesh))
+}
+
+fn update_inputs(
+ context: &mut Context,
+ window: &Window,
+ keyboard: &Keyboard,
+ mouse: &Mouse,
+ mouse_buttons: &MouseButtons,
+)
+{
+ let io = context.ctx.get_io_mut();
+
+ for (key, key_state) in keyboard.new_key_states() {
+ if let Some(key) = key_to_imgui_key(key) {
+ io.add_key_event(key, key_state.is_pressed());
+ }
+ }
+
+ for character in keyboard.text_keys().chars() {
+ if character == '\u{7f}' {
+ continue;
+ }
+
+ io.add_input_character(character);
+ }
+
+ io.add_mouse_source_event(dear_imgui_rs::input::MouseSource::Mouse);
+
+ let mouse_pos = mouse.position.to_logical::<f64>(window.scale_factor());
+
+ io.add_mouse_pos_event([mouse_pos.x as f32, mouse_pos.y as f32]);
+
+ if !mouse.curr_tick_scroll_delta.is_zero() {
+ io.add_mouse_wheel_event([
+ mouse.curr_tick_scroll_delta.hor_lines,
+ mouse.curr_tick_scroll_delta.vert_lines,
+ ]);
+ }
+
+ for (mouse_button, mouse_button_state) in mouse_buttons.all_current() {
+ if let Some(mouse_button) = mouse_button_to_imgui_mouse_button(mouse_button) {
+ io.add_mouse_button_event(mouse_button, mouse_button_state.is_pressed());
+ }
+ }
+}
+
+fn add_drawing_render_pass(
+ context: &mut Context,
+ render_passes: &mut RenderPasses,
+ shader_context: &ShaderContext,
+ window_surface_id: SurfaceId,
+ shader_asset: AssetHandle<ShaderModuleSource>,
+ mesh_obj_id: RenderingObjectId,
+ mesh: &mut Mesh,
+) -> Result<(), crate::Error>
+{
+ let render_pass = render_passes.passes.push_back_mut(RenderPass {
+ commands: Vec::new(),
+ draw_properties: DrawProperties {
+ blending_enabled: true,
+ blending_config: RenderingBlendingConfiguration {
+ equation: RenderingBlendingEquation::Add,
+ source_factor: RenderingBlendingFactor::SrcAlpha,
+ destination_factor: RenderingBlendingFactor::OneMinusSrcAlpha,
+ },
+ depth_test_enabled: false,
+ face_culling_enabled: false,
+ scissor_test_enabled: true,
+ ..Default::default()
+ },
+ });
+
+ let shader_program = shader_context
+ .get_program(&shader_asset.id())
+ .expect("Not possible");
+
+ let shader_cursor = ShaderCursor::new(
+ shader_program
+ .reflection(0)
+ .unwrap()
+ .global_params_var_layout()
+ .unwrap(),
+ );
+
+ let Context {
+ enabled: _,
+ ctx,
+ texture_lookup,
+ texture_id_lookup,
+ } = context;
+
+ let mut render_frame = ctx.render();
+
+ let mut tex_feedbacks = Vec::with_capacity(render_frame.texture_requests().len());
+
+ for texture_request in render_frame.texture_requests() {
+ match texture_request.operation() {
+ dear_imgui_rs::TextureOp::Create {
+ format,
+ width,
+ height,
+ row_pitch: _,
+ pixels,
+ } => {
+ if !matches!(format, dear_imgui_rs::TextureFormat::RGBA32) {
+ unimplemented!();
+ }
+
+ let texture_lookup_id = TextureLookupId::new();
+
+ tracing::trace!(
+ snapshot_tex_id = ?texture_request.texture(),
+ tex_lookup_id = ?texture_lookup_id,
+ "Performing texture operation: Create"
+ );
+
+ let texture_object_id = RenderingObjectId::new_sequential();
+
+ render_pass.commands.push(RenderingCommand::CreateTexture {
+ obj_id: texture_object_id,
+ pixel_data_format: TexturePixelDataFormat::Srgba(
+ SrgbaTextureDataType::UnsignedByte,
+ ),
+ creation: TextureCreation::Texture2D {
+ size: Dimens { width: *width, height: *height },
+ image: Some(pixels.clone().into_boxed_slice()),
+ },
+ properties: TextureProperties::default(),
+ });
+
+ texture_lookup.insert(texture_lookup_id, texture_object_id);
+
+ texture_id_lookup
+ .insert(texture_request.texture(), texture_lookup_id.into());
+
+ let texture_feedback =
+ match texture_request.uploaded(texture_lookup_id.into()) {
+ Ok(texture_feedback) => texture_feedback,
+ Err(
+ dear_imgui_rs::TextureFeedbackError::UploadForDestroy
+ | dear_imgui_rs::TextureFeedbackError::DestroyForUpload,
+ ) => unreachable!(),
+ };
+
+ tex_feedbacks.push(texture_feedback);
+ }
+ dear_imgui_rs::TextureOp::Update { format, width: _, height: _, rects } => {
+ if !matches!(format, dear_imgui_rs::TextureFormat::RGBA32) {
+ unimplemented!();
+ }
+
+ let texture_lookup_id =
+ TextureLookupId::from(texture_id_lookup[&texture_request.texture()]);
+
+ tracing::trace!(
+ snapshot_tex_id = ?texture_request.texture(),
+ tex_lookup_id = ?texture_lookup_id,
+ "Performing texture operation: Update"
+ );
+
+ let Some(texture_object_id) = texture_lookup.get(texture_lookup_id)
+ else {
+ tracing::error!(
+ snapshot_tex_id = ?texture_request.texture(),
+ tex_lookup_id = ?texture_lookup_id,
+ "Unknown texture. Skipping update",
+ );
+ continue;
+ };
+
+ render_pass.commands.reserve(rects.len());
+
+ for rect in rects {
+ render_pass.commands.push(RenderingCommand::UpdateTexture {
+ obj_id: *texture_object_id,
+ pixels: rect.data.clone().into_boxed_slice(),
+ pixel_data_format: TexturePixelDataFormat::Srgba(
+ SrgbaTextureDataType::UnsignedByte,
+ ),
+ update: TextureUpdate::Texture2D {
+ size: Dimens::<u32> {
+ width: rect.rect.w.into(),
+ height: rect.rect.h.into(),
+ },
+ offset: Vec2 {
+ x: rect.rect.x as u32,
+ y: rect.rect.y as u32,
+ },
+ },
+ });
+ }
+
+ let texture_feedback =
+ match texture_request.uploaded(texture_lookup_id.into()) {
+ Ok(texture_feedback) => texture_feedback,
+ Err(
+ dear_imgui_rs::TextureFeedbackError::UploadForDestroy
+ | dear_imgui_rs::TextureFeedbackError::DestroyForUpload,
+ ) => unreachable!(),
+ };
+
+ tex_feedbacks.push(texture_feedback);
+ }
+ dear_imgui_rs::TextureOp::Destroy => {
+ let texture_lookup_id =
+ TextureLookupId::from(texture_id_lookup[&texture_request.texture()]);
+
+ tracing::trace!(
+ snapshot_tex_id = ?texture_request.texture(),
+ tex_lookup_id = ?texture_lookup_id,
+ "Performing texture operation: Destroy"
+ );
+
+ let Some(texture_object_id) = texture_lookup.remove(texture_lookup_id)
+ else {
+ tracing::error!(
+ texture = ?texture_lookup_id,
+ "Unknown texture. Skipping destroyal",
+ );
+ continue;
+ };
+
+ render_pass
+ .commands
+ .push(RenderingCommand::RemoveTexture(texture_object_id));
+
+ let texture_feedback = match texture_request.destroyed() {
+ Ok(texture_feedback) => texture_feedback,
+ Err(
+ dear_imgui_rs::TextureFeedbackError::UploadForDestroy
+ | dear_imgui_rs::TextureFeedbackError::DestroyForUpload,
+ ) => unreachable!(),
+ };
+
+ tex_feedbacks.push(texture_feedback);
+ }
+ }
+ }
+
+ render_frame
+ .reconcile_texture_feedback(tex_feedbacks)
+ .with_context(|| "Failed to reconcile texture feedback")?;
+
+ let draw_data = render_frame.draw_data();
+
+ let [display_width, display_height] = draw_data.display_size();
+
+ // let [scale_width, scale_height] = draw_data.framebuffer_scale;
+
+ let shader_object_id = RenderingObjectId::Asset(shader_asset.id());
+
+ render_pass.commands.extend([
+ RenderingCommand::MakeCurrent(window_surface_id),
+ RenderingCommand::ActivateShader(shader_object_id),
+ RenderingCommand::SetShaderBinding(
+ shader_object_id,
+ shader_cursor
+ .field("Uniforms")
+ .field("projection")
+ .binding(
+ OrthographicProjection::builder()
+ .near(1.0)
+ .far(-1.0)
+ .viewport_origin(Vec2 { x: 0.0, y: 1.0 })
+ .size(crate::projection::OrthographicSize::FixedSize(Dimens {
+ width: display_width,
+ height: -display_height,
+ }))
+ .build()
+ .to_matrix_rh(
+ // Window size can be 0x0 since it will not be used
+ PhysicalSize::default(),
+ ProjectionClipVolume::NegOneToOne,
+ )
+ .into(),
+ )?,
+ ),
+ ]);
+
+ for draw_list in draw_data.draw_lists() {
+ mesh.vertex_buf_mut().clear();
+
+ for vertex in draw_list.vtx_buffer() {
+ mesh.vertex_buf_mut().push((
+ MeshNamedVertexAttr::<[f32; 3]> {
+ label: VertexLabel::Position,
+ value: [vertex.pos[0], vertex.pos[1], 0.0],
+ },
+ MeshNamedVertexAttr {
+ label: VertexLabel::Color,
+ value: vertex.rgba().map(|elem| (elem as f32) / 255.0),
+ },
+ MeshNamedVertexAttr {
+ label: VertexLabel::UvFromTopLeft,
+ value: vertex.uv,
+ },
+ ));
+ }
+
+ mesh.set_indices(draw_list.idx_buffer().iter().map(|index| (*index).into()));
+
+ render_pass.commands.push(RenderingCommand::UpdateMesh {
+ obj_id: mesh_obj_id,
+ mesh: mesh.clone(),
+ usage: MeshUsage::Stream,
+ });
+
+ for command in draw_list.commands() {
+ match command {
+ ImguiDrawCmd::Elements { count, cmd_params } => {
+ let Some(scissor_box) =
+ calc_draw_cmd_scissor_box(draw_data, &cmd_params)
+ else {
+ continue;
+ };
+
+ let tex_id = cmd_params.texture_id;
+
+ let Some(texture_object_id) =
+ texture_lookup.get(TextureLookupId::from(tex_id))
+ else {
+ tracing::error!(
+ "Unknown texture {}. Skipping skipping draw command",
+ tex_id.id()
+ );
+ continue;
+ };
+
+ render_pass.commands.extend([
+ RenderingCommand::UpdateDrawProperties(
+ DrawProperties { scissor_box, ..Default::default() },
+ DrawPropertiesUpdateFlags::SCISSOR_BOX,
+ ),
+ RenderingCommand::SetShaderBinding(
+ shader_object_id,
+ shader_cursor.field("main_texture").binding(
+ ShaderBindingValue::Texture(
+ *texture_object_id,
+ ShaderBindingTextureKind::Texture2D,
+ ),
+ )?,
+ ),
+ RenderingCommand::DrawMesh(
+ mesh_obj_id,
+ DrawMeshOptions::builder()
+ .element_offset(cmd_params.idx_offset.try_into().unwrap())
+ .vertex_offset(cmd_params.vtx_offset.try_into().unwrap())
+ .element_cnt(count.try_into().unwrap())
+ .build(),
+ ),
+ ]);
+ }
+ _ => {}
+ }
+ }
+ }
+
+ Ok(())
+}
+
+fn calc_draw_cmd_scissor_box(
+ draw_data: &dear_imgui_rs::DrawData,
+ cmd_params: &dear_imgui_rs::DrawCmdParams,
+) -> Option<ScissorBox>
+{
+ let clip_rect = cmd_params.clip_rect;
+
+ let display_size = draw_data.display_size();
+ let display_pos = draw_data.display_pos();
+ let framebuffer_scale = draw_data.framebuffer_scale();
+
+ let clip_min_x = (clip_rect[0] - display_pos[0]) * framebuffer_scale[0];
+
+ let clip_min_y = (clip_rect[1] - display_pos[1]) * framebuffer_scale[1];
+
+ let clip_max_x = (clip_rect[2] - display_pos[0]) * framebuffer_scale[0];
+
+ let clip_max_y = (clip_rect[3] - display_pos[1]) * framebuffer_scale[1];
+
+ if clip_max_x <= clip_min_x || clip_max_y <= clip_min_y {
+ return None;
+ }
+
+ let fb_height = display_size[1] * framebuffer_scale[1];
+
+ Some(ScissorBox {
+ size: Some(Dimens {
+ width: (clip_max_x - clip_min_x) as u16,
+ height: (clip_max_y - clip_min_y) as u16,
+ }),
+ lower_left_corner_pos: Vec2 {
+ x: clip_min_x as u16,
+ y: (fb_height - clip_max_y) as u16,
+ },
+ })
+}
+
+fn key_to_imgui_key(key: Key) -> Option<ImguiKey>
+{
+ match key {
+ Key::Backquote => Some(ImguiKey::GraveAccent),
+ Key::Backslash => Some(ImguiKey::Backslash),
+ Key::BracketLeft => Some(ImguiKey::LeftBracket),
+ Key::BracketRight => Some(ImguiKey::RightBracket),
+ Key::Comma => Some(ImguiKey::Comma),
+ Key::Digit0 => Some(ImguiKey::Key0),
+ Key::Digit1 => Some(ImguiKey::Key1),
+ Key::Digit2 => Some(ImguiKey::Key2),
+ Key::Digit3 => Some(ImguiKey::Key3),
+ Key::Digit4 => Some(ImguiKey::Key4),
+ Key::Digit5 => Some(ImguiKey::Key5),
+ Key::Digit6 => Some(ImguiKey::Key6),
+ Key::Digit7 => Some(ImguiKey::Key7),
+ Key::Digit8 => Some(ImguiKey::Key8),
+ Key::Digit9 => Some(ImguiKey::Key9),
+ Key::Equal => Some(ImguiKey::Equal),
+ Key::A => Some(ImguiKey::A),
+ Key::B => Some(ImguiKey::B),
+ Key::C => Some(ImguiKey::C),
+ Key::D => Some(ImguiKey::D),
+ Key::E => Some(ImguiKey::E),
+ Key::F => Some(ImguiKey::F),
+ Key::G => Some(ImguiKey::G),
+ Key::H => Some(ImguiKey::H),
+ Key::I => Some(ImguiKey::I),
+ Key::J => Some(ImguiKey::J),
+ Key::K => Some(ImguiKey::K),
+ Key::L => Some(ImguiKey::L),
+ Key::M => Some(ImguiKey::M),
+ Key::N => Some(ImguiKey::N),
+ Key::O => Some(ImguiKey::O),
+ Key::P => Some(ImguiKey::P),
+ Key::Q => Some(ImguiKey::Q),
+ Key::R => Some(ImguiKey::R),
+ Key::S => Some(ImguiKey::S),
+ Key::T => Some(ImguiKey::T),
+ Key::U => Some(ImguiKey::U),
+ Key::V => Some(ImguiKey::V),
+ Key::W => Some(ImguiKey::W),
+ Key::X => Some(ImguiKey::X),
+ Key::Y => Some(ImguiKey::Y),
+ Key::Z => Some(ImguiKey::Z),
+ Key::Minus => Some(ImguiKey::Minus),
+ Key::Period => Some(ImguiKey::Period),
+ Key::Quote => Some(ImguiKey::Apostrophe),
+ Key::Semicolon => Some(ImguiKey::Semicolon),
+ Key::Slash => Some(ImguiKey::Slash),
+ Key::AltLeft => Some(ImguiKey::LeftAlt),
+ Key::AltRight => Some(ImguiKey::RightAlt),
+ Key::Backspace => Some(ImguiKey::Backspace),
+ Key::CapsLock => Some(ImguiKey::CapsLock),
+ Key::ControlLeft => Some(ImguiKey::LeftCtrl),
+ Key::ControlRight => Some(ImguiKey::RightCtrl),
+ Key::Enter => Some(ImguiKey::Enter),
+ Key::SuperLeft => Some(ImguiKey::LeftSuper),
+ Key::SuperRight => Some(ImguiKey::RightSuper),
+ Key::ShiftLeft => Some(ImguiKey::LeftShift),
+ Key::ShiftRight => Some(ImguiKey::RightShift),
+ Key::Space => Some(ImguiKey::Space),
+ Key::Tab => Some(ImguiKey::Tab),
+ Key::Delete => Some(ImguiKey::Delete),
+ Key::End => Some(ImguiKey::End),
+ Key::Home => Some(ImguiKey::Home),
+ Key::Insert => Some(ImguiKey::Insert),
+ Key::PageDown => Some(ImguiKey::PageDown),
+ Key::PageUp => Some(ImguiKey::PageUp),
+ Key::ArrowDown => Some(ImguiKey::DownArrow),
+ Key::ArrowLeft => Some(ImguiKey::LeftArrow),
+ Key::ArrowRight => Some(ImguiKey::RightArrow),
+ Key::ArrowUp => Some(ImguiKey::UpArrow),
+ Key::NumLock => Some(ImguiKey::NumLock),
+ Key::Numpad0 => Some(ImguiKey::Keypad0),
+ Key::Numpad1 => Some(ImguiKey::Keypad1),
+ Key::Numpad2 => Some(ImguiKey::Keypad2),
+ Key::Numpad3 => Some(ImguiKey::Keypad3),
+ Key::Numpad4 => Some(ImguiKey::Keypad4),
+ Key::Numpad5 => Some(ImguiKey::Keypad5),
+ Key::Numpad6 => Some(ImguiKey::Keypad6),
+ Key::Numpad7 => Some(ImguiKey::Keypad7),
+ Key::Numpad8 => Some(ImguiKey::Keypad8),
+ Key::Numpad9 => Some(ImguiKey::Keypad9),
+ Key::NumpadAdd => Some(ImguiKey::KeypadAdd),
+ Key::NumpadDecimal => Some(ImguiKey::KeypadDecimal),
+ Key::NumpadDivide => Some(ImguiKey::KeypadDivide),
+ Key::NumpadEnter => Some(ImguiKey::KeypadEnter),
+ Key::NumpadEqual => Some(ImguiKey::KeypadEqual),
+ Key::NumpadMultiply => Some(ImguiKey::KeypadMultiply),
+ Key::NumpadSubtract => Some(ImguiKey::KeypadSubtract),
+ Key::Escape => Some(ImguiKey::Escape),
+ Key::PrintScreen => Some(ImguiKey::PrintScreen),
+ Key::ScrollLock => Some(ImguiKey::ScrollLock),
+ Key::Pause => Some(ImguiKey::Pause),
+ Key::Meta => Some(ImguiKey::ModSuper),
+ Key::F1 => Some(ImguiKey::F1),
+ Key::F2 => Some(ImguiKey::F2),
+ Key::F3 => Some(ImguiKey::F3),
+ Key::F4 => Some(ImguiKey::F4),
+ Key::F5 => Some(ImguiKey::F5),
+ Key::F6 => Some(ImguiKey::F6),
+ Key::F7 => Some(ImguiKey::F7),
+ Key::F8 => Some(ImguiKey::F8),
+ Key::F9 => Some(ImguiKey::F9),
+ Key::F10 => Some(ImguiKey::F10),
+ Key::F11 => Some(ImguiKey::F11),
+ Key::F12 => Some(ImguiKey::F12),
+ _ => None,
+ }
+}
+
+fn mouse_button_to_imgui_mouse_button(
+ mouse_button: MouseButton,
+) -> Option<ImguiMouseButton>
+{
+ match mouse_button {
+ MouseButton::Left | MouseButton::Other(0) => Some(ImguiMouseButton::Left),
+ MouseButton::Right | MouseButton::Other(1) => Some(ImguiMouseButton::Right),
+ MouseButton::Middle | MouseButton::Other(2) => Some(ImguiMouseButton::Middle),
+ MouseButton::Other(3) => Some(ImguiMouseButton::Extra1),
+ MouseButton::Other(4) => Some(ImguiMouseButton::Extra2),
+ _ => None,
+ }
+}
+
+mod inner_context_wrapper
+{
+ use std::path::PathBuf;
+ use std::pin::Pin;
+ use std::ptr::null_mut;
+
+ pub struct InnerContextWrapper
+ {
+ ctx: Pin<Box<dear_imgui_rs::Context>>,
+ frame: *mut dear_imgui_rs::Ui,
+ _renderer_consumer: dear_imgui_rs::RendererConsumer,
+ }
+
+ impl InnerContextWrapper
+ {
+ pub fn new() -> Self
+ {
+ let mut ctx = Box::pin(dear_imgui_rs::Context::create());
+
+ let _ = ctx.set_platform_name(Some("engine-windowing"));
+ let _ = ctx.set_renderer_name(Some("engine-rendering"));
+
+ ctx.io_mut()
+ .set_backend_flags(dear_imgui_rs::BackendFlags::RENDERER_HAS_TEXTURES);
+
+ let _renderer_consumer = ctx.create_renderer_consumer().unwrap();
+
+ Self {
+ ctx,
+ frame: null_mut(),
+ _renderer_consumer,
+ }
+ }
+
+ pub fn set_settings_ini_file_path(
+ &mut self,
+ path: Option<PathBuf>,
+ ) -> Result<(), dear_imgui_rs::ImGuiError>
+ {
+ self.ctx.set_ini_filename(path)
+ }
+
+ pub fn register_texture(
+ &mut self,
+ texture_data: dear_imgui_rs::texture::OwnedTextureData,
+ ) -> dear_imgui_rs::ManagedTextureId
+ {
+ self.ctx.register_texture(texture_data)
+ }
+
+ pub fn get_io_mut(&mut self) -> &mut dear_imgui_rs::Io
+ {
+ self.ctx.io_mut()
+ }
+
+ pub fn font_atlas(&self) -> &dear_imgui_rs::FontAtlas
+ {
+ self.ctx.font_atlas()
+ }
+
+ // pub fn create_renderer_consumer(&mut self) -> dear_imgui_rs::RendererConsumer
+ // {
+ // self.ctx.create_renderer_consumer().unwrap()
+ // }
+
+ pub fn render(&mut self) -> dear_imgui_rs::RenderedFrame<'_>
+ {
+ self.ctx.render()
+ }
+
+ pub fn new_frame(&mut self)
+ {
+ let frame = &raw mut *self.ctx.frame();
+
+ self.frame = frame;
+ }
+
+ pub fn get_frame(&mut self) -> Option<&mut dear_imgui_rs::Ui>
+ {
+ unsafe { self.frame.as_mut() }
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+struct TextureLookupId
+{
+ inner: u64,
+}
+
+impl TextureLookupId
+{
+ fn new() -> Self
+ {
+ static NEXT: AtomicU64 = AtomicU64::new(1);
+
+ Self {
+ inner: NEXT.fetch_add(1, Ordering::Relaxed),
+ }
+ }
+}
+
+impl intmap::IntKey for TextureLookupId
+{
+ type Int = u64;
+
+ const PRIME: Self::Int = <u64 as intmap::IntKey>::PRIME;
+
+ fn into_int(self) -> Self::Int
+ {
+ self.inner
+ }
+}
+
+impl From<dear_imgui_rs::TextureId> for TextureLookupId
+{
+ fn from(texture_id: dear_imgui_rs::TextureId) -> Self
+ {
+ assert!(!texture_id.is_null(), "Bad texture ID: is null");
+
+ Self { inner: texture_id.id() }
+ }
+}
+
+impl From<TextureLookupId> for dear_imgui_rs::TextureId
+{
+ fn from(texture_id: TextureLookupId) -> Self
+ {
+ Self::new(texture_id.inner)
+ }
+}
diff --git a/engine/src/ui/view.rs b/engine/src/ui/view.rs
new file mode 100644
index 0000000..42601a3
--- /dev/null
+++ b/engine/src/ui/view.rs
@@ -0,0 +1,2 @@
+pub mod transform_3d;
+pub mod world;
diff --git a/engine/src/ui/view/transform_3d.rs b/engine/src/ui/view/transform_3d.rs
new file mode 100644
index 0000000..a426b2a
--- /dev/null
+++ b/engine/src/ui/view/transform_3d.rs
@@ -0,0 +1,128 @@
+use dear_imguizmo::GuizmoExt;
+
+use crate::camera::{Active as ActiveCamera, Camera};
+use crate::data_types::dimens::Dimens3;
+use crate::ecs::pair::ChildOf;
+use crate::ecs::query::term::{Traverse, TraverseUp, With, Without};
+use crate::ecs::sole::Single;
+use crate::ecs::Query;
+use crate::projection::ClipVolume as ProjectionClipVolume;
+use crate::scene::{Active as ActiveScene, Scene};
+use crate::transform::Transform;
+use crate::ui::dear_imgui::{
+ Context as DearImguiContext,
+ TargetWindow as DearImguiTargetWindow,
+};
+use crate::vector::Vec3;
+use crate::windowing::window::{Closed as WindowClosed, Window};
+
+pub fn show(
+ subject_query: Query<(
+ &mut Transform,
+ Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>,
+ Without<ActiveCamera>,
+ )>,
+ camera_query: Query<(
+ &Camera,
+ &Transform,
+ Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>,
+ With<ActiveCamera>,
+ )>,
+ target_window_query: Query<(
+ &Window,
+ With<DearImguiTargetWindow>,
+ Without<WindowClosed>,
+ )>,
+ mut dear_imgui_context: Single<DearImguiContext>,
+) -> Result<(), crate::Error>
+{
+ let dear_imgui_context = dear_imgui_context.get_mut()?;
+
+ let Some(frame) = dear_imgui_context.frame() else {
+ return Ok(());
+ };
+
+ let Some((camera, camera_world_pos)) = camera_query.iter().next() else {
+ return Ok(());
+ };
+
+ let Some((target_window,)) = target_window_query.iter().next() else {
+ return Ok(());
+ };
+
+ let gizmo = frame.guizmo();
+
+ let [display_width, display_height] = frame.io().display_size();
+
+ gizmo.set_rect(0.0, 0.0, display_width, display_height);
+
+ // The widgets should appear under dear imgui windows
+ gizmo.set_drawlist_background();
+
+ let col_major_view = flatten_matrix(
+ camera
+ .to_view_matrix(camera_world_pos.position)
+ .to_column_major(),
+ );
+
+ let col_major_projection = flatten_matrix(
+ camera
+ .projection
+ .to_matrix_rh(target_window.inner_size, ProjectionClipVolume::NegOneToOne)
+ .to_column_major(),
+ );
+
+ for (subject_index, (mut subject_transform,)) in subject_query.iter().enumerate() {
+ let subject_index = match i32::try_from(subject_index) {
+ Ok(subject_index) => subject_index,
+ Err(err) => {
+ tracing::error!("Invalid subject index {subject_index}: {err}");
+ break;
+ }
+ };
+
+ // No rotation
+ let rotation = [0.0, 0.0, 0.0];
+
+ let mut col_major_model_mat = dear_imguizmo::recompose_matrix(
+ &subject_transform.position.into(),
+ &rotation,
+ &subject_transform.scale.into(),
+ );
+
+ let _id_token = gizmo.push_id(subject_index);
+
+ let used = gizmo
+ .manipulate_config(
+ &col_major_view,
+ &col_major_projection,
+ &mut col_major_model_mat,
+ )
+ .operation(
+ dear_imguizmo::Operation::TRANSLATE | dear_imguizmo::Operation::SCALE,
+ )
+ .mode(dear_imguizmo::Mode::World)
+ .build();
+
+ if used {
+ let (translation, _rotation, scale) =
+ dear_imguizmo::decompose_matrix(&col_major_model_mat);
+
+ subject_transform.position = Vec3::from(translation);
+ subject_transform.scale = Dimens3::from(scale);
+ }
+ }
+
+ Ok(())
+}
+
+fn flatten_matrix(mat: [[f32; 4]; 4]) -> [f32; 4 * 4]
+{
+ let flattened = mat.as_flattened();
+
+ let Some(flattened) = flattened.as_array::<{ 4 * 4 }>() else {
+ unreachable!();
+ };
+
+ flattened.clone()
+}
diff --git a/engine/src/ui/view/world.rs b/engine/src/ui/view/world.rs
new file mode 100644
index 0000000..81c2377
--- /dev/null
+++ b/engine/src/ui/view/world.rs
@@ -0,0 +1,2314 @@
+use std::any::{Any, TypeId};
+use std::borrow::Cow;
+use std::io::Cursor;
+use std::ops::{Deref, DerefMut};
+
+use crate::color::{Rgb, Rgba};
+use crate::ecs::actions::Actions;
+use crate::ecs::component::local::Local;
+use crate::ecs::component::{
+ Handle as ComponentHandle,
+ Info as ComponentInfo,
+ Parts as ComponentParts,
+};
+use crate::ecs::entity::{Handle as EntityHandle, Name as EntityName};
+use crate::ecs::sole::Single;
+use crate::ecs::uid::Uid;
+use crate::ecs::util::Either;
+use crate::ecs::{Component, Query, World};
+use crate::image::{Format as ImageFormat, Image};
+use crate::reflection::{
+ Enum as EnumReflection,
+ EnumVariant as EnumVariantReflection,
+ GetError as ReflectionGetError,
+ LiteralType,
+ Struct as StructReflection,
+ Type as TypeReflection,
+ Visibility,
+};
+use crate::ui::dear_imgui::{
+ bindings as dear_imgui_bindings,
+ Context as DearImguiContext,
+};
+
+const BUTTON_RED_NORMAL: [f32; 4] = [124.0, 52.0, 39.0, 255.0];
+const BUTTON_RED_HOVERED: [f32; 4] = [181.0, 76.0, 57.0, 255.0];
+const BUTTON_RED_ACTIVE: [f32; 4] = [162.0, 68.0, 51.0, 255.0];
+
+#[derive(Default, Component)]
+pub struct State
+{
+ spawning_entity: Option<Uid>,
+ popup_state: Option<PopupState>,
+ textures: Option<Textures>,
+}
+
+pub fn show(
+ query: Query<()>,
+ mut dear_imgui_context: Single<DearImguiContext>,
+ mut state: Local<State>,
+ world: &World,
+ mut actions: Actions,
+) -> Result<(), crate::Error>
+{
+ let dear_imgui_context = dear_imgui_context.get_mut()?;
+
+ let State {
+ spawning_entity,
+ popup_state,
+ textures,
+ } = &mut *state;
+
+ let textures = textures.get_or_insert_with(|| Textures::new(dear_imgui_context));
+
+ let Some(frame) = dear_imgui_context.frame() else {
+ return Ok(());
+ };
+
+ frame
+ .window("World")
+ .size([600.0, 400.0], dear_imgui_bindings::Condition::Once)
+ .build(|| {
+ create_spawn_button_widgets(
+ frame,
+ spawning_entity,
+ popup_state,
+ world,
+ &mut actions,
+ );
+
+ frame.spacing();
+ frame.spacing();
+
+ for ent_handle in &query.into_flexible_query() {
+ create_entity_widgets(
+ frame,
+ popup_state,
+ textures,
+ &ent_handle,
+ world,
+ &mut actions,
+ );
+ }
+
+ let popup_closed = match popup_state {
+ Some(PopupState::AddComponent(popup_state)) => {
+ show_add_component_popup(frame, popup_state, &mut actions)
+ }
+ Some(PopupState::RenameEntity(popup_state)) => {
+ show_rename_entity_popup(frame, popup_state, &mut actions, world)
+ }
+ _ => false,
+ };
+
+ if popup_closed {
+ *popup_state = None;
+ }
+ });
+
+ Ok(())
+}
+
+fn create_texture(
+ dear_imgui_context: &mut DearImguiContext,
+ image: Image,
+) -> dear_imgui_bindings::ManagedTextureId
+{
+ let image = image.into_rgba8();
+
+ let mut texture_data = dear_imgui_bindings::OwnedTextureData::new();
+
+ texture_data.create(
+ dear_imgui_bindings::TextureFormat::RGBA32,
+ image.dimensions().width,
+ image.dimensions().height,
+ );
+
+ texture_data.set_data(image.as_bytes());
+
+ dear_imgui_context.register_texture(texture_data)
+}
+
+fn create_spawn_button_widgets(
+ frame: &dear_imgui_bindings::Ui,
+ spawning_entity: &mut Option<Uid>,
+ popup_state: &mut Option<PopupState>,
+ world: &World,
+ actions: &mut Actions,
+)
+{
+ let _disabled_token = frame.begin_disabled_with_cond(spawning_entity.is_some());
+
+ if frame.button("Spawn") {
+ let new_ent_id = actions.spawn(());
+
+ *spawning_entity = Some(new_ent_id);
+ }
+
+ if let Some(spawning_ent) = &spawning_entity {
+ if let Some(spawned_ent) = world.get_entity(*spawning_ent) {
+ let spawned_ent_title = create_entity_title(&spawned_ent);
+
+ if enter_add_component_popup_state(
+ popup_state,
+ &spawned_ent,
+ &spawned_ent_title,
+ world,
+ ) {
+ *spawning_entity = None;
+ }
+ }
+ }
+}
+
+fn create_entity_widgets(
+ frame: &dear_imgui_bindings::Ui,
+ popup_state: &mut Option<PopupState>,
+ textures: &Textures,
+ ent_handle: &EntityHandle,
+ world: &World,
+ actions: &mut Actions,
+)
+{
+ if ent_handle.component_ids().next().is_some()
+ && !ent_handle.component_ids().any(|comp_id| {
+ world.get_entity(comp_id).is_some_and(|comp_ent| {
+ comp_ent
+ .get::<ComponentInfo>()
+ .is_some_and(|comp_info| comp_info.type_reflection.is_some())
+ })
+ })
+ {
+ return;
+ }
+
+ let ent_title = create_entity_title(&ent_handle);
+
+ let mut is_open = false;
+
+ frame
+ .table(&format!("ent_{}_table", ent_handle.uid()))
+ .headers(false)
+ .sizing_policy(dear_imgui_bindings::TableSizingPolicy::FixedFit)
+ .columns([
+ dear_imgui_bindings::TableColumnSetup::new("a")
+ .fixed_width(frame.content_region_avail_width() * 0.8),
+ dear_imgui_bindings::TableColumnSetup::new("b"),
+ dear_imgui_bindings::TableColumnSetup::new("c"),
+ dear_imgui_bindings::TableColumnSetup::new("d"),
+ ])
+ .build(|frame| {
+ frame.table_next_row();
+
+ frame.table_next_column();
+
+ is_open = frame
+ .collapsing_header(&ent_title, dear_imgui_bindings::TreeNodeFlags::NONE);
+
+ frame.table_next_column();
+
+ create_add_component_button_widget(
+ frame,
+ &ent_handle,
+ &ent_title,
+ popup_state,
+ world,
+ );
+
+ frame.table_next_column();
+
+ create_despawn_button_widget(frame, textures, ent_handle, actions);
+
+ frame.table_next_column();
+
+ create_rename_entity_button_widget(
+ frame,
+ textures,
+ ent_handle,
+ &ent_title,
+ popup_state,
+ );
+ });
+
+ if is_open {
+ for component_id in ent_handle.component_ids() {
+ if component_id.is_pair() {
+ create_pair_component_widgets(
+ frame,
+ textures,
+ ent_handle,
+ component_id,
+ world,
+ actions,
+ );
+
+ frame.spacing();
+
+ continue;
+ }
+
+ create_component_widgets(frame, ent_handle, component_id, world, actions);
+ }
+ }
+}
+
+fn create_component_widgets(
+ frame: &dear_imgui_bindings::Ui,
+ ent_handle: &EntityHandle,
+ component_id: Uid,
+ world: &World,
+ actions: &mut Actions,
+)
+{
+ let Some(component_info) = world
+ .get_entity(component_id)
+ .and_then(|comp_ent| comp_ent.get::<ComponentInfo>())
+ else {
+ return;
+ };
+
+ let Some(component_type) = component_info.type_reflection else {
+ return;
+ };
+
+ let Some(mut component) = ent_handle.get_any_by_id_mut(component_id) else {
+ unreachable!();
+ };
+
+ {
+ let _color_token = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::Button,
+ BUTTON_RED_NORMAL.map(|num| num / 255.0),
+ );
+
+ let _color_token_b = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::ButtonHovered,
+ BUTTON_RED_HOVERED.map(|num| num / 255.0),
+ );
+
+ let _color_token_c = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::ButtonActive,
+ BUTTON_RED_ACTIVE.map(|num| num / 255.0),
+ );
+
+ if frame.small_button(format!(
+ "X##world_view_entity_{}_remove_component_button_{}",
+ ent_handle.uid(),
+ component_info.name
+ )) {
+ actions.remove_components(ent_handle.uid(), [component_id]);
+ }
+ }
+
+ frame.same_line();
+
+ let mut component_changed = false;
+
+ create_item_title_widget(
+ frame,
+ component_info.name,
+ &ItemType::Reflected(component_type),
+ None,
+ );
+
+ let item_type = ItemType::Reflected(component_type);
+
+ let _token = item_type.indent_or_same_line(frame);
+
+ add_item_to_frame(
+ frame,
+ ItemRef::Mutable(&mut *component),
+ &mut component_changed,
+ ItemInfo {
+ item_title: component_info.name,
+ item_tag: &format!(
+ "world_view_entity_{}_component_{}",
+ ent_handle.uid(),
+ component_info.name,
+ ),
+ item_type_name: None,
+ item_type,
+ item_is_read_only: false,
+ },
+ &[],
+ );
+
+ frame.spacing();
+
+ if component_changed {
+ component.set_changed();
+ }
+}
+
+fn create_pair_component_widgets(
+ frame: &dear_imgui_bindings::Ui,
+ textures: &Textures,
+ ent_handle: &EntityHandle,
+ pair_id: Uid,
+ world: &World,
+ actions: &mut Actions,
+)
+{
+ {
+ let _color_token = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::Button,
+ BUTTON_RED_NORMAL.map(|num| num / 255.0),
+ );
+
+ let _color_token_b = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::ButtonHovered,
+ BUTTON_RED_HOVERED.map(|num| num / 255.0),
+ );
+
+ let _color_token_c = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::ButtonActive,
+ BUTTON_RED_ACTIVE.map(|num| num / 255.0),
+ );
+
+ if frame.small_button(format!(
+ "X##world_view_entity_{}_remove_component_button_{}",
+ ent_handle.uid(),
+ pair_id
+ )) {
+ actions.remove_components(ent_handle.uid(), [pair_id]);
+ }
+ }
+
+ frame.same_line();
+
+ let relation_comp_info_or_ent_name =
+ get_comp_info_or_ent_name(world, pair_id.relation());
+
+ let relation_name: Cow<'_, str> = match &relation_comp_info_or_ent_name {
+ Some(Either::A(comp_info)) => comp_info.name.into(),
+ Some(Either::B(ent_name)) => format!("🌐 {}", ent_name.name).into(),
+ None => format!("<unnamed> {}", pair_id.relation()).into(),
+ };
+
+ let target_comp_info_or_ent_name = get_comp_info_or_ent_name(world, pair_id.target());
+
+ let target_name: Cow<'_, str> = match &target_comp_info_or_ent_name {
+ Some(Either::A(comp_info)) => comp_info.name.into(),
+ Some(Either::B(ent_name)) => format!("🌐 {}", ent_name.name).into(),
+ None => format!("<unnamed> {}", pair_id.target()).into(),
+ };
+
+ // TODO: Make relation & target ids editable
+
+ frame.text(format!("({relation_name}, {target_name})"));
+
+ let Some(mut pair_data) = ent_handle.get_any_by_id_mut(pair_id) else {
+ unreachable!();
+ };
+
+ let pair_data_ty_id = (*pair_data).type_id();
+
+ let Some(pair_data_comp_info) = [
+ &relation_comp_info_or_ent_name,
+ &target_comp_info_or_ent_name,
+ ]
+ .map(Option::as_ref)
+ .map(|item| item.and_then(Either::as_a))
+ .into_iter()
+ .flatten()
+ .find(|comp_info| comp_info.ty_id == pair_data_ty_id) else {
+ return;
+ };
+
+ let Some(pair_data_ty) = pair_data_comp_info.type_reflection else {
+ frame.same_line();
+
+ frame.image(textures.warning_icon_tex_id, [16.0, 16.0]);
+
+ if frame.is_item_hovered() {
+ frame.tooltip_text(format!(
+ "Pair data type {} does not have reflection",
+ pair_data_comp_info.name
+ ));
+ }
+
+ return;
+ };
+
+ let _indent = frame.begin_indent();
+
+ let mut component_changed = false;
+
+ create_item_title_widget(
+ frame,
+ pair_data_comp_info.name,
+ &ItemType::Reflected(pair_data_ty),
+ None,
+ );
+
+ let item_type = ItemType::Reflected(pair_data_ty);
+
+ let _token = item_type.indent_or_same_line(frame);
+
+ add_item_to_frame(
+ frame,
+ ItemRef::Mutable(&mut *pair_data),
+ &mut component_changed,
+ ItemInfo {
+ item_title: pair_data_comp_info.name,
+ item_tag: &format!(
+ "world_view_entity_{}_pair_component_{pair_id}",
+ ent_handle.uid(),
+ ),
+ item_type_name: None,
+ item_type,
+ item_is_read_only: false,
+ },
+ &[],
+ );
+
+ // The pair component is not set as changed here since events do not support
+ // pairs as event targets
+}
+
+fn get_comp_info_or_ent_name(
+ world: &World,
+ id: Uid,
+) -> Option<Either<ComponentHandle<'_, ComponentInfo>, ComponentHandle<'_, EntityName>>>
+{
+ let ent = world.get_entity(id)?;
+
+ if let Some(component_info) = ent.get::<ComponentInfo>() {
+ return Some(Either::A(component_info));
+ }
+
+ if let Some(entity_name) = ent.get::<EntityName>() {
+ return Some(Either::B(entity_name));
+ }
+
+ None
+}
+
+fn create_add_component_button_widget(
+ frame: &dear_imgui_bindings::Ui,
+ ent_handle: &EntityHandle,
+ ent_title: &str,
+ popup_state: &mut Option<PopupState>,
+ world: &World,
+)
+{
+ let _color_token = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::Button,
+ [49.0, 89.0, 28.0, 255.0].map(|num| num / 255.0),
+ );
+
+ let _color_token_b = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::ButtonHovered,
+ [100.0, 181.0, 57.0, 255.0].map(|num| num / 255.0),
+ );
+
+ let _color_token_c = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::ButtonActive,
+ [90.0, 162.0, 51.0, 255.0].map(|num| num / 255.0),
+ );
+
+ let is_button_clicked = frame.button(format!(
+ "+##add_component_to_entity_button_{}",
+ ent_handle.uid()
+ ));
+
+ if is_button_clicked {
+ enter_add_component_popup_state(popup_state, ent_handle, ent_title, world);
+ }
+}
+
+fn create_despawn_button_widget(
+ frame: &dear_imgui_bindings::Ui,
+ textures: &Textures,
+ ent_handle: &EntityHandle,
+ actions: &mut Actions,
+)
+{
+ let _color_token = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::Button,
+ BUTTON_RED_NORMAL.map(|num| num / 255.0),
+ );
+
+ let _color_token_b = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::ButtonHovered,
+ BUTTON_RED_HOVERED.map(|num| num / 255.0),
+ );
+
+ let _color_token_c = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::ButtonActive,
+ BUTTON_RED_ACTIVE.map(|num| num / 255.0),
+ );
+
+ if frame
+ .image_button_config(
+ &format!("ent_{}_despawn_button", ent_handle.uid()),
+ textures.despawn_icon_tex_id,
+ [14.0, 14.0],
+ )
+ .build()
+ {
+ actions.despawn(ent_handle.uid());
+ }
+}
+
+fn create_rename_entity_button_widget(
+ frame: &dear_imgui_bindings::Ui,
+ textures: &Textures,
+ ent_handle: &EntityHandle,
+ ent_title: &str,
+ popup_state: &mut Option<PopupState>,
+)
+{
+ if frame
+ .image_button_config(
+ &format!("ent_{}_rename_button", ent_handle.uid()),
+ textures.edit_icon_tex_id,
+ [16.0, 16.0],
+ )
+ .build()
+ {
+ enter_rename_entity_popup_state(popup_state, ent_handle, ent_title);
+ }
+}
+
+fn enter_rename_entity_popup_state(
+ popup_state: &mut Option<PopupState>,
+ ent_handle: &EntityHandle,
+ ent_title: &str,
+) -> bool
+{
+ if popup_state.is_some() {
+ return false;
+ }
+
+ *popup_state = Some(PopupState::RenameEntity(RenameEntityPopupState {
+ popup_name: format!("Rename entity {ent_title}"),
+ target_entity_id: ent_handle.uid(),
+ new_entity_name: String::with_capacity(32),
+ }));
+
+ true
+}
+
+fn show_rename_entity_popup(
+ frame: &dear_imgui_bindings::Ui,
+ popup_state: &mut RenameEntityPopupState,
+ actions: &mut Actions,
+ world: &World,
+) -> bool
+{
+ frame.open_popup(&popup_state.popup_name);
+
+ let mut is_opened = true;
+
+ let should_close =
+ frame.modal_popup_with_opened(&popup_state.popup_name, &mut is_opened, || {
+ frame.set_window_size([
+ frame.calc_text_size(&popup_state.popup_name)[0] + 32.0,
+ 120.0,
+ ]);
+
+ frame.spacing();
+ frame.spacing();
+
+ frame.text("Name:");
+ frame.same_line();
+
+ frame
+ .input_text(
+ "##rename_entity_popup_new_name_input",
+ &mut popup_state.new_entity_name,
+ )
+ .build();
+
+ frame.spacing();
+ frame.spacing();
+
+ let rename_text_size = frame.calc_text_size("Rename");
+
+ if frame.button_with_size(
+ "Rename##rename_entity_popup_rename_button",
+ [rename_text_size[0] * 2.0, rename_text_size[1] * 2.0],
+ ) {
+ let Some(target_ent) = world.get_entity(popup_state.target_entity_id)
+ else {
+ tracing::error!(
+ entity = ?popup_state.target_entity_id,
+ "Cannot rename entity: entity no longer exists"
+ );
+ return true;
+ };
+
+ if let Some(mut target_ent_name) = target_ent.get_mut::<EntityName>() {
+ target_ent_name.name = popup_state.new_entity_name.clone().into();
+ } else {
+ actions.add_components(
+ popup_state.target_entity_id,
+ (EntityName {
+ name: popup_state.new_entity_name.clone().into(),
+ },),
+ );
+ }
+
+ return true;
+ }
+
+ false
+ });
+
+ if let Some(true) = should_close {
+ return true;
+ }
+
+ if !is_opened {
+ return true;
+ }
+
+ false
+}
+
+fn enter_add_component_popup_state(
+ popup_state: &mut Option<PopupState>,
+ ent_handle: &EntityHandle,
+ ent_title: &str,
+ world: &World,
+) -> bool
+{
+ if popup_state.is_some() {
+ return false;
+ }
+
+ let popup_name = format!("Add component to {ent_title}");
+
+ let mut searchable_components = world
+ .query::<(&ComponentInfo,)>()
+ .iter_with_euids()
+ .map(|(id, (component_info,))| {
+ let user_creatable = if component_info.is_sole {
+ ComponentUserCreatable::No { reason: "is a sole" }
+ } else if component_info.type_reflection.is_none() {
+ ComponentUserCreatable::No { reason: "no type reflection" }
+ } else if component_info
+ .type_reflection
+ .is_some_and(|ty| !ty.has_default_value())
+ {
+ ComponentUserCreatable::No { reason: "no default value" }
+ } else if !get_whole_type_is_user_editable(
+ component_info.type_reflection,
+ component_info.ty_id,
+ ) {
+ ComponentUserCreatable::No {
+ reason: "whole component is not user editable",
+ }
+ } else {
+ ComponentUserCreatable::Yes
+ };
+
+ (id, component_info.clone(), user_creatable)
+ })
+ .collect::<Vec<_>>();
+
+ searchable_components.sort_by_key(|(_, _, user_creatable)| *user_creatable);
+
+ *popup_state = Some(PopupState::AddComponent(AddComponentPopupState {
+ popup_name: popup_name.clone(),
+ target_entity_id: ent_handle.uid(),
+ search_text: String::with_capacity(20),
+ searched_components: searchable_components.clone(),
+ searchable_components,
+ selected_search_result: -1,
+ new_component: None,
+ }));
+
+ true
+}
+
+fn show_add_component_popup(
+ frame: &dear_imgui_bindings::Ui,
+ add_component_popup_state: &mut AddComponentPopupState,
+ actions: &mut Actions,
+) -> bool
+{
+ frame.open_popup(&add_component_popup_state.popup_name);
+
+ let mut is_opened = true;
+
+ let should_close = frame.modal_popup_with_opened(
+ &add_component_popup_state.popup_name,
+ &mut is_opened,
+ || {
+ frame.set_window_size([700.0, 600.0]);
+
+ frame.spacing();
+ frame.spacing();
+
+ frame.text("Search:");
+ frame.same_line();
+
+ if frame
+ .input_text(
+ "##add_component_popup_search_input",
+ &mut add_component_popup_state.search_text,
+ )
+ .build()
+ {
+ add_component_popup_state.searched_components = add_component_popup_state
+ .searchable_components
+ .iter()
+ .cloned()
+ .filter(|(_, searchable_comp_info, _)| {
+ searchable_comp_info
+ .name
+ .contains(&add_component_popup_state.search_text)
+ })
+ .collect();
+
+ add_component_popup_state.selected_search_result = -1;
+ }
+
+ frame.spacing();
+ frame.spacing();
+
+ let _item_width_token = frame.push_item_width(-1.0);
+
+ if frame
+ .list_box_config("##add_component_popup_search_results")
+ .build_extended(
+ frame,
+ &mut add_component_popup_state.selected_search_result,
+ &add_component_popup_state.searched_components,
+ &|(_, comp_info, comp_user_creatable)| ListBoxItem {
+ label: comp_info.name.into(),
+ disabled: matches!(
+ comp_user_creatable,
+ ComponentUserCreatable::No { .. }
+ ),
+ hover_tooltip: match comp_user_creatable {
+ ComponentUserCreatable::Yes => None,
+ ComponentUserCreatable::No { reason } => Some(
+ format!("Component cannot be selected: {reason}").into(),
+ ),
+ },
+ },
+ )
+ {
+ let (selected_comp_id, selected_comp_info, _) = add_component_popup_state
+ .searched_components
+ [add_component_popup_state.selected_search_result as usize]
+ .clone();
+
+ let Some(selected_component_type) = selected_comp_info.type_reflection
+ else {
+ unreachable!();
+ };
+
+ let Some(selected_comp_default_value) =
+ selected_component_type.default_value()
+ else {
+ unreachable!(); // Only components with default values are selectable
+ };
+
+ add_component_popup_state.new_component = Some((
+ selected_comp_default_value,
+ selected_comp_info,
+ selected_comp_id,
+ ));
+ }
+
+ frame.spacing();
+ frame.spacing();
+ frame.spacing();
+
+ if let Some((new_component, new_component_info, _)) =
+ add_component_popup_state.new_component.as_mut()
+ {
+ let mut component_changed = false;
+
+ let Some(new_component_type) = new_component_info.type_reflection else {
+ unreachable!();
+ };
+
+ create_item_title_widget(
+ frame,
+ new_component_info.name,
+ &ItemType::Reflected(new_component_type),
+ None,
+ );
+
+ let item_type = ItemType::Reflected(new_component_type);
+
+ let _token = item_type.indent_or_same_line(frame);
+
+ add_item_to_frame(
+ frame,
+ ItemRef::Mutable(&mut **new_component),
+ &mut component_changed,
+ ItemInfo {
+ item_title: new_component_info.name,
+ item_tag: &format!(
+ "add_component_popup_component_{}",
+ new_component_info.name,
+ ),
+ item_type_name: None,
+ item_type,
+ item_is_read_only: false,
+ },
+ &[],
+ );
+
+ frame.spacing();
+ frame.spacing();
+ frame.spacing();
+ }
+
+ let _disabled = frame.begin_disabled_with_cond(
+ add_component_popup_state.new_component.is_none(),
+ );
+
+ let add_text_size = frame.calc_text_size("Add");
+
+ if frame.button_with_size(
+ "Add##add_component_popup_add_button",
+ [add_text_size[0] * 2.0, add_text_size[1] * 2.0],
+ ) {
+ if let Some((new_component_data, new_component_info, new_component_id)) =
+ add_component_popup_state.new_component.take()
+ {
+ actions.add_components(
+ add_component_popup_state.target_entity_id,
+ [ComponentParts::from_component_info(
+ &new_component_info,
+ new_component_id,
+ new_component_data,
+ )],
+ );
+
+ return true;
+ }
+ }
+
+ false
+ },
+ );
+
+ if let Some(true) = should_close {
+ return true;
+ }
+
+ if !is_opened {
+ return true;
+ }
+
+ false
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum ComponentUserCreatable
+{
+ Yes,
+ No
+ {
+ reason: &'static str,
+ },
+}
+
+struct ItemInfo<'a>
+{
+ item_title: &'a str,
+ item_tag: &'a str,
+ item_type_name: Option<&'static str>,
+ item_type: ItemType,
+ item_is_read_only: bool,
+}
+
+enum ItemType
+{
+ Reflected(&'static TypeReflection),
+ String,
+ CowStr,
+ Color(ColorItemType, LiteralType),
+}
+
+impl ItemType
+{
+ fn spans_multiple_rows(&self) -> bool
+ {
+ match self {
+ Self::Reflected(
+ TypeReflection::Struct(_)
+ | TypeReflection::Array(_)
+ | TypeReflection::Slice(_),
+ ) => true,
+ Self::Reflected(TypeReflection::Enum(enum_ty)) => !enum_ty.is_unit_only,
+ Self::Reflected(TypeReflection::Reference(ref_ty)) => {
+ ItemType::Reflected(ref_ty.ty).spans_multiple_rows()
+ }
+ Self::Color(..) => false,
+ Self::Reflected(TypeReflection::Literal(_)) | Self::String | Self::CowStr => {
+ false
+ }
+ Self::Reflected(_) => unimplemented!(),
+ }
+ }
+
+ fn indent_or_same_line<'frame>(
+ &self,
+ frame: &'frame dear_imgui_bindings::Ui,
+ ) -> Option<dear_imgui_bindings::IndentToken<'frame>>
+ {
+ if self.spans_multiple_rows() {
+ return Some(frame.begin_indent());
+ }
+
+ frame.same_line();
+
+ None
+ }
+}
+
+#[derive(Debug)]
+enum ColorItemType
+{
+ Rgb,
+ Rgba,
+}
+
+fn get_item_type(ty: Option<&'static TypeReflection>, type_id: TypeId)
+ -> Option<ItemType>
+{
+ if let Some(TypeReflection::Struct(_)) = ty {
+ if type_id == TypeId::of::<Rgb<f32>>() {
+ return Some(ItemType::Color(ColorItemType::Rgb, LiteralType::F32));
+ }
+ if type_id == TypeId::of::<Rgb<u8>>() {
+ return Some(ItemType::Color(ColorItemType::Rgb, LiteralType::U8));
+ }
+
+ if type_id == TypeId::of::<Rgba<f32>>() {
+ return Some(ItemType::Color(ColorItemType::Rgba, LiteralType::F32));
+ }
+ if type_id == TypeId::of::<Rgba<u8>>() {
+ return Some(ItemType::Color(ColorItemType::Rgba, LiteralType::U8));
+ }
+ }
+
+ if let Some(ty) = ty {
+ return Some(ItemType::Reflected(ty));
+ } else if type_id == TypeId::of::<String>() {
+ Some(ItemType::String)
+ } else if type_id == TypeId::of::<Cow<'static, str>>() {
+ Some(ItemType::CowStr)
+ } else {
+ None
+ }
+}
+
+enum ItemRef<'item>
+{
+ Mutable(&'item mut dyn Any),
+ Immutable(&'item dyn Any),
+}
+
+impl<'item> ItemRef<'item>
+{
+ fn get_struct_field(
+ &mut self,
+ ty: &StructReflection,
+ field_index: usize,
+ ) -> Result<ItemRef<'_>, ReflectionGetError>
+ {
+ match self {
+ ItemRef::Mutable(item) => {
+ (ty.try_get_field_mut)(*item, field_index).map(ItemRef::Mutable)
+ }
+ ItemRef::Immutable(item) => {
+ (ty.try_get_field)(*item, field_index).map(ItemRef::Immutable)
+ }
+ }
+ }
+
+ fn get_enum_variant_field(
+ &mut self,
+ variant: &EnumVariantReflection,
+ field_index: usize,
+ ) -> Result<ItemRef<'_>, ReflectionGetError>
+ {
+ match self {
+ ItemRef::Mutable(item) => {
+ (variant.try_get_field_mut)(*item, field_index).map(ItemRef::Mutable)
+ }
+ ItemRef::Immutable(item) => {
+ (variant.try_get_field)(*item, field_index).map(ItemRef::Immutable)
+ }
+ }
+ }
+
+ fn into_writable<T: Clone + 'static>(self) -> OwnedOrRefMut<'item, T>
+ {
+ match self {
+ ItemRef::Mutable(item) => OwnedOrRefMut::Ref(
+ (*item)
+ .downcast_mut::<T>()
+ .expect("Cannot downcast: wrong item type"),
+ ),
+ ItemRef::Immutable(item) => OwnedOrRefMut::Owned(
+ (*item)
+ .downcast_ref::<T>()
+ .expect("Cannot downcast: wrong item type")
+ .clone(),
+ ),
+ }
+ }
+}
+
+impl AsRef<dyn Any> for ItemRef<'_>
+{
+ fn as_ref(&self) -> &dyn Any
+ {
+ match self {
+ Self::Mutable(item) => &**item,
+ Self::Immutable(item) => *item,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum OwnedOrRefMut<'a, Value>
+{
+ Ref(&'a mut Value),
+ Owned(Value),
+}
+
+impl<Value> Deref for OwnedOrRefMut<'_, Value>
+{
+ type Target = Value;
+
+ fn deref(&self) -> &Self::Target
+ {
+ match self {
+ Self::Ref(value) => *value,
+ Self::Owned(value) => value,
+ }
+ }
+}
+
+impl<Value> DerefMut for OwnedOrRefMut<'_, Value>
+{
+ fn deref_mut(&mut self) -> &mut Self::Target
+ {
+ match self {
+ Self::Ref(value) => *value,
+ Self::Owned(value) => value,
+ }
+ }
+}
+
+fn create_item_title_widget(
+ frame: &dear_imgui_bindings::Ui,
+ item_title: &str,
+ item_type: &ItemType,
+ item_type_name: Option<&str>,
+)
+{
+ if !matches!(item_type, ItemType::Reflected(TypeReflection::Reference(_))) {
+ frame.text(&item_title);
+
+ if let Some(item_type_name) = item_type_name {
+ if frame.is_item_hovered() {
+ frame.tooltip_text(item_type_name);
+ }
+ }
+ }
+}
+
+fn add_item_to_frame<'a>(
+ frame: &dear_imgui_bindings::Ui,
+ mut item: ItemRef<'_>,
+ data_changed: &mut bool,
+ ItemInfo {
+ item_title,
+ item_tag,
+ item_type_name,
+ item_type,
+ item_is_read_only,
+ }: ItemInfo<'a>,
+ prev_item_tags: &[&'a str],
+)
+{
+ let _disabled = frame.begin_disabled_with_cond(
+ !matches!(
+ item_type,
+ ItemType::Reflected(
+ TypeReflection::Struct(_)
+ | TypeReflection::Enum(EnumReflection { is_unit_only: false, .. })
+ | TypeReflection::Array(_)
+ | TypeReflection::Slice(_)
+ )
+ ) && item_is_read_only,
+ );
+
+ match item_type {
+ ItemType::Reflected(TypeReflection::Struct(struct_ty)) => {
+ create_struct_widgets(
+ frame,
+ &mut item,
+ data_changed,
+ item_tag,
+ item_is_read_only,
+ struct_ty,
+ prev_item_tags,
+ );
+ }
+ ItemType::Reflected(TypeReflection::Enum(enum_type)) => {
+ create_enum_widgets(
+ frame,
+ &mut item,
+ data_changed,
+ item_tag,
+ item_is_read_only,
+ enum_type,
+ prev_item_tags,
+ );
+ }
+ ItemType::Reflected(TypeReflection::Literal(literal_reflection)) => {
+ match literal_reflection.ty {
+ LiteralType::U8 => create_scalar_item_input::<u8>(
+ frame,
+ &mut item,
+ data_changed,
+ &item_tag,
+ &prev_item_tags,
+ ),
+ LiteralType::I8 => create_scalar_item_input::<i8>(
+ frame,
+ &mut item,
+ data_changed,
+ &item_tag,
+ &prev_item_tags,
+ ),
+ LiteralType::U16 => create_scalar_item_input::<u16>(
+ frame,
+ &mut item,
+ data_changed,
+ &item_tag,
+ &prev_item_tags,
+ ),
+ LiteralType::I16 => create_scalar_item_input::<i16>(
+ frame,
+ &mut item,
+ data_changed,
+ &item_tag,
+ &prev_item_tags,
+ ),
+ LiteralType::U32 => create_scalar_item_input::<u32>(
+ frame,
+ &mut item,
+ data_changed,
+ &item_tag,
+ &prev_item_tags,
+ ),
+ LiteralType::I32 => create_scalar_item_input::<i32>(
+ frame,
+ &mut item,
+ data_changed,
+ &item_tag,
+ &prev_item_tags,
+ ),
+ LiteralType::U64 => create_scalar_item_input::<u64>(
+ frame,
+ &mut item,
+ data_changed,
+ &item_tag,
+ &prev_item_tags,
+ ),
+ LiteralType::I64 => create_scalar_item_input::<i64>(
+ frame,
+ &mut item,
+ data_changed,
+ &item_tag,
+ &prev_item_tags,
+ ),
+ LiteralType::F32 => create_scalar_item_input::<f32>(
+ frame,
+ &mut item,
+ data_changed,
+ &item_tag,
+ &prev_item_tags,
+ ),
+ LiteralType::F64 => create_scalar_item_input::<f64>(
+ frame,
+ &mut item,
+ data_changed,
+ &item_tag,
+ &prev_item_tags,
+ ),
+ LiteralType::Usize => create_scalar_item_input::<usize>(
+ frame,
+ &mut item,
+ data_changed,
+ &item_tag,
+ &prev_item_tags,
+ ),
+ LiteralType::Isize => create_scalar_item_input::<isize>(
+ frame,
+ &mut item,
+ data_changed,
+ &item_tag,
+ &prev_item_tags,
+ ),
+ LiteralType::U128 => {
+ let item = match item {
+ ItemRef::Mutable(item) => {
+ let Some(item) = (*item).downcast_mut::<u128>() else {
+ unreachable!();
+ };
+
+ item
+ }
+ ItemRef::Immutable(item) => {
+ let Some(item) = (*item).downcast_ref::<u128>() else {
+ unreachable!();
+ };
+
+ &mut item.clone()
+ }
+ };
+
+ frame.text(item.to_string());
+ }
+ LiteralType::I128 => {
+ let item = match item {
+ ItemRef::Mutable(item) => {
+ let Some(item) = (*item).downcast_mut::<i128>() else {
+ unreachable!();
+ };
+
+ item
+ }
+ ItemRef::Immutable(item) => {
+ let Some(item) = (*item).downcast_ref::<i128>() else {
+ unreachable!();
+ };
+
+ &mut item.clone()
+ }
+ };
+
+ frame.text(item.to_string());
+ }
+ LiteralType::Bool => {
+ let item = match item {
+ ItemRef::Mutable(item) => {
+ let Some(item) = (*item).downcast_mut::<bool>() else {
+ unreachable!();
+ };
+
+ item
+ }
+ ItemRef::Immutable(item) => {
+ let Some(item) = (*item).downcast_ref::<bool>() else {
+ unreachable!();
+ };
+
+ &mut item.clone()
+ }
+ };
+
+ if frame.checkbox(
+ create_item_label(&item_tag, "value_input", &prev_item_tags),
+ item,
+ ) {
+ *data_changed = true;
+ }
+ }
+ LiteralType::Str => {
+ let item = match item {
+ ItemRef::Mutable(item) => {
+ let Some(item) = (*item).downcast_ref::<&'static str>()
+ else {
+ unreachable!();
+ };
+
+ *item
+ }
+ ItemRef::Immutable(item) => {
+ let Some(item) = (*item).downcast_ref::<&'static str>()
+ else {
+ unreachable!();
+ };
+
+ *item
+ }
+ };
+
+ frame.text(item);
+ }
+ _ => unimplemented!(),
+ }
+ }
+ ItemType::Reflected(TypeReflection::Array(array_type)) => {
+ let mut prev_item_tags = prev_item_tags.to_vec();
+
+ prev_item_tags.push(item_tag);
+
+ for array_item_index in 0..array_type.length {
+ let array_item_name = array_item_index.to_string();
+
+ let Ok(item_item) = (match &mut item {
+ ItemRef::Mutable(item) => {
+ (array_type.try_get_item_mut)(*item, array_item_index)
+ .map(ItemRef::Mutable)
+ }
+ ItemRef::Immutable(item) => {
+ (array_type.try_get_item)(*item, array_item_index)
+ .map(ItemRef::Immutable)
+ }
+ }) else {
+ unreachable!();
+ };
+
+ create_item_title_widget(
+ frame,
+ &array_item_name,
+ &ItemType::Reflected(array_type.item_type),
+ Some(array_type.item_type_name()),
+ );
+
+ let array_item_type = ItemType::Reflected(array_type.item_type);
+
+ let _token = array_item_type.indent_or_same_line(frame);
+
+ add_item_to_frame(
+ frame,
+ item_item,
+ data_changed,
+ ItemInfo {
+ item_title: array_item_name.as_ref(),
+ item_tag: array_item_name.as_ref(),
+ item_type_name: Some(array_type.item_type_name()),
+ item_type: array_item_type,
+ item_is_read_only,
+ },
+ &prev_item_tags,
+ );
+ }
+ }
+ ItemType::Reflected(TypeReflection::Slice(slice_type)) => {
+ let mut prev_item_tags = prev_item_tags.to_vec();
+
+ prev_item_tags.push(item_tag);
+
+ let Some(len) = (slice_type.try_get_len)(item.as_ref()) else {
+ unreachable!();
+ };
+
+ for index in 0..len {
+ let item_name = index.to_string();
+
+ let Ok(item_item) = (slice_type.try_get_item)(item.as_ref(), index)
+ else {
+ unreachable!();
+ };
+
+ let mut slice_item_changed = false;
+
+ create_item_title_widget(
+ frame,
+ &item_name,
+ &ItemType::Reflected(slice_type.item_type),
+ Some(slice_type.item_type_name()),
+ );
+
+ let slice_item_type = ItemType::Reflected(slice_type.item_type);
+
+ let _token = slice_item_type.indent_or_same_line(frame);
+
+ add_item_to_frame(
+ frame,
+ ItemRef::Immutable(item_item),
+ &mut slice_item_changed,
+ ItemInfo {
+ item_title: &item_name,
+ item_tag: &item_name,
+ item_type_name: Some(slice_type.item_type_name()),
+ item_type: slice_item_type,
+ item_is_read_only: true,
+ },
+ &prev_item_tags,
+ );
+
+ assert!(!slice_item_changed);
+ }
+ }
+ ItemType::Reflected(TypeReflection::Reference(ref_type)) => {
+ let mut derefed_changed = false;
+
+ let Some(derefed_item) = (ref_type.try_deref)(item.as_ref()) else {
+ unreachable!();
+ };
+
+ create_item_title_widget(
+ frame,
+ item_title,
+ &ItemType::Reflected(ref_type.ty),
+ item_type_name,
+ );
+
+ let ref_item_type = ItemType::Reflected(ref_type.ty);
+
+ let _token = ref_item_type.indent_or_same_line(frame);
+
+ add_item_to_frame(
+ frame,
+ ItemRef::Immutable(derefed_item),
+ &mut derefed_changed,
+ ItemInfo {
+ item_title,
+ item_tag: "derefed".into(),
+ item_type_name,
+ item_type: ref_item_type,
+ item_is_read_only: true,
+ },
+ prev_item_tags,
+ );
+
+ assert!(!derefed_changed);
+ }
+ ItemType::String => {
+ let item = match item {
+ ItemRef::Mutable(item) => {
+ let Some(item) = (*item).downcast_mut::<String>() else {
+ unreachable!();
+ };
+
+ item
+ }
+ ItemRef::Immutable(item) => {
+ let Some(item) = (*item).downcast_ref::<String>() else {
+ unreachable!();
+ };
+
+ &mut item.clone()
+ }
+ };
+
+ if frame
+ .input_text(
+ create_item_label(&item_tag, "value_input", &prev_item_tags),
+ item,
+ )
+ .build()
+ {
+ *data_changed = true;
+ }
+ }
+ ItemType::CowStr => {
+ let item = match item {
+ ItemRef::Mutable(item) => {
+ let Some(item) = (*item).downcast_mut::<Cow<'static, str>>() else {
+ unreachable!();
+ };
+
+ item
+ }
+ ItemRef::Immutable(item) => {
+ let Some(item) = (*item).downcast_ref::<Cow<'static, str>>() else {
+ unreachable!();
+ };
+
+ &mut item.clone()
+ }
+ };
+
+ if frame
+ .input_text(
+ create_item_label(&item_tag, "value_input", &prev_item_tags),
+ item.to_mut(),
+ )
+ .build()
+ {
+ *data_changed = true;
+ }
+ }
+ ItemType::Color(ColorItemType::Rgb, LiteralType::F32) => {
+ let mut item = item.into_writable::<Rgb<f32>>();
+
+ let mut scratch = [item.r, item.g, item.b];
+
+ if frame
+ .color_edit3_config(
+ create_item_label(&item_tag, "value_input", &prev_item_tags),
+ &mut scratch,
+ )
+ .display_mode(dear_imgui_bindings::ColorDisplayMode::Hex)
+ .build()
+ {
+ let [new_r, new_g, new_b] = scratch;
+
+ *item = Rgb { r: new_r, g: new_g, b: new_b };
+
+ *data_changed = true;
+ }
+ }
+ ItemType::Color(ColorItemType::Rgb, LiteralType::U8) => {
+ let mut item = item.into_writable::<Rgb<u8>>();
+
+ let mut scratch = [
+ item.r as f32 / 255.0,
+ item.g as f32 / 255.0,
+ item.b as f32 / 255.0,
+ ];
+
+ if frame
+ .color_edit3_config(
+ create_item_label(&item_tag, "value_input", &prev_item_tags),
+ &mut scratch,
+ )
+ .display_mode(dear_imgui_bindings::ColorDisplayMode::Hex)
+ .build()
+ {
+ let [new_r, new_g, new_b] = scratch;
+
+ *item = Rgb {
+ r: (new_r * 255.0) as u8,
+ g: (new_g * 255.0) as u8,
+ b: (new_b * 255.0) as u8,
+ };
+
+ *data_changed = true;
+ }
+ }
+ ItemType::Color(ColorItemType::Rgba, LiteralType::F32) => {
+ let mut item = item.into_writable::<Rgba<f32>>();
+
+ let mut scratch = [item.r, item.g, item.b, item.a];
+
+ if frame
+ .color_edit4_config(
+ create_item_label(&item_tag, "value_input", &prev_item_tags),
+ &mut scratch,
+ )
+ .display_mode(dear_imgui_bindings::ColorDisplayMode::Hex)
+ .build()
+ {
+ let [new_r, new_g, new_b, new_a] = scratch;
+
+ *item = Rgba {
+ r: new_r,
+ g: new_g,
+ b: new_b,
+ a: new_a,
+ };
+
+ *data_changed = true;
+ }
+ }
+ ItemType::Color(ColorItemType::Rgba, LiteralType::U8) => {
+ let mut item = item.into_writable::<Rgba<u8>>();
+
+ let mut scratch = [
+ item.r as f32 / 255.0,
+ item.g as f32 / 255.0,
+ item.b as f32 / 255.0,
+ item.a as f32 / 255.0,
+ ];
+
+ if frame
+ .color_edit4_config(
+ create_item_label(&item_tag, "value_input", &prev_item_tags),
+ &mut scratch,
+ )
+ .display_mode(dear_imgui_bindings::ColorDisplayMode::Hex)
+ .build()
+ {
+ let [new_r, new_g, new_b, new_a] = scratch;
+
+ *item = Rgba {
+ r: (new_r * 255.0) as u8,
+ g: (new_g * 255.0) as u8,
+ b: (new_b * 255.0) as u8,
+ a: (new_a * 255.0) as u8,
+ };
+
+ *data_changed = true;
+ }
+ }
+ ItemType::Color(ColorItemType::Rgb, _) => unreachable!(),
+ ItemType::Color(ColorItemType::Rgba, _) => unreachable!(),
+ ItemType::Reflected(_) => unimplemented!(),
+ }
+}
+
+fn create_scalar_item_input<Scalar>(
+ frame: &dear_imgui_bindings::Ui,
+ item: &mut ItemRef<'_>,
+ data_changed: &mut bool,
+ item_tag: &str,
+ prev_item_tags: &[&str],
+) where
+ Scalar: dear_imgui_bindings::internal::DataTypeKind + Clone + 'static,
+{
+ let item = match item {
+ ItemRef::Mutable(item) => {
+ let Some(item) = (*item).downcast_mut::<Scalar>() else {
+ unreachable!();
+ };
+
+ item
+ }
+ ItemRef::Immutable(item) => {
+ let Some(item) = (*item).downcast_ref::<Scalar>() else {
+ unreachable!();
+ };
+
+ &mut item.clone()
+ }
+ };
+
+ if frame
+ .input_scalar(
+ create_item_label(item_tag, "value_input", &prev_item_tags),
+ item,
+ )
+ .build()
+ {
+ *data_changed = true;
+ }
+}
+
+fn item_layout_table<'frame>(
+ frame: &'frame dear_imgui_bindings::Ui,
+ prev_item_tags: &[&str],
+) -> dear_imgui_bindings::TableBuilder<'frame>
+{
+ frame
+ .table(create_item_label("item_layout", "table", &prev_item_tags))
+ .headers(false)
+ .sizing_policy(dear_imgui_bindings::TableSizingPolicy::StretchSame)
+ .columns([
+ dear_imgui_bindings::TableColumnSetup::new("a"),
+ dear_imgui_bindings::TableColumnSetup::new("b").stretch_weight(2.0),
+ ])
+}
+
+fn create_struct_widgets(
+ frame: &dear_imgui_bindings::Ui,
+ item: &mut ItemRef<'_>,
+ data_changed: &mut bool,
+ item_tag: &str,
+ item_is_read_only: bool,
+ struct_ty: &StructReflection,
+ prev_item_tags: &[&str],
+)
+{
+ let mut prev_item_tags = prev_item_tags.to_vec();
+
+ prev_item_tags.push(item_tag.as_ref());
+
+ let struct_fields =
+ struct_ty
+ .fields
+ .iter()
+ .enumerate()
+ .filter_map(|(field_index, field)| {
+ let field_item_type =
+ get_item_type(field.type_reflection(), field.type_id)?;
+
+ Some((field_index, field, field_item_type))
+ });
+
+ item_layout_table(frame, &prev_item_tags).build(|frame| {
+ for (field_index, field, field_item_type) in struct_fields
+ .clone()
+ .filter(|(.., field_item_type)| !field_item_type.spans_multiple_rows())
+ {
+ frame.table_next_row();
+ frame.table_next_column();
+
+ let field_name = field
+ .name
+ .map(Cow::Borrowed)
+ .unwrap_or_else(|| field_index.to_string().into());
+
+ let Ok(field_item) = item.get_struct_field(struct_ty, field_index) else {
+ unreachable!();
+ };
+
+ create_item_title_widget(
+ frame,
+ &field_name,
+ &field_item_type,
+ Some(field.type_name()),
+ );
+
+ frame.table_next_column();
+
+ add_item_to_frame(
+ frame,
+ field_item,
+ data_changed,
+ ItemInfo {
+ item_title: field_name.as_ref(),
+ item_tag: field_name.as_ref(),
+ item_type_name: Some(field.type_name()),
+ item_type: field_item_type,
+ item_is_read_only: item_is_read_only
+ || matches!(
+ field.visibility,
+ Visibility::Private | Visibility::PubScoped(_)
+ ),
+ },
+ &prev_item_tags,
+ );
+ }
+ });
+
+ for (field_index, field, field_item_type) in struct_fields
+ .filter(|(.., field_item_type)| field_item_type.spans_multiple_rows())
+ {
+ let field_name = field
+ .name
+ .map(Cow::Borrowed)
+ .unwrap_or_else(|| field_index.to_string().into());
+
+ let Ok(field_item) = item.get_struct_field(struct_ty, field_index) else {
+ unreachable!();
+ };
+
+ create_item_title_widget(
+ frame,
+ &field_name,
+ &field_item_type,
+ Some(field.type_name()),
+ );
+
+ let _token = frame.begin_indent();
+
+ add_item_to_frame(
+ frame,
+ field_item,
+ data_changed,
+ ItemInfo {
+ item_title: field_name.as_ref(),
+ item_tag: field_name.as_ref(),
+ item_type_name: Some(field.type_name()),
+ item_type: field_item_type,
+ item_is_read_only: item_is_read_only
+ || matches!(
+ field.visibility,
+ Visibility::Private | Visibility::PubScoped(_)
+ ),
+ },
+ &prev_item_tags,
+ );
+ }
+}
+
+fn create_enum_widgets(
+ frame: &dear_imgui_bindings::Ui,
+ item: &mut ItemRef<'_>,
+ data_changed: &mut bool,
+ item_tag: &str,
+ item_is_read_only: bool,
+ enum_type: &EnumReflection,
+ prev_item_tags: &[&str],
+)
+{
+ let Some(mut curr_variant_index) = (enum_type.get_variant_index)(item.as_ref())
+ else {
+ unreachable!();
+ };
+
+ if create_combo_box(
+ frame,
+ create_item_label(&item_tag, "variant_select", &prev_item_tags),
+ &mut curr_variant_index,
+ enum_type.variants,
+ |variant| {
+ let disabled = variant.fields.as_ref().and_then(|fields| {
+ fields.fields().iter().find_map(|field| {
+ let field_name = match field.name {
+ Some(field_name) => Either::A(field_name),
+ None => Either::B(field.index),
+ };
+
+ let Some(field_type) = field.type_reflection() else {
+ return Some((field_name, "has no type reflection"));
+ };
+
+ if !field_type.has_default_value() {
+ return Some((field_name, "has no default value"));
+ }
+
+ None
+ })
+ });
+
+ ComboBoxItem {
+ label: variant.name.into(),
+ disabled: disabled.is_some(),
+ hover_tooltip: disabled.map(|(bad_field_name, bad_field_reason)| {
+ format!(
+ "Variant cannot be selected since field {} {}",
+ bad_field_name, bad_field_reason
+ )
+ .into()
+ }),
+ }
+ },
+ ) {
+ 'used: {
+ let ItemRef::Mutable(item) = item else {
+ break 'used;
+ };
+
+ let new_variant = &enum_type.variants[curr_variant_index];
+
+ let Ok(()) = (new_variant.try_write_new_to)(
+ *item,
+ &mut new_variant
+ .fields
+ .iter()
+ .map(|fields| fields.fields())
+ .flatten()
+ .map(|field| {
+ let Some(field_type) = field.type_reflection() else {
+ // Variants with any field that is missing type reflection is
+ // disabled so that the user cannot select it
+ unreachable!();
+ };
+
+ let Some(default_value) = field_type.default_value() else {
+ unreachable!();
+ };
+
+ default_value
+ }),
+ ) else {
+ unreachable!();
+ };
+
+ *data_changed = true;
+ }
+ }
+
+ let mut prev_item_tags = prev_item_tags.to_vec();
+
+ prev_item_tags.push(item_tag);
+
+ let curr_variant = &enum_type.variants[curr_variant_index];
+
+ let Some(curr_variant_fields) = &curr_variant.fields else {
+ return;
+ };
+
+ let variant_fields = curr_variant_fields.fields().iter().enumerate().filter_map(
+ |(field_index, field)| {
+ let field_item_type = get_item_type(field.type_reflection(), field.type_id)?;
+
+ Some((field_index, field, field_item_type))
+ },
+ );
+
+ let variant_field_cnt = variant_fields.clone().count();
+
+ item_layout_table(frame, &prev_item_tags).build(|_| {
+ for (field_index, field, field_item_type) in variant_fields
+ .clone()
+ .filter(|(.., field_item_type)| !field_item_type.spans_multiple_rows())
+ {
+ frame.table_next_row();
+ frame.table_next_column();
+
+ let field_name: Cow<str> = match field.name {
+ Some(field_name) => field_name.into(),
+ None => field_index.to_string().into(),
+ };
+
+ let Ok(field_item) = item.get_enum_variant_field(curr_variant, field_index)
+ else {
+ unreachable!();
+ };
+
+ if variant_field_cnt != 1 || field.name.is_some() {
+ create_item_title_widget(
+ frame,
+ &field_name,
+ &field_item_type,
+ Some(field.type_name()),
+ );
+
+ frame.table_next_column();
+ }
+
+ add_item_to_frame(
+ frame,
+ field_item,
+ data_changed,
+ ItemInfo {
+ item_title: field_name.as_ref(),
+ item_tag: field_name.as_ref(),
+ item_type_name: Some(field.type_name()),
+ item_type: field_item_type,
+ item_is_read_only: item_is_read_only,
+ },
+ &prev_item_tags,
+ );
+ }
+ });
+
+ for (field_index, field, field_item_type) in variant_fields
+ .filter(|(.., field_item_type)| field_item_type.spans_multiple_rows())
+ {
+ let field_name: Cow<str> = match field.name {
+ Some(field_name) => field_name.into(),
+ None => field_index.to_string().into(),
+ };
+
+ let Ok(field_item) = item.get_enum_variant_field(curr_variant, field_index)
+ else {
+ unreachable!();
+ };
+
+ if variant_field_cnt != 1 || field.name.is_some() {
+ create_item_title_widget(
+ frame,
+ &field_name,
+ &field_item_type,
+ Some(field.type_name()),
+ );
+
+ let _token = frame.begin_indent();
+ }
+
+ add_item_to_frame(
+ frame,
+ field_item,
+ data_changed,
+ ItemInfo {
+ item_title: field_name.as_ref(),
+ item_tag: field_name.as_ref(),
+ item_type_name: Some(field.type_name()),
+ item_type: field_item_type,
+ item_is_read_only: item_is_read_only,
+ },
+ &prev_item_tags,
+ );
+ }
+}
+
+fn create_item_label(item_tag: &str, value_name: &str, prev_item_tags: &[&str])
+ -> String
+{
+ let mut item_label = String::with_capacity(
+ 2 + item_tag.len()
+ + prev_item_tags.len()
+ + prev_item_tags.iter().max().unwrap_or(&"".into()).len(),
+ );
+
+ item_label.push_str("##");
+
+ for prev_item_tag in prev_item_tags {
+ item_label.push('/');
+ item_label.push_str(prev_item_tag);
+ }
+
+ item_label.push('/');
+ item_label.push_str(item_tag);
+ item_label.push(':');
+ item_label.push_str(value_name);
+
+ item_label
+}
+
+fn get_whole_type_is_user_editable(
+ ty: Option<&'static TypeReflection>,
+ type_id: TypeId,
+) -> bool
+{
+ let Some(item_type) = get_item_type(ty, type_id) else {
+ return false;
+ };
+
+ match item_type {
+ ItemType::Reflected(TypeReflection::Struct(struct_ty)) => {
+ for field in struct_ty.fields {
+ if matches!(
+ field.visibility,
+ Visibility::Private | Visibility::PubScoped(_)
+ ) {
+ continue;
+ }
+
+ if !get_whole_type_is_user_editable(
+ field.type_reflection(),
+ field.type_id,
+ ) {
+ return false;
+ }
+ }
+
+ true
+ }
+ ItemType::Reflected(TypeReflection::Enum(enum_ty)) => {
+ enum_ty.variants.iter().any(|variant| {
+ for field in variant.fields.iter().flat_map(|fields| fields.fields()) {
+ if !get_whole_type_is_user_editable(
+ field.type_reflection(),
+ field.type_id,
+ ) {
+ return false;
+ }
+ }
+
+ true
+ })
+ }
+ ItemType::Reflected(
+ TypeReflection::Slice(_)
+ | TypeReflection::Array(_)
+ | TypeReflection::Literal(_),
+ )
+ | ItemType::String
+ | ItemType::CowStr
+ | ItemType::Color(..) => true,
+ ItemType::Reflected(_) => unimplemented!(),
+ }
+}
+
+struct ComboBoxItem<'b>
+{
+ label: Cow<'b, str>,
+ disabled: bool,
+ hover_tooltip: Option<Cow<'b, str>>,
+}
+
+fn create_combo_box<Item, ItemFn>(
+ frame: &dear_imgui_bindings::Ui,
+ label: impl AsRef<str>,
+ current_item: &mut usize,
+ items: &[Item],
+ item_fn: ItemFn,
+) -> bool
+where
+ for<'b> ItemFn: Fn(&'b Item) -> ComboBoxItem<'b>,
+{
+ let item_fn = &item_fn;
+
+ let mut result = false;
+
+ let preview_label = items.get(*current_item).map(|item| item_fn(item).label);
+
+ if let Some(combo_token) = frame.begin_combo(
+ label,
+ preview_label
+ .as_ref()
+ .map(|preview_label| preview_label.as_ref())
+ .unwrap_or(""),
+ ) {
+ for (idx, item) in items.iter().enumerate() {
+ let is_selected = idx == *current_item;
+
+ if is_selected {
+ frame.set_item_default_focus();
+ }
+
+ let ComboBoxItem {
+ label: item_label,
+ disabled: item_disabled,
+ hover_tooltip: item_hover_tooltip,
+ } = item_fn(item);
+
+ let disabled_token = frame.begin_disabled_with_cond(item_disabled);
+
+ let clicked = frame.selectable(item_label.as_ref());
+
+ if clicked {
+ *current_item = idx;
+ result = true;
+ }
+
+ disabled_token.end();
+
+ if let Some(item_hover_tooltip) = item_hover_tooltip {
+ if frame.is_item_hovered_with_flags(
+ dear_imgui_bindings::ItemHoveredFlags::ALLOW_WHEN_DISABLED,
+ ) {
+ frame.tooltip_text(item_hover_tooltip.as_ref());
+ }
+ }
+ }
+
+ combo_token.end();
+ }
+
+ result
+}
+
+struct ListBoxItem<'b>
+{
+ label: Cow<'b, str>,
+ disabled: bool,
+ hover_tooltip: Option<Cow<'b, str>>,
+}
+
+trait ListBoxExt
+{
+ fn build_extended<Item, ItemFn>(
+ self,
+ ui: &dear_imgui_bindings::Ui,
+ current_item: &mut i32,
+ items: &[Item],
+ item_fn: &ItemFn,
+ ) -> bool
+ where
+ for<'b> ItemFn: Fn(&'b Item) -> ListBoxItem<'b>;
+}
+
+impl<Label> ListBoxExt for dear_imgui_bindings::ListBox<Label>
+where
+ Label: AsRef<str>,
+{
+ fn build_extended<Item, ItemFn>(
+ self,
+ ui: &dear_imgui_bindings::Ui,
+ current_item: &mut i32,
+ items: &[Item],
+ item_fn: &ItemFn,
+ ) -> bool
+ where
+ for<'b> ItemFn: Fn(&'b Item) -> ListBoxItem<'b>,
+ {
+ let mut result = false;
+
+ let lb = self;
+
+ if let Some(_cb) = lb.begin(ui) {
+ for (idx, item) in items.iter().enumerate() {
+ if idx > i32::MAX as usize {
+ break;
+ }
+
+ let idx_i32 = idx as i32;
+
+ let ListBoxItem {
+ label: item_label,
+ disabled: item_disabled,
+ hover_tooltip: item_hover_tooltip,
+ } = item_fn(item);
+
+ let is_selected = idx_i32 == *current_item;
+
+ let disabled_token = ui.begin_disabled_with_cond(item_disabled);
+
+ if ui
+ .selectable_config(&item_label)
+ .selected(is_selected)
+ .build()
+ {
+ *current_item = idx_i32;
+ result = true;
+ }
+
+ disabled_token.end();
+
+ if let Some(item_hover_tooltip) = item_hover_tooltip {
+ if ui.is_item_hovered_with_flags(
+ dear_imgui_bindings::ItemHoveredFlags::ALLOW_WHEN_DISABLED,
+ ) {
+ ui.tooltip_text(item_hover_tooltip.as_ref());
+ }
+ }
+ }
+ }
+
+ result
+ }
+}
+
+fn create_entity_title(ent_handle: &EntityHandle) -> String
+{
+ let ent_name = ent_handle.get::<EntityName>();
+
+ format!(
+ "{} {}",
+ ent_name
+ .as_ref()
+ .map(|ent_name| ent_name.name.as_ref())
+ .unwrap_or("<unnamed>"),
+ ent_handle.uid()
+ )
+}
+
+struct Textures
+{
+ despawn_icon_tex_id: dear_imgui_bindings::ManagedTextureId,
+ warning_icon_tex_id: dear_imgui_bindings::ManagedTextureId,
+ edit_icon_tex_id: dear_imgui_bindings::ManagedTextureId,
+}
+
+impl Textures
+{
+ fn new(dear_imgui_context: &mut DearImguiContext) -> Self
+ {
+ let despawn_icon_tex_id = create_texture(
+ dear_imgui_context,
+ Image::from_reader(
+ Cursor::new(include_bytes!("../../../res/ui/delete.png")),
+ ImageFormat::Png,
+ )
+ .unwrap(),
+ );
+
+ let warning_icon_tex_id = create_texture(
+ dear_imgui_context,
+ Image::from_reader(
+ Cursor::new(include_bytes!("../../../res/ui/warning.png")),
+ ImageFormat::Png,
+ )
+ .unwrap(),
+ );
+
+ let edit_icon_tex_id = create_texture(
+ dear_imgui_context,
+ Image::from_reader(
+ Cursor::new(include_bytes!("../../../res/ui/edit.png")),
+ ImageFormat::Png,
+ )
+ .unwrap(),
+ );
+
+ Self {
+ despawn_icon_tex_id,
+ warning_icon_tex_id,
+ edit_icon_tex_id,
+ }
+ }
+}
+
+#[derive(Debug)]
+struct AddComponentPopupState
+{
+ popup_name: String,
+ target_entity_id: Uid,
+ search_text: String,
+ searched_components: Vec<(Uid, ComponentInfo, ComponentUserCreatable)>,
+ searchable_components: Vec<(Uid, ComponentInfo, ComponentUserCreatable)>,
+ selected_search_result: i32,
+ new_component: Option<(Box<dyn Any>, ComponentInfo, Uid)>,
+}
+
+#[derive(Debug)]
+struct RenameEntityPopupState
+{
+ popup_name: String,
+ target_entity_id: Uid,
+ new_entity_name: String,
+}
+
+#[derive(Debug)]
+enum PopupState
+{
+ AddComponent(AddComponentPopupState),
+ RenameEntity(RenameEntityPopupState),
+}
diff --git a/engine/src/util.rs b/engine/src/util.rs
index a505a38..f016ad3 100644
--- a/engine/src/util.rs
+++ b/engine/src/util.rs
@@ -1,18 +1,265 @@
-macro_rules! try_option {
- ($expr: expr) => {
- match $expr {
- Ok(value) => value,
- Err(err) => {
- return Some(Err(err.into()));
- }
+use std::fmt::Debug;
+
+use crate::ecs::util::StreamingIterator;
+
+pub trait OptionExt<T>
+{
+ /// Substitute for the currently experimental function
+ /// [`Option::get_or_try_insert_with`].
+ /// See https://github.com/rust-lang/rust/issues/143648
+ fn get_or_try_insert_with_fn<Err>(
+ &mut self,
+ func: impl Fn() -> Result<T, Err>,
+ ) -> Result<&mut T, Err>;
+}
+
+impl<T> OptionExt<T> for Option<T>
+{
+ fn get_or_try_insert_with_fn<Err>(
+ &mut self,
+ func: impl FnOnce() -> Result<T, Err>,
+ ) -> Result<&mut T, Err>
+ {
+ if let None = self {
+ *self = Some(func()?);
}
- };
+
+ Ok(unsafe { self.as_mut().unwrap_unchecked() })
+ }
+}
+
+#[derive(Debug)]
+pub struct BitArray<const SIZE: usize, const BITS_PER_ITEM: usize>
+{
+ inner: [u8; SIZE],
+}
+
+impl<const SIZE: usize, const BITS_PER_ITEM: usize> BitArray<SIZE, BITS_PER_ITEM>
+{
+ const ITEM_MASK: u8 = !(u8::MAX << BITS_PER_ITEM);
+
+ pub fn new() -> Self
+ {
+ assert!(BITS_PER_ITEM > 1);
+ assert!(BITS_PER_ITEM <= 8);
+ assert_eq!(BITS_PER_ITEM % 2, 0);
+
+ Self { inner: [0; SIZE] }
+ }
+
+ pub fn get(&self, item_index: usize) -> u8
+ {
+ let bit_index = item_index * BITS_PER_ITEM;
+
+ let byte_index = bit_index / 8;
+
+ let bit_index_in_byte = bit_index - (byte_index * 8);
+
+ (self.inner[byte_index] >> (bit_index_in_byte)) & Self::ITEM_MASK
+ }
+
+ #[tracing::instrument(skip(self))]
+ pub fn clear_and_set(&mut self, item_index: usize, item_bits: u8, clear_mask: u8)
+ {
+ let item_bits = item_bits & Self::ITEM_MASK;
+
+ let clear_mask = clear_mask & Self::ITEM_MASK;
+
+ let bit_index = item_index * BITS_PER_ITEM;
+
+ let byte_index = bit_index / 8;
+
+ let bit_index_in_byte = bit_index - (byte_index * 8);
+
+ tracing::trace!(
+ bit_index,
+ byte_index,
+ bit_index_in_byte,
+ "Setting item bits"
+ );
+
+ self.inner[byte_index] &= !(clear_mask << bit_index_in_byte);
+
+ self.inner[byte_index] |= item_bits << bit_index_in_byte;
+ }
+
+ pub fn iter_occupied(&self) -> BitArrayOccupiedIter<'_, BITS_PER_ITEM>
+ {
+ BitArrayOccupiedIter {
+ inner: self.inner.iter().enumerate().peekable(),
+ mask: u8::MAX,
+ }
+ }
+
+ pub fn iter_occupied_mut(&mut self) -> BitArrayOccupiedMutIter<'_, BITS_PER_ITEM>
+ {
+ BitArrayOccupiedMutIter {
+ inner: self.inner.iter_mut().enumerate().peekable(),
+ mask: u8::MAX,
+ }
+ }
+
+ pub fn bytes_mut(&mut self) -> &mut [u8]
+ {
+ &mut self.inner
+ }
+}
+
+impl<const SIZE: usize, const BITS_PER_ITEM: usize> Default
+ for BitArray<SIZE, BITS_PER_ITEM>
+{
+ fn default() -> Self
+ {
+ Self::new()
+ }
}
-use std::mem::ManuallyDrop;
-use std::ops::{Deref, DerefMut};
+pub struct BitArrayOccupiedIter<'a, const BITS_PER_ITEM: usize>
+{
+ inner: std::iter::Peekable<std::iter::Enumerate<std::slice::Iter<'a, u8>>>,
+ mask: u8,
+}
+
+impl<const BITS_PER_ITEM: usize> BitArrayOccupiedIter<'_, BITS_PER_ITEM>
+{
+ const ITEM_MASK: u8 = !(u8::MAX << BITS_PER_ITEM);
+}
+
+impl<const BITS_PER_ITEM: usize> Iterator for BitArrayOccupiedIter<'_, BITS_PER_ITEM>
+{
+ type Item = (usize, u8);
+
+ fn next(&mut self) -> Option<Self::Item>
+ {
+ let (byte_masked, byte_index, item_bit_index_in_byte) = loop {
+ let current = self.inner.peek().and_then(|(byte_index, byte)| {
+ let byte_masked = **byte & self.mask;
+
+ let lowest_one = byte_masked.lowest_one()?;
+
+ let item_bit_index_in_byte = match lowest_one as usize % BITS_PER_ITEM {
+ 0 => lowest_one as usize,
+ remainder => lowest_one as usize - remainder,
+ };
+
+ Some((byte_masked, byte_index, item_bit_index_in_byte))
+ });
+
+ let Some((byte_masked, byte_index, item_bit_index_in_byte)) = current else {
+ let _ = self.inner.next()?;
+
+ self.mask = u8::MAX;
+
+ continue;
+ };
+
+ break (byte_masked, byte_index, item_bit_index_in_byte);
+ };
+
+ let item_bits = (byte_masked >> (item_bit_index_in_byte)) & Self::ITEM_MASK;
+
+ self.mask &= (!Self::ITEM_MASK) << item_bit_index_in_byte;
-pub(crate) use try_option;
+ let item_index_in_byte = item_bit_index_in_byte as usize / BITS_PER_ITEM;
+
+ let prev_bytes_item_cnt = (byte_index * 8) / BITS_PER_ITEM;
+
+ let index = prev_bytes_item_cnt + item_index_in_byte;
+
+ Some((index, item_bits))
+ }
+}
+
+pub struct BitArrayOccupiedMutIter<'a, const BITS_PER_ITEM: usize>
+{
+ inner: std::iter::Peekable<std::iter::Enumerate<std::slice::IterMut<'a, u8>>>,
+ mask: u8,
+}
+
+impl<const BITS_PER_ITEM: usize> BitArrayOccupiedMutIter<'_, BITS_PER_ITEM>
+{
+ const ITEM_MASK: u8 = !(u8::MAX << BITS_PER_ITEM);
+}
+
+impl<const BITS_PER_ITEM: usize> StreamingIterator
+ for BitArrayOccupiedMutIter<'_, BITS_PER_ITEM>
+{
+ type Item<'a>
+ = BitArrayItemMut<'a, BITS_PER_ITEM>
+ where
+ Self: 'a;
+
+ fn streaming_next(&mut self) -> Option<Self::Item<'_>>
+ {
+ let (byte_masked, byte_index, item_bit_index_in_byte) = loop {
+ let current = self.inner.peek().and_then(|(byte_index, byte)| {
+ let byte_masked = **byte & self.mask;
+
+ let lowest_one = byte_masked.lowest_one()?;
+
+ let item_bit_index_in_byte = match lowest_one as usize % BITS_PER_ITEM {
+ 0 => lowest_one as usize,
+ remainder => lowest_one as usize - remainder,
+ };
+
+ Some((byte_masked, byte_index, item_bit_index_in_byte))
+ });
+
+ let Some((byte_masked, byte_index, item_bit_index_in_byte)) = current else {
+ let _ = self.inner.next()?;
+
+ self.mask = u8::MAX;
+
+ continue;
+ };
+
+ break (byte_masked, byte_index, item_bit_index_in_byte);
+ };
+
+ let item_bits = (byte_masked >> (item_bit_index_in_byte)) & Self::ITEM_MASK;
+
+ self.mask &= (!Self::ITEM_MASK) << item_bit_index_in_byte;
+
+ let item_index_in_byte = item_bit_index_in_byte as usize / BITS_PER_ITEM;
+
+ let prev_bytes_item_cnt = (byte_index * 8) / BITS_PER_ITEM;
+
+ let index = prev_bytes_item_cnt + item_index_in_byte;
+
+ Some(BitArrayItemMut {
+ index,
+ bits: item_bits,
+ byte: self.inner.peek_mut().unwrap().1,
+ bit_index_in_byte: item_bit_index_in_byte,
+ })
+ }
+}
+
+pub struct BitArrayItemMut<'a, const BITS_PER_ITEM: usize>
+{
+ pub index: usize,
+ pub bits: u8,
+ byte: &'a mut u8,
+ bit_index_in_byte: usize,
+}
+
+impl<'a, const BITS_PER_ITEM: usize> BitArrayItemMut<'a, BITS_PER_ITEM>
+{
+ const ITEM_MASK: u8 = !(u8::MAX << BITS_PER_ITEM);
+
+ pub fn clear_and_set(&mut self, new_bits: u8, clear_mask: u8)
+ {
+ let new_bits = new_bits & Self::ITEM_MASK;
+
+ let clear_mask = clear_mask & Self::ITEM_MASK;
+
+ *self.byte &= !(clear_mask << self.bit_index_in_byte);
+
+ *self.byte |= new_bits << self.bit_index_in_byte;
+
+ self.bits = new_bits;
+ }
+}
macro_rules! or {
(($($tt: tt)+) else ($($else_tt: tt)*)) => {
@@ -26,6 +273,18 @@ macro_rules! or {
pub(crate) use or;
+#[macro_export]
+macro_rules! expand_map_opt {
+ ($in: tt, no_occurance=($($no_occurance: tt)*), occurance=($($occurance: tt)*)) => {
+ $($occurance)*
+ };
+
+ (, no_occurance=($($no_occurance: tt)*), occurance=($($occurance: tt)*)) => {
+ $($no_occurance)*
+ };
+}
+
+#[macro_export]
macro_rules! builder {
(
$(#[doc = $doc: literal])*
@@ -37,7 +296,8 @@ macro_rules! builder {
$visibility: vis struct $name: ident
{
$(
- $(#[$field_attr: meta])*
+ $(#[doc = $field_doc: literal])*
+ $(#[builder(skip_generate_fn$($field_skip_generate_fn: tt)?)])?
$field_visibility: vis $field: ident: $field_type: ty,
)*
}
@@ -47,7 +307,7 @@ macro_rules! builder {
$visibility struct $name
{
$(
- $(#[$field_attr])*
+ $(#[doc = $field_doc])*
$field_visibility $field: $field_type,
)*
}
@@ -63,16 +323,23 @@ macro_rules! builder {
impl $builder_name
{
$(
- #[must_use]
- $visibility fn $field(mut self, $field: $field_type) -> Self
- {
- self.$field = $field;
- self
- }
+ $crate::expand_map_opt!(
+ $(true $($field_skip_generate_fn)?)?,
+ no_occurance=(
+ #[must_use]
+ $visibility fn $field(mut self, $field: $field_type) -> Self
+ {
+ self.$field = $field;
+ self
+ }
+ ),
+ occurance=()
+ );
)*
#[must_use]
- $visibility fn build(self) -> $name {
+ $visibility const fn build(self) -> $name
+ {
$name {
$(
$field: self.$field,
@@ -83,6 +350,7 @@ macro_rules! builder {
impl From<$name> for $builder_name
{
+ #[allow(unused_variables)]
fn from(built: $name) -> Self
{
Self {
@@ -94,39 +362,3 @@ macro_rules! builder {
}
};
}
-
-pub(crate) use builder;
-
-/// Wrapper that ensures the contained value will never be dropped.
-#[derive(Debug)]
-pub struct NeverDrop<Value>
-{
- value: ManuallyDrop<Value>,
-}
-
-impl<Value> NeverDrop<Value>
-{
- #[must_use]
- pub fn new(value: Value) -> Self
- {
- Self { value: ManuallyDrop::new(value) }
- }
-}
-
-impl<Value> Deref for NeverDrop<Value>
-{
- type Target = Value;
-
- fn deref(&self) -> &Self::Target
- {
- &self.value
- }
-}
-
-impl<Value> DerefMut for NeverDrop<Value>
-{
- fn deref_mut(&mut self) -> &mut Self::Target
- {
- &mut self.value
- }
-}
diff --git a/engine/src/vertex.rs b/engine/src/vertex.rs
deleted file mode 100644
index 897ee97..0000000
--- a/engine/src/vertex.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-use std::mem::size_of;
-
-use crate::util::builder;
-use crate::vector::{Vec2, Vec3};
-
-builder! {
-#[builder(name = Builder, derives = (Debug, Default))]
-#[derive(Debug, Clone, Default)]
-#[repr(C)]
-pub struct Vertex
-{
- pos: Vec3<f32>,
- texture_coords: Vec2<f32>,
- normal: Vec3<f32>,
-}
-}
-
-impl Vertex
-{
- pub(crate) fn attrs() -> &'static [Attribute]
- {
- #[allow(clippy::cast_possible_truncation)]
- &[
- Attribute {
- index: 0,
- component_type: AttributeComponentType::Float,
- component_cnt: AttributeComponentCnt::Three,
- component_size: size_of::<f32>() as u32,
- },
- Attribute {
- index: 1,
- component_type: AttributeComponentType::Float,
- component_cnt: AttributeComponentCnt::Two,
- component_size: size_of::<f32>() as u32,
- },
- Attribute {
- index: 2,
- component_type: AttributeComponentType::Float,
- component_cnt: AttributeComponentCnt::Three,
- component_size: size_of::<f32>() as u32,
- },
- ]
- }
-}
-
-pub(crate) struct Attribute
-{
- pub(crate) index: u32,
- pub(crate) component_type: AttributeComponentType,
- pub(crate) component_cnt: AttributeComponentCnt,
- pub(crate) component_size: u32,
-}
-
-pub(crate) enum AttributeComponentType
-{
- Float,
-}
-
-#[derive(Debug, Clone, Copy)]
-#[repr(u32)]
-#[allow(dead_code)]
-pub(crate) enum AttributeComponentCnt
-{
- One = 1,
- Two = 2,
- Three = 3,
- Four = 4,
-}
diff --git a/engine/src/window.rs b/engine/src/window.rs
deleted file mode 100644
index ccc1b8d..0000000
--- a/engine/src/window.rs
+++ /dev/null
@@ -1,318 +0,0 @@
-use std::borrow::Cow;
-use std::ffi::{CStr, CString};
-
-use ecs::actions::Actions;
-use ecs::extension::Collector as ExtensionCollector;
-use ecs::sole::Single;
-use ecs::Sole;
-use glfw::WindowSize;
-
-use crate::data_types::dimens::Dimens;
-use crate::event::{Conclude as ConcludeEvent, Start as StartEvent};
-use crate::vector::Vec2;
-
-mod reexports
-{
- pub use glfw::window::{
- CursorMode,
- Hint as CreationHint,
- HintValue as CreationHintValue,
- InputMode,
- Key,
- KeyModifiers,
- KeyState,
- };
-}
-
-pub use reexports::*;
-
-#[derive(Debug, Sole)]
-/// Has to be dropped last since it holds the OpenGL context.
-#[sole(drop_last)]
-pub struct Window
-{
- inner: glfw::Window,
-}
-
-impl Window
-{
- /// Returns a new Window builder.
- #[must_use]
- pub fn builder() -> Builder
- {
- Builder::default()
- }
-
- /// Sets the value of a input mode.
- ///
- /// # Errors
- /// Returns `Err` if the input mode is unsupported on the current system.
- pub fn set_input_mode(
- &self,
- input_mode: InputMode,
- enabled: bool,
- ) -> Result<(), Error>
- {
- Ok(self.inner.set_input_mode(input_mode, enabled)?)
- }
-
- /// Sets the cursor mode.
- ///
- /// # Errors
- /// If a platform error occurs.
- pub fn set_cursor_mode(&self, cursor_mode: CursorMode) -> Result<(), Error>
- {
- Ok(self.inner.set_cursor_mode(cursor_mode)?)
- }
-
- /// Returns whether or not the window should close. Will return true when the user has
- /// attempted to close the window.
- #[must_use]
- pub fn should_close(&self) -> bool
- {
- self.inner.should_close()
- }
-
- /// Processes all pending events.
- ///
- /// # Errors
- /// If a platform error occurs.
- pub fn poll_events(&self) -> Result<(), Error>
- {
- Ok(self.inner.poll_events()?)
- }
-
- /// Swaps the front and back buffers of the window.
- ///
- /// # Errors
- /// Will return `Err` if a platform error occurs or if no OpenGL window context
- /// is present.
- pub fn swap_buffers(&self) -> Result<(), Error>
- {
- Ok(self.inner.swap_buffers()?)
- }
-
- /// Returns the size of the window.
- ///
- /// # Errors
- /// Will return `Err` if a platform error occurs.
- pub fn size(&self) -> Result<Dimens<u32>, Error>
- {
- let size = self.inner.size()?;
-
- Ok(Dimens {
- width: size.width,
- height: size.height,
- })
- }
-
- /// Returns the address of the specified OpenGL function, if it is supported by the
- /// current OpenGL context.
- ///
- /// # Errors
- /// Will return `Err` if a platform error occurs or if no current context has
- /// been set.
- ///
- /// # Panics
- /// Will panic if the `proc_name` argument contains a nul byte.
- pub fn get_proc_address(
- &self,
- proc_name: &str,
- ) -> Result<unsafe extern "C" fn(), Error>
- {
- let proc_name_c: Cow<CStr> = CStr::from_bytes_with_nul(proc_name.as_bytes())
- .map(Cow::Borrowed)
- .or_else(|_| CString::new(proc_name).map(Cow::Owned))
- .expect("OpenGL function name contains a nul byte");
-
- Ok(self.inner.get_proc_address(&proc_name_c)?)
- }
-
- /// Makes the OpenGL context of the window current for the calling thread.
- ///
- /// # Errors
- /// Will return `Err` if a platform error occurs or if no OpenGL context is
- /// present.
- pub fn make_context_current(&self) -> Result<(), Error>
- {
- Ok(self.inner.make_context_current()?)
- }
-
- /// Sets the window's framebuffer size callback.
- pub fn set_framebuffer_size_callback(&self, callback: impl Fn(Dimens<u32>) + 'static)
- {
- self.inner.set_framebuffer_size_callback(move |size| {
- callback(Dimens {
- width: size.width,
- height: size.height,
- });
- });
- }
-
- /// Sets the window's key size callback.
- pub fn set_key_callback(
- &self,
- callback: impl Fn(Key, i32, KeyState, KeyModifiers) + 'static,
- )
- {
- self.inner.set_key_callback(callback);
- }
-
- /// Sets the window's cursor position callback.
- pub fn set_cursor_pos_callback(&self, callback: impl Fn(Vec2<f64>) + 'static)
- {
- self.inner
- .set_cursor_pos_callback(move |pos| callback(Vec2 { x: pos.x, y: pos.y }));
- }
-
- /// Sets the window's close callback.
- pub fn set_close_callback(&self, callback: impl Fn() + 'static)
- {
- self.inner.set_close_callback(callback);
- }
-
- /// Sets the window's focus callback. The callback is called when the window loses or
- /// gains input focus.
- pub fn set_focus_callback(&self, callback: impl Fn(bool) + 'static)
- {
- self.inner.set_focus_callback(callback);
- }
-}
-
-/// [`Window`] builder.
-#[derive(Debug, Clone, Default)]
-pub struct Builder
-{
- inner: glfw::WindowBuilder,
-}
-
-impl Builder
-{
- #[must_use]
- pub fn creation_hint(mut self, hint: CreationHint, value: CreationHintValue) -> Self
- {
- self.inner = self.inner.hint(hint, value);
-
- self
- }
-
- /// Creates a new window.
- ///
- /// # Errors
- /// Will return `Err` if the title contains a internal nul byte or if a platform error
- /// occurs.
- pub fn create(&self, size: Dimens<u32>, title: &str) -> Result<Window, Error>
- {
- let builder = self.inner.clone().hint(
- CreationHint::OpenGLDebugContext,
- CreationHintValue::Bool(cfg!(feature = "debug")),
- );
-
- let window = builder.create(
- &WindowSize {
- width: size.width,
- height: size.height,
- },
- title,
- )?;
-
- Ok(Window { inner: window })
- }
-}
-
-#[derive(Debug)]
-pub struct Extension
-{
- window_builder: Builder,
- window_size: Dimens<u32>,
- window_title: String,
-}
-
-impl Extension
-{
- #[must_use]
- pub fn new(window_builder: Builder) -> Self
- {
- Self { window_builder, ..Default::default() }
- }
-
- #[must_use]
- pub fn window_size(mut self, window_size: Dimens<u32>) -> Self
- {
- self.window_size = window_size;
-
- self
- }
-
- #[must_use]
- pub fn window_title(mut self, window_title: impl Into<String>) -> Self
- {
- self.window_title = window_title.into();
-
- self
- }
-}
-
-impl ecs::extension::Extension for Extension
-{
- fn collect(self, mut collector: ExtensionCollector<'_>)
- {
- collector.add_system(StartEvent, initialize);
- collector.add_system(ConcludeEvent, update);
-
- let window = self
- .window_builder
- .create(self.window_size, &self.window_title)
- .unwrap();
-
- window.set_cursor_mode(CursorMode::Normal).unwrap();
-
- collector.add_sole(window).ok();
- }
-}
-
-impl Default for Extension
-{
- fn default() -> Self
- {
- Self {
- window_builder: Builder::default(),
- window_size: Dimens { width: 1920, height: 1080 },
- window_title: String::new(),
- }
- }
-}
-
-#[derive(Debug, thiserror::Error)]
-#[error(transparent)]
-pub struct Error(glfw::Error);
-
-impl From<glfw::Error> for Error
-{
- fn from(err: glfw::Error) -> Self
- {
- Self(err)
- }
-}
-
-fn initialize(window: Single<Window>, actions: Actions)
-{
- let actions_weak_ref = actions.to_weak_ref();
-
- window.set_close_callback(move || {
- let actions_weak_ref = actions_weak_ref.clone();
-
- let actions_ref = actions_weak_ref.access().expect("No world");
-
- actions_ref.to_actions().stop();
- });
-}
-
-fn update(window: Single<Window>)
-{
- window
- .swap_buffers()
- .expect("Failed to swap window buffers");
-
- window.poll_events().expect("Failed to poll window events");
-}
diff --git a/engine/src/windowing.rs b/engine/src/windowing.rs
new file mode 100644
index 0000000..2493851
--- /dev/null
+++ b/engine/src/windowing.rs
@@ -0,0 +1,1169 @@
+use std::collections::VecDeque;
+use std::hint::cold_path;
+use std::panic::{catch_unwind, AssertUnwindSafe};
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::{Arc, Condvar, Mutex, Weak};
+use std::thread::Builder as ThreadBuilder;
+use std::time::Duration;
+
+use bitflags::{bitflags, bitflags_match, Flags};
+use crossbeam_queue::ArrayQueue;
+use intmap::IntMap;
+use raw_window_handle::{DisplayHandle, HandleError, HasDisplayHandle, WindowHandle};
+use winit::application::ApplicationHandler;
+use winit::error::EventLoopError;
+use winit::event::{DeviceEvent, DeviceId, StartCause, WindowEvent};
+use winit::event_loop::{
+ ActiveEventLoop,
+ ControlFlow as EventLoopControlFlow,
+ EventLoop,
+ OwnedDisplayHandle,
+};
+use winit::keyboard::PhysicalKey;
+use winit::window::{Window as WinitWindow, WindowId as WinitWindowId};
+
+use crate::ecs::actions::Actions;
+use crate::ecs::component::Component;
+use crate::ecs::entity::obtainer::Obtainer as EntityObtainer;
+use crate::ecs::event::component::{Added, Changed, EventMatchExt, Removed};
+use crate::ecs::pair::{ChildOf, Pair};
+use crate::ecs::phase::{Phase, PRE_UPDATE as PRE_UPDATE_PHASE};
+use crate::ecs::sole::Single;
+use crate::ecs::system::observer::Observe;
+use crate::ecs::uid::Uid;
+use crate::ecs::util::StreamingIterator;
+use crate::ecs::{declare_entity, pair, Query, Sole};
+use crate::util::BitArray;
+use crate::vector::Vec2;
+use crate::windowing::dpi::{PhysicalPosition, PhysicalSize, Position};
+use crate::windowing::keyboard::{Key, KeyState, Keyboard, UnknownKeyCodeError};
+use crate::windowing::monitor::Handle as MonitorHandle;
+use crate::windowing::mouse::{
+ Button as MouseButton,
+ ButtonState as MouseButtonState,
+ Buttons as MouseButtons,
+ Mouse,
+ ScrollDelta as MouseScrollDelta,
+};
+use crate::windowing::window::{
+ Closed as WindowClosed,
+ CreationAttributes as WindowCreationAttributes,
+ CreationReady as WindowCreationReady,
+ CursorGrabMode,
+ Id as WindowId,
+ Window,
+};
+use crate::{error, Error as EngineError};
+
+pub mod dpi;
+pub mod keyboard;
+pub mod monitor;
+pub mod mouse;
+pub mod window;
+
+const MESSAGE_FROM_APP_QUEUE_SIZE: usize = 512;
+
+const MESSAGE_TO_APP_QUEUE_SIZE: usize = 16; // Increase if more messages are added
+
+const TEXT_KEY_QUEUE_SIZE: usize = 255;
+
+static CONTEXT_CREATED: AtomicBool = AtomicBool::new(false);
+
+declare_entity! {
+pub PHASE: (Phase, pair!(ChildOf, { *PRE_UPDATE_PHASE }));
+}
+
+#[derive(Debug, Default)]
+#[non_exhaustive]
+pub struct Extension {}
+
+impl crate::ecs::extension::Extension for Extension
+{
+ fn collect(self, mut collector: crate::ecs::extension::Collector<'_>)
+ {
+ if !CONTEXT_CREATED.load(Ordering::Relaxed) {
+ collector.add_sole(Context::new()).ok();
+ }
+
+ collector.add_sole(Keyboard::default()).ok();
+ collector.add_sole(Mouse::default()).ok();
+ collector.add_sole(MouseButtons::default()).ok();
+
+ collector.spawn_declared_entity(&PHASE);
+
+ collector.add_system(*PHASE, update_stuff);
+
+ collector.add_observer(handle_window_changed);
+ collector.add_observer(handle_window_removed);
+ collector.add_observer(handle_window_creation_ready);
+ }
+}
+
+fn handle_window_creation_ready(
+ observe: Observe<Pair<Added, WindowCreationReady>>,
+ context: Single<Context>,
+)
+{
+ let Ok(context) = context.get() else {
+ unreachable!();
+ };
+
+ for evt_match in &observe {
+ let Some(ent) = evt_match.try_get_entity() else {
+ unreachable!();
+ };
+
+ if ent.has_component(Window::id()) || ent.has_component(WindowClosed::id()) {
+ continue;
+ }
+
+ let Some(window_creation_attrs) = ent.get::<WindowCreationAttributes>() else {
+ unreachable!();
+ };
+
+ context.try_send_message_to_app(MessageToApp::CreateWindow(
+ ent.uid(),
+ window_creation_attrs.clone(),
+ ));
+ }
+}
+
+#[tracing::instrument(skip_all)]
+fn update_stuff(
+ mut context: Single<Context>,
+ mut keyboard: Single<Keyboard>,
+ mut mouse: Single<Mouse>,
+ mut mouse_buttons: Single<MouseButtons>,
+ mut actions: Actions,
+ entity_obtainer: EntityObtainer,
+) -> Result<(), EngineError>
+{
+ let Ok(context) = context.get_mut() else {
+ unreachable!();
+ };
+
+ let keyboard = keyboard.get_mut()?;
+ let mouse = mouse.get_mut()?;
+ let mouse_buttons = mouse_buttons.get_mut()?;
+
+ if context.display_handle.is_none() {
+ cold_path();
+ actions.stop();
+ return Ok(());
+ }
+
+ keyboard.make_key_states_previous();
+
+ {
+ let Some(mut input) = context
+ .shared_state
+ .input
+ .try_lock_for(Duration::from_millis(100))
+ else {
+ tracing::error!("Locking input mutex timed out after 100ms");
+ return Ok(());
+ };
+
+ mouse.curr_tick_position_delta = input.relative_mouse_pos_delta;
+ mouse.position = input.absolute_mouse_pos.clone();
+ mouse.curr_tick_scroll_delta = input.mouse_scroll_delta.clone();
+
+ for (mouse_button, mouse_button_input) in input.mouse_buttons.iter_mut() {
+ mouse_buttons.set_previous_to_current(mouse_button);
+
+ if mouse_button_input.flags.is_all() {
+ match mouse_buttons.get_previous(mouse_button) {
+ MouseButtonState::Pressed => {
+ mouse_buttons.set(mouse_button, MouseButtonState::Released);
+
+ mouse_button_input.flags.remove(MouseButtonFlags::RELEASED);
+ }
+ MouseButtonState::Released => {
+ mouse_buttons.set(mouse_button, MouseButtonState::Pressed);
+
+ mouse_button_input.flags.remove(MouseButtonFlags::PRESSED);
+ }
+ }
+
+ continue;
+ }
+
+ let mouse_button_state = if mouse_button_input.flags.is_empty() {
+ let Some(mouse_button_state) = mouse_button_input.lagged.pop_front()
+ else {
+ continue;
+ };
+
+ mouse_button_state
+ } else {
+ bitflags_match!(mouse_button_input.flags, {
+ MouseButtonFlags::PRESSED => MouseButtonState::Pressed,
+ MouseButtonFlags::RELEASED => MouseButtonState::Released,
+ _ => unreachable!()
+ })
+ };
+
+ mouse_buttons.set(mouse_button, mouse_button_state);
+
+ mouse_button_input.flags.clear();
+ }
+
+ let mut key_updates = input.keys.iter_occupied_mut();
+
+ while let Some(mut key_item) = key_updates.streaming_next() {
+ let key = Key::KEYS[key_item.index];
+
+ if key_item.bits == KEY_PRESSED_BITS | KEY_RELEASED_BITS {
+ match keyboard.get_key_state(key) {
+ KeyState::Pressed => {
+ keyboard.set_key_state(key, KeyState::Released);
+
+ key_item.clear_and_set(KEY_PRESSED_BITS, u8::MAX);
+ }
+ KeyState::Released => {
+ keyboard.set_key_state(key, KeyState::Pressed);
+
+ key_item.clear_and_set(KEY_RELEASED_BITS, u8::MAX);
+ }
+ }
+
+ continue;
+ }
+
+ let key_state = match key_item.bits {
+ KEY_PRESSED_BITS => KeyState::Pressed,
+ KEY_RELEASED_BITS => KeyState::Released,
+ _ => unreachable!(),
+ };
+
+ keyboard.set_key_state(key, key_state);
+
+ key_item.clear_and_set(0, u8::MAX);
+ }
+
+ input.relative_mouse_pos_delta = Vec2 { x: 0.0, y: 0.0 };
+ input.mouse_scroll_delta = MouseScrollDelta { vert_lines: 0.0, hor_lines: 0.0 };
+ };
+
+ keyboard.set_text_keys(iter_array_queue(&context.shared_state.text_keys));
+
+ let Context {
+ ref mut windows, ref shared_state, ..
+ } = *context;
+
+ for message in iter_array_queue(&shared_state.message_from_app_queue) {
+ tracing::trace!(message=?message, "Received message from app");
+
+ match message {
+ MessageFromApp::WindowCreated(
+ window_ent_id,
+ winit_window,
+ window_creation_attrs,
+ ) => {
+ actions.add_components(
+ window_ent_id,
+ (Window::new(&winit_window, &window_creation_attrs),),
+ );
+
+ actions.remove_comps::<(WindowCreationReady,)>(window_ent_id);
+
+ let window_id = WindowId::from_inner(winit_window.id());
+
+ windows.insert(window_id, (winit_window, window_ent_id));
+
+ tracing::info!(
+ window_id = ?window_id,
+ window_title = %window_creation_attrs.title,
+ "Window creation completed"
+ );
+ }
+ MessageFromApp::WindowResized(window_id, new_window_size) => {
+ tracing::trace!(
+ window_id = ?window_id,
+ "Received window resized message"
+ );
+
+ let Some(window_ent_id) =
+ windows.get(window_id).map(|(_, ent_id)| ent_id)
+ else {
+ tracing::error!(
+ wid = ?window_id,
+ "Window does not exist in windowing context"
+ );
+ continue;
+ };
+
+ let Some(window_ent) = entity_obtainer.get_entity(*window_ent_id) else {
+ continue;
+ };
+
+ let Some(mut window) = window_ent.get_mut::<Window>() else {
+ continue;
+ };
+
+ window.inner_size = new_window_size;
+
+ window.set_changed();
+ }
+ MessageFromApp::WindowCloseRequested(window_id) => {
+ let Some(window_ent_id) =
+ windows.get(window_id).map(|(_, ent_id)| ent_id)
+ else {
+ tracing::error!(
+ wid = ?window_id,
+ "Window does not exist in windowing context"
+ );
+ continue;
+ };
+
+ actions.remove_comps::<(Window,)>(*window_ent_id);
+ }
+ MessageFromApp::WindowScaleFactorChanged(window_id, scale_factor) => {
+ let Some(window_ent_id) =
+ windows.get(window_id).map(|(_, ent_id)| ent_id)
+ else {
+ tracing::error!(
+ wid = ?window_id,
+ "Window does not exist in windowing context"
+ );
+ continue;
+ };
+
+ let Some(window_ent) = entity_obtainer.get_entity(*window_ent_id) else {
+ continue;
+ };
+
+ let Some(mut window) = window_ent.get_mut::<Window>() else {
+ continue;
+ };
+
+ window.set_scale_factor(scale_factor);
+
+ window.set_changed();
+ }
+ }
+ }
+
+ if shared_state.thread_panicked.load(Ordering::Relaxed) {
+ cold_path();
+ return Err(error!("Windowing app thread panicked"));
+ }
+
+ if shared_state.has_thread_err.load(Ordering::Relaxed) {
+ cold_path();
+
+ let Ok(mut err) = shared_state.thread_err.try_lock() else {
+ // The windowing app thread always unlocks the thread_err mutex before
+ // setting the has_thread_err atomic
+ unreachable!();
+ };
+
+ let Some(err) = err.take() else {
+ // The has_thread_err atomic is set to true so this option is always
+ // Some
+ unreachable!();
+ };
+
+ return Err(err.into());
+ }
+
+ Ok(())
+}
+
+fn handle_window_changed(
+ observe: Observe<'_, Pair<Changed, Window>>,
+ context: Single<Context>,
+)
+{
+ let Ok(context) = context.get() else {
+ unreachable!();
+ };
+
+ for evt_match in &observe {
+ let window_ent_id = evt_match.entity_id();
+
+ let mut window = evt_match.get_ent_target_comp_mut();
+
+ let Some((winit_window, _)) = context.windows.get(window.wid()) else {
+ tracing::error!(
+ wid = ?window.wid(),
+ entity_id = %window_ent_id,
+ "Window does not exist in windowing context",
+ );
+ continue;
+ };
+
+ let window_apply_results = window.apply(winit_window);
+
+ if let Err(actual_window_inner_size) = window_apply_results.inner_size_request {
+ window.inner_size = actual_window_inner_size;
+ }
+
+ context.try_send_message_to_app(MessageToApp::SetWindowCursorGrabMode(
+ window.wid(),
+ window.cursor_grab_mode,
+ ));
+ }
+}
+
+fn handle_window_removed(
+ observe: Observe<Pair<Removed, Window>>,
+ window_query: Query<(&Window,)>,
+ mut context: Single<Context>,
+ mut actions: Actions,
+)
+{
+ let Ok(context) = context.get_mut() else {
+ unreachable!();
+ };
+
+ for evt_match in &observe {
+ let window = evt_match.get_ent_target_comp();
+
+ context.windows.remove(window.wid());
+
+ actions.add_components(evt_match.entity_id(), (WindowClosed,));
+ }
+
+ if window_query.iter().count() == 1 {
+ actions.stop();
+ }
+}
+
+#[derive(Debug, Sole)]
+pub struct Context
+{
+ shared_state: Arc<SharedState>,
+ display_handle: Option<OwnedDisplayHandle>,
+ windows: IntMap<WindowId, (Arc<WinitWindow>, Uid)>,
+ available_monitors: Vec<MonitorHandle>,
+ primary_monitor: Option<MonitorHandle>,
+}
+
+impl Context
+{
+ pub fn display_handle(&self) -> Option<DisplayHandle<'_>>
+ {
+ self.display_handle.as_ref()?.display_handle().ok()
+ }
+
+ /// Returns the specified window as a window handle, if it exists.
+ ///
+ /// # Safety
+ /// The Window handle must only be used with thread safe APIs.
+ pub unsafe fn get_window_as_handle(
+ &self,
+ window_id: &WindowId,
+ ) -> Option<Result<WindowHandle<'_>, HandleError>>
+ {
+ self.windows.get(*window_id).map(|(winit_window, _)| {
+ #[cfg(windows)]
+ {
+ use winit::platform::windows::WindowExtWindows;
+
+ // SAFETY: I don't care
+ unsafe { winit_window.window_handle_any_thread() }
+ }
+
+ #[cfg(not(windows))]
+ {
+ use raw_window_handle::HasWindowHandle;
+
+ winit_window.window_handle()
+ }
+ })
+ }
+
+ /// Returns the last known primary monitor. This monitor may or may not exist any
+ /// longer and it may or may not be the primary monitor any longer.
+ pub fn get_primary_monitor(&self) -> Option<&MonitorHandle>
+ {
+ self.primary_monitor.as_ref()
+ }
+
+ /// Returns an iterator of the last known available monitors. These monitors may or
+ /// may not exist any longer.
+ pub fn available_monitors(&self) -> impl Iterator<Item = &MonitorHandle>
+ {
+ self.available_monitors.iter()
+ }
+
+ fn try_send_message_to_app(&self, message: MessageToApp)
+ {
+ if self
+ .shared_state
+ .message_to_app_queue
+ .push(message)
+ .is_err()
+ {
+ tracing::error!(
+ "Failed to send message. Queue for messages to windowing app is full"
+ );
+ }
+ }
+}
+
+impl Context
+{
+ /// Returns a new windowing context.
+ ///
+ /// # Panics
+ /// Will panic if a windowing context have already been created.
+ pub fn new() -> Self
+ {
+ if CONTEXT_CREATED.swap(true, Ordering::Relaxed) {
+ panic!("A windowing context have already been created");
+ }
+
+ let shared_state = Arc::new(SharedState::default());
+ let shared_state_b = shared_state.clone();
+
+ if let Err(err) = ThreadBuilder::new()
+ .name("windowing app".to_string())
+ .spawn(move || {
+ let shared_state_c = shared_state_b.clone();
+
+ match catch_unwind(move || {
+ let mut app = App {
+ shared_state: shared_state_b,
+ windows: IntMap::with_capacity(1),
+ };
+
+ let event_loop = match create_event_loop() {
+ Ok(event_loop) => event_loop,
+ Err(err) => {
+ return Err((app, AppThreadError::CreateEventLoop(err)));
+ }
+ };
+
+ event_loop.set_control_flow(EventLoopControlFlow::Poll);
+
+ event_loop
+ .run_app(&mut app)
+ .map_err(|err| (app, AppThreadError::StartEventLoop(err)))?;
+
+ Ok(())
+ }) {
+ Ok(Ok(())) => {}
+ Ok(Err((app, err))) => {
+ {
+ let Ok(mut thread_err) =
+ app.shared_state.thread_err.try_lock()
+ else {
+ // The thread_err mutex is only locked by the main thread
+ // when the has_thread_err atomic is set to true, which
+ // it can not be set to yet
+ unreachable!();
+ };
+
+ *thread_err = Some(err);
+ }
+
+ app.shared_state
+ .has_thread_err
+ .store(true, Ordering::Relaxed);
+
+ app.shared_state.init_data_cond.notify_one();
+ }
+ Err(_) => {
+ shared_state_c
+ .thread_panicked
+ .store(true, Ordering::Relaxed);
+
+ shared_state_c.init_data_cond.notify_one();
+ }
+ };
+ })
+ {
+ tracing::error!(
+ "Failed to create windowing app thread: {:#}",
+ crate::Error::new(err)
+ );
+
+ return Self {
+ shared_state,
+ display_handle: None,
+ windows: IntMap::with_capacity(1),
+ available_monitors: Vec::with_capacity(2),
+ primary_monitor: None,
+ };
+ }
+
+ let mut init_data_lock = shared_state.init_data.lock().unwrap_or_else(|err| {
+ let mut lock = err.into_inner();
+ *lock = None;
+ lock
+ });
+
+ while init_data_lock.is_none()
+ && !shared_state.has_thread_err.load(Ordering::Relaxed)
+ && !shared_state.thread_panicked.load(Ordering::Relaxed)
+ {
+ init_data_lock = shared_state
+ .init_data_cond
+ .wait(init_data_lock)
+ .unwrap_or_else(|err| {
+ let mut lock = err.into_inner();
+ *lock = None;
+ lock
+ });
+ }
+
+ let init_data = init_data_lock.take();
+
+ drop(init_data_lock);
+
+ let Some(init_data) = init_data else {
+ if shared_state.thread_panicked.load(Ordering::Relaxed) {
+ tracing::error!("Windowing app thread panicked");
+ } else if shared_state.has_thread_err.load(Ordering::Relaxed) {
+ let Ok(mut err) = shared_state.thread_err.try_lock() else {
+ // The windowing app thread always unlocks the thread_err mutex before
+ // setting the has_thread_err atomic
+ unreachable!();
+ };
+
+ let Some(err) = err.take() else {
+ // The has_thread_err atomic is set to true so this option is always
+ // Some
+ unreachable!();
+ };
+
+ tracing::error!(
+ "Error in windowing app thread: {:#}",
+ crate::Error::new(err)
+ );
+ } else {
+ tracing::error!("Windowing app initialization aborted unexpectedly");
+ }
+
+ return Self {
+ shared_state,
+ display_handle: None,
+ windows: IntMap::with_capacity(1),
+ available_monitors: Vec::with_capacity(2),
+ primary_monitor: None,
+ };
+ };
+
+ Self {
+ shared_state,
+ display_handle: Some(init_data.display),
+ windows: IntMap::with_capacity(1),
+ available_monitors: init_data.available_monitors,
+ primary_monitor: init_data.primary_monitor,
+ }
+ }
+}
+
+impl Drop for Context
+{
+ fn drop(&mut self)
+ {
+ self.shared_state.is_dropped.store(true, Ordering::Relaxed);
+ }
+}
+
+fn create_event_loop() -> Result<EventLoop<()>, EventLoopError>
+{
+ let mut event_loop_builder = EventLoop::builder();
+
+ cfg_select! {
+ target_os = "linux" => {
+ winit::platform::x11::EventLoopBuilderExtX11::with_any_thread(
+ &mut event_loop_builder,
+ true,
+ );
+ }
+ windows => {
+ winit::platform::windows::EventLoopBuilderExtWindows::with_any_thread(
+ &mut event_loop_builder,
+ true,
+ );
+ }
+ _ => {
+ compile_error!("Unsupported platform")
+ }
+ }
+
+ event_loop_builder.build()
+}
+
+#[derive(Debug, thiserror::Error)]
+enum AppThreadError
+{
+ #[error("Event loop creation failed")]
+ CreateEventLoop(#[source] winit::error::EventLoopError),
+
+ #[error("Starting event loop failed")]
+ StartEventLoop(#[source] winit::error::EventLoopError),
+}
+
+#[derive(Debug)]
+enum MessageFromApp
+{
+ WindowCreated(Uid, Arc<WinitWindow>, WindowCreationAttributes),
+ WindowResized(WindowId, PhysicalSize<u32>),
+ WindowCloseRequested(WindowId),
+ WindowScaleFactorChanged(WindowId, f64),
+}
+
+#[derive(Debug)]
+enum MessageToApp
+{
+ CreateWindow(Uid, WindowCreationAttributes),
+ SetWindowCursorGrabMode(WindowId, CursorGrabMode),
+}
+
+#[derive(Debug)]
+struct SharedState
+{
+ message_from_app_queue: ArrayQueue<MessageFromApp>,
+ message_to_app_queue: ArrayQueue<MessageToApp>,
+ input: AssertUnwindSafe<parking_lot::Mutex<Input>>,
+ text_keys: ArrayQueue<char>,
+ is_dropped: AtomicBool,
+ thread_panicked: AtomicBool,
+ thread_err: Mutex<Option<AppThreadError>>,
+ has_thread_err: AtomicBool,
+ init_data: Mutex<Option<InitData>>,
+ init_data_cond: Condvar,
+}
+
+impl Default for SharedState
+{
+ fn default() -> Self
+ {
+ Self {
+ message_from_app_queue: ArrayQueue::new(MESSAGE_FROM_APP_QUEUE_SIZE),
+ message_to_app_queue: ArrayQueue::new(MESSAGE_TO_APP_QUEUE_SIZE),
+ input: AssertUnwindSafe(parking_lot::Mutex::default()),
+ text_keys: ArrayQueue::new(TEXT_KEY_QUEUE_SIZE),
+ is_dropped: AtomicBool::new(false),
+ thread_panicked: AtomicBool::new(false),
+ thread_err: Mutex::new(None),
+ has_thread_err: AtomicBool::new(false),
+ init_data: Mutex::new(None),
+ init_data_cond: Condvar::new(),
+ }
+ }
+}
+
+#[derive(Debug)]
+struct Input
+{
+ relative_mouse_pos_delta: Vec2<f64>,
+ absolute_mouse_pos: PhysicalPosition<f64>,
+ mouse_scroll_delta: MouseScrollDelta,
+ mouse_buttons: IntMap<MouseButton, MouseButtonInput>,
+ keys: BitArray<{ (Key::KEYS.len() * BITS_PER_KEY).div_ceil(8) }, BITS_PER_KEY>,
+}
+
+impl Default for Input
+{
+ fn default() -> Self
+ {
+ Self {
+ relative_mouse_pos_delta: Vec2::default(),
+ absolute_mouse_pos: PhysicalPosition::default(),
+ mouse_scroll_delta: MouseScrollDelta::default(),
+ mouse_buttons: IntMap::from_iter([
+ (MouseButton::Left, MouseButtonInput::default()),
+ (MouseButton::Right, MouseButtonInput::default()),
+ (MouseButton::Middle, MouseButtonInput::default()),
+ (MouseButton::Back, MouseButtonInput::default()),
+ (MouseButton::Forward, MouseButtonInput::default()),
+ ]),
+ keys: BitArray::new(),
+ }
+ }
+}
+
+#[derive(Debug, Default)]
+struct MouseButtonInput
+{
+ flags: MouseButtonFlags,
+ lagged: VecDeque<MouseButtonState>,
+}
+
+bitflags! {
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+struct MouseButtonFlags: u8
+{
+ const PRESSED = 1 << 0;
+ const RELEASED = 1 << 1;
+}
+}
+
+const BITS_PER_KEY: usize = 2;
+
+const KEY_PRESSED_BITS: u8 = 0b10;
+const KEY_RELEASED_BITS: u8 = 0b01;
+
+#[derive(Debug)]
+struct InitData
+{
+ display: OwnedDisplayHandle,
+ available_monitors: Vec<MonitorHandle>,
+ primary_monitor: Option<MonitorHandle>,
+}
+
+#[derive(Debug)]
+struct App
+{
+ shared_state: Arc<SharedState>,
+ windows: IntMap<WindowId, (Weak<WinitWindow>, WindowSettings)>,
+}
+
+impl App
+{
+ #[tracing::instrument(skip_all)]
+ fn handle_received_messages(&mut self, event_loop: &ActiveEventLoop)
+ {
+ for message in iter_array_queue(&self.shared_state.message_to_app_queue) {
+ match message {
+ MessageToApp::CreateWindow(window_ent_id, window_creation_attrs) => {
+ tracing::info!(
+ "Creating window with title {}",
+ window_creation_attrs.title
+ );
+
+ let winit_window = Arc::new(
+ match event_loop.create_window(
+ window_creation_attrs.clone().into_window_attrs(),
+ ) {
+ Ok(window) => window,
+ Err(err) => {
+ tracing::error!(
+ "Failed to create window: {:#}",
+ crate::Error::new(err)
+ );
+ continue;
+ }
+ },
+ );
+
+ window_creation_attrs.apply_extra_attrs_to_window(&winit_window);
+
+ self.windows.insert(
+ WindowId::from_inner(winit_window.id()),
+ (
+ Arc::downgrade(&winit_window),
+ WindowSettings {
+ cursor_grab_mode: window_creation_attrs.cursor_grab_mode,
+ },
+ ),
+ );
+
+ self.send_message(MessageFromApp::WindowCreated(
+ window_ent_id,
+ winit_window,
+ window_creation_attrs,
+ ));
+ }
+ MessageToApp::SetWindowCursorGrabMode(window_id, cursor_grab_mode) => {
+ let Some((_, window_settings)) = self.windows.get_mut(window_id)
+ else {
+ tracing::warn!(
+ window_id=?window_id,
+ "Cannot set window cursor grab mode. Window not found"
+ );
+
+ continue;
+ };
+
+ window_settings.cursor_grab_mode = cursor_grab_mode;
+ }
+ }
+ }
+ }
+
+ #[tracing::instrument(skip_all)]
+ fn send_message(&self, message: MessageFromApp)
+ {
+ if self.shared_state.message_from_app_queue.is_full() {
+ tracing::warn!(
+ "Queue for messages to windowing app is full! Dropping oldest message"
+ );
+ }
+
+ self.shared_state.message_from_app_queue.force_push(message);
+ }
+
+ fn lock_input(&self) -> Option<parking_lot::MutexGuard<'_, Input>>
+ {
+ self.shared_state
+ .input
+ .try_lock_for(Duration::from_millis(100))
+ }
+}
+
+impl ApplicationHandler for App
+{
+ fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause)
+ {
+ match cause {
+ StartCause::Init => {
+ let available_monitors = event_loop
+ .available_monitors()
+ .map(MonitorHandle::from_winit_monitor_handle)
+ .collect::<Vec<_>>();
+
+ let Ok(mut init_data) = self.shared_state.init_data.lock() else {
+ tracing::error!("Init data mutex is poisoned, exiting event loop");
+ event_loop.exit();
+ return;
+ };
+
+ *init_data = Some(InitData {
+ display: event_loop.owned_display_handle(),
+ available_monitors: available_monitors,
+ primary_monitor: event_loop
+ .primary_monitor()
+ .map(|monitor| MonitorHandle::from_winit_monitor_handle(monitor)),
+ });
+
+ self.shared_state.init_data_cond.notify_one();
+ }
+ StartCause::Poll => {
+ if self.shared_state.is_dropped.load(Ordering::Relaxed) {
+ event_loop.exit();
+ return;
+ }
+
+ self.handle_received_messages(event_loop);
+ }
+ _ => {}
+ }
+ }
+
+ fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop)
+ {
+ for (window, _) in self.windows.values() {
+ let Some(window) = window.upgrade() else {
+ continue;
+ };
+
+ window.request_redraw();
+ }
+ }
+
+ fn resumed(&mut self, _event_loop: &ActiveEventLoop) {}
+
+ #[tracing::instrument(skip_all)]
+ fn window_event(
+ &mut self,
+ _event_loop: &ActiveEventLoop,
+ window_id: WinitWindowId,
+ event: WindowEvent,
+ )
+ {
+ match event {
+ WindowEvent::Resized(new_window_size) => {
+ self.send_message(MessageFromApp::WindowResized(
+ WindowId::from_inner(window_id),
+ new_window_size.into(),
+ ));
+ }
+ WindowEvent::CloseRequested => {
+ self.send_message(MessageFromApp::WindowCloseRequested(
+ WindowId::from_inner(window_id),
+ ));
+ }
+ WindowEvent::KeyboardInput {
+ device_id: _,
+ event: keyboard_event,
+ is_synthetic: _,
+ } => {
+ if let Some(key_text) = keyboard_event
+ .text
+ .filter(|_| keyboard_event.state.is_pressed())
+ {
+ for character in key_text.chars() {
+ if self.shared_state.text_keys.is_full() {
+ cold_path();
+ tracing::warn!(
+ "Text key queue is full. Dropping oldest character"
+ );
+ }
+
+ self.shared_state.text_keys.force_push(character);
+ }
+ }
+
+ if keyboard_event.repeat {
+ return;
+ }
+
+ let key_code = match keyboard_event.physical_key {
+ PhysicalKey::Code(key_code) => key_code,
+ PhysicalKey::Unidentified(native_key) => {
+ tracing::warn!("Ignoring unidentified key: {native_key:?}");
+ return;
+ }
+ };
+
+ let key: Key = match key_code.try_into() {
+ Ok(key) => key,
+ Err(UnknownKeyCodeError) => {
+ tracing::warn!("Ignoring key with unknown key code {key_code:?}");
+ return;
+ }
+ };
+
+ let Some(mut input) = self.lock_input() else {
+ tracing::error!("Locking input mutex timed out after 100ms");
+ return;
+ };
+
+ let key_state_bits = match keyboard_event.state.into() {
+ KeyState::Pressed => KEY_PRESSED_BITS,
+ KeyState::Released => KEY_RELEASED_BITS,
+ };
+
+ input.keys.clear_and_set(key as usize, key_state_bits, 0);
+ }
+ WindowEvent::CursorMoved { device_id: _, position } => {
+ {
+ let Some(mut input) = self.lock_input() else {
+ tracing::error!("Locking input mutex timed out after 100ms");
+ return;
+ };
+
+ input.absolute_mouse_pos = position.into();
+ }
+
+ let Some((window, window_settings)) =
+ self.windows.get(WindowId::from_inner(window_id))
+ else {
+ cold_path();
+ return;
+ };
+
+ if window_settings.cursor_grab_mode != CursorGrabMode::Locked {
+ return;
+ }
+
+ let Some(window) = window.upgrade() else {
+ cold_path();
+ return;
+ };
+
+ if !window.has_focus() {
+ return;
+ }
+
+ let window_size = window.inner_size();
+
+ if let Err(err) =
+ window.set_cursor_position(Position::Physical(PhysicalPosition {
+ x: window_size.width as i32 / 2,
+ y: window_size.height as i32 / 2,
+ }))
+ {
+ cold_path();
+ tracing::error!(
+ window_id=?window_id,
+ "Failed to lock cursor position: {:#}",
+ crate::Error::new(err)
+ );
+ };
+ }
+ WindowEvent::MouseWheel { device_id: _, delta, phase: _ } => {
+ let (hor_lines, vert_lines) = match delta {
+ winit::event::MouseScrollDelta::LineDelta(hor_lines, vert_lines) => {
+ (hor_lines, vert_lines)
+ }
+ winit::event::MouseScrollDelta::PixelDelta(pos_delta) => {
+ let hor_lines = match pos_delta.x.partial_cmp(&0.0) {
+ Some(std::cmp::Ordering::Greater) => 1.0,
+ Some(std::cmp::Ordering::Less) => -1.0,
+ _ => 0.0,
+ };
+
+ let vert_lines = match pos_delta.y.partial_cmp(&0.0) {
+ Some(std::cmp::Ordering::Greater) => 1.0,
+ Some(std::cmp::Ordering::Less) => -1.0,
+ _ => 0.0,
+ };
+
+ (hor_lines, vert_lines)
+ }
+ };
+
+ let Some(mut input) = self.lock_input() else {
+ tracing::error!("Locking input mutex timed out after 100ms");
+ return;
+ };
+
+ input.mouse_scroll_delta.hor_lines += hor_lines;
+ input.mouse_scroll_delta.vert_lines += vert_lines;
+ }
+ WindowEvent::MouseInput { device_id: _, state, button } => {
+ let Some(mut input) = self.lock_input() else {
+ tracing::error!("Locking input mutex timed out after 100ms");
+ return;
+ };
+
+ let button = MouseButton::from(button);
+ let button_state = MouseButtonState::from(state);
+
+ let button_input = input
+ .mouse_buttons
+ .entry(button)
+ .or_insert_with(|| MouseButtonInput::default());
+
+ if button_input.flags.is_all() || !button_input.lagged.is_empty() {
+ button_input.lagged.push_back(button_state);
+ return;
+ }
+
+ button_input.flags.insert(match button_state {
+ MouseButtonState::Pressed => MouseButtonFlags::PRESSED,
+ MouseButtonState::Released => MouseButtonFlags::RELEASED,
+ });
+ }
+ WindowEvent::ScaleFactorChanged { scale_factor, inner_size_writer: _ } => {
+ self.send_message(MessageFromApp::WindowScaleFactorChanged(
+ WindowId::from_inner(window_id),
+ scale_factor,
+ ));
+ }
+ _ => {}
+ }
+ }
+
+ #[tracing::instrument(skip_all)]
+ fn device_event(
+ &mut self,
+ _event_loop: &ActiveEventLoop,
+ _device_id: DeviceId,
+ device_event: DeviceEvent,
+ )
+ {
+ match device_event {
+ DeviceEvent::MouseMotion { delta } => {
+ let Some(mut input) = self.lock_input() else {
+ tracing::error!("Locking input mutex timed out after 100ms");
+ return;
+ };
+
+ input.relative_mouse_pos_delta += Vec2::from(delta);
+ }
+ _ => {}
+ }
+ }
+}
+
+#[derive(Debug, Default)]
+struct WindowSettings
+{
+ cursor_grab_mode: CursorGrabMode,
+}
+
+fn iter_array_queue<Item>(
+ queue: &ArrayQueue<Item>,
+) -> impl Iterator<Item = Item> + use<'_, Item>
+{
+ (0..queue.len()).into_iter().filter_map(|_| queue.pop())
+}
diff --git a/engine/src/windowing/dpi.rs b/engine/src/windowing/dpi.rs
new file mode 100644
index 0000000..e3b1be1
--- /dev/null
+++ b/engine/src/windowing/dpi.rs
@@ -0,0 +1,217 @@
+use crate::reflection::Reflection;
+
+macro_rules! gen_struct_from_to_impls {
+ ($struct: ident, fields=($($field: ident),*)) => {
+ impl<Pixel> From<winit::dpi::$struct<Pixel>> for $struct<Pixel>
+ {
+ fn from(source: winit::dpi::$struct<Pixel>) -> Self
+ {
+ Self {
+ $($field: source.$field),*
+ }
+ }
+ }
+
+ impl<Pixel> From<$struct<Pixel>> for winit::dpi::$struct<Pixel>
+ {
+ fn from(source: $struct<Pixel>) -> Self
+ {
+ Self {
+ $($field: source.$field),*
+ }
+ }
+ }
+ };
+}
+
+macro_rules! gen_enum_from_to_impls {
+ ($enum: ident, variants=($($variant: ident),*)) => {
+ impl From<winit::dpi::$enum> for $enum
+ {
+ fn from(source: winit::dpi::$enum) -> Self
+ {
+ match source {
+ $(winit::dpi::$enum::$variant(pos) => Self::$variant(pos.into())),*
+ }
+ }
+ }
+
+ impl From<$enum> for winit::dpi::$enum
+ {
+ fn from(source: $enum) -> Self
+ {
+ match source {
+ $($enum::$variant(pos) => Self::$variant(pos.into())),*
+ }
+ }
+ }
+ };
+}
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Reflection)]
+#[reflection(impl_with_generics(<u32>, <i32>, <f32>, <f64>))]
+pub struct PhysicalPosition<Pixel>
+{
+ pub x: Pixel,
+ pub y: Pixel,
+}
+
+impl<Pixel> PhysicalPosition<Pixel>
+{
+ pub fn to_logical<LogicalPixel>(
+ &self,
+ scale_factor: f64,
+ ) -> LogicalPosition<LogicalPixel>
+ where
+ Pixel: Into<f64> + Clone,
+ LogicalPixel: From<f64>,
+ {
+ let x = self.x.clone().into();
+ let y = self.y.clone().into();
+
+ LogicalPosition {
+ x: (x / scale_factor).into(),
+ y: (y / scale_factor).into(),
+ }
+ }
+
+ pub fn try_convert_from<SourcePixel>(
+ source: PhysicalPosition<SourcePixel>,
+ ) -> Result<Self, Pixel::Error>
+ where
+ Pixel: TryFrom<SourcePixel>,
+ {
+ Ok(Self {
+ x: source.x.try_into()?,
+ y: source.y.try_into()?,
+ })
+ }
+}
+
+gen_struct_from_to_impls!(PhysicalPosition, fields = (x, y));
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Reflection)]
+#[reflection(impl_with_generics(<f64>))]
+pub struct LogicalPosition<Pixel>
+{
+ pub x: Pixel,
+ pub y: Pixel,
+}
+
+impl<Pixel> LogicalPosition<Pixel>
+{
+ pub fn try_convert_from<SourcePixel>(
+ source: LogicalPosition<SourcePixel>,
+ ) -> Result<Self, Pixel::Error>
+ where
+ Pixel: TryFrom<SourcePixel>,
+ {
+ Ok(Self {
+ x: source.x.try_into()?,
+ y: source.y.try_into()?,
+ })
+ }
+}
+
+gen_struct_from_to_impls!(LogicalPosition, fields = (x, y));
+
+#[derive(Debug, Clone, Copy, PartialEq, Reflection)]
+pub enum Position
+{
+ Physical(PhysicalPosition<i32>),
+ Logical(LogicalPosition<f64>),
+}
+
+impl Default for Position
+{
+ fn default() -> Self
+ {
+ Self::Logical(LogicalPosition::default())
+ }
+}
+
+gen_enum_from_to_impls!(Position, variants = (Physical, Logical));
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Reflection)]
+#[reflection(impl_with_generics(<u32>))]
+pub struct PhysicalSize<Pixel>
+{
+ pub width: Pixel,
+ pub height: Pixel,
+}
+
+impl<Pixel> PhysicalSize<Pixel>
+{
+ pub fn to_logical<LogicalPixel>(&self, scale_factor: f64) -> LogicalSize<LogicalPixel>
+ where
+ Pixel: Into<f64> + Clone,
+ LogicalPixel: From<f64>,
+ {
+ let width = self.width.clone().into();
+ let height = self.height.clone().into();
+
+ LogicalSize {
+ width: (width / scale_factor).into(),
+ height: (height / scale_factor).into(),
+ }
+ }
+}
+
+impl<Pixel> PhysicalSize<Pixel>
+{
+ pub fn try_convert_from<SourcePixel>(
+ source: PhysicalSize<SourcePixel>,
+ ) -> Result<Self, Pixel::Error>
+ where
+ Pixel: TryFrom<SourcePixel>,
+ {
+ Ok(Self {
+ width: source.width.try_into()?,
+ height: source.height.try_into()?,
+ })
+ }
+}
+
+gen_struct_from_to_impls!(PhysicalSize, fields = (width, height));
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Reflection)]
+#[reflection(impl_with_generics(<f64>))]
+pub struct LogicalSize<Pixel>
+{
+ pub width: Pixel,
+ pub height: Pixel,
+}
+
+impl<Pixel> LogicalSize<Pixel>
+{
+ pub fn try_convert_from<SourcePixel>(
+ source: LogicalSize<SourcePixel>,
+ ) -> Result<Self, Pixel::Error>
+ where
+ Pixel: TryFrom<SourcePixel>,
+ {
+ Ok(Self {
+ width: source.width.try_into()?,
+ height: source.height.try_into()?,
+ })
+ }
+}
+
+gen_struct_from_to_impls!(LogicalSize, fields = (width, height));
+
+#[derive(Debug, Clone, Copy, PartialEq, Reflection)]
+pub enum Size
+{
+ Physical(PhysicalSize<u32>),
+ Logical(LogicalSize<f64>),
+}
+
+impl Default for Size
+{
+ fn default() -> Self
+ {
+ Self::Logical(LogicalSize::default())
+ }
+}
+
+gen_enum_from_to_impls!(Size, variants = (Physical, Logical));
diff --git a/engine/src/windowing/keyboard.rs b/engine/src/windowing/keyboard.rs
new file mode 100644
index 0000000..dffa95e
--- /dev/null
+++ b/engine/src/windowing/keyboard.rs
@@ -0,0 +1,826 @@
+use util_macros::VariantArr;
+
+use crate::ecs::Sole;
+use crate::util::BitArray;
+
+#[derive(Debug, Default, Sole)]
+pub struct Keyboard
+{
+ keys: BitArray<{ (Key::KEYS.len() * BITS_PER_KEY).div_ceil(8) }, BITS_PER_KEY>,
+ text_keys: String,
+}
+
+impl Keyboard
+{
+ /// Returns whether the given key was just pressed this frame. This function will
+ /// return `false` if the key was also pressed the previous frame.
+ pub fn just_pressed(&self, key: Key) -> bool
+ {
+ self.get_key_state(key) == KeyState::Pressed
+ && self.get_prev_key_state(key) == KeyState::Released
+ }
+
+ /// Returns whether the given key was just released this frame. This function will
+ /// return `false` if the key was also released the previous frame.
+ pub fn just_released(&self, key: Key) -> bool
+ {
+ self.get_key_state(key) == KeyState::Released
+ && self.get_prev_key_state(key) == KeyState::Pressed
+ }
+
+ /// Returns whether the given key is currently pressed.
+ pub fn pressed(&self, key: Key) -> bool
+ {
+ self.get_key_state(key) == KeyState::Pressed
+ }
+
+ /// Returns whether the given key is currently released.
+ pub fn released(&self, key: Key) -> bool
+ {
+ self.get_key_state(key) == KeyState::Released
+ }
+
+ /// Returns a iterator of key & key state pairs where each of the keys have a
+ /// different state than they had the previous frame.
+ pub fn new_key_states(&self) -> impl Iterator<Item = (Key, KeyState)> + use<'_>
+ {
+ self.keys.iter_occupied().filter_map(|(key_index, bits)| {
+ if bits == KEY_CURR_PRESSED_BITS | KEY_PREV_PRESSED_BITS {
+ return None;
+ }
+
+ let key = Key::KEYS[key_index];
+
+ let curr_state = match bits & KEY_CURR_PRESSED_BITS {
+ KEY_CURR_PRESSED_BITS => KeyState::Pressed,
+ 0 => KeyState::Released,
+ _ => unreachable!(),
+ };
+
+ Some((key, curr_state))
+ })
+ }
+
+ #[must_use]
+ pub fn get_key_state(&self, key: Key) -> KeyState
+ {
+ let bits = self.keys.get(key as usize);
+
+ let state = match bits & KEY_CURR_PRESSED_BITS {
+ KEY_CURR_PRESSED_BITS => KeyState::Pressed,
+ 0 => KeyState::Released,
+ _ => unreachable!(),
+ };
+
+ state
+ }
+
+ #[must_use]
+ pub fn get_prev_key_state(&self, key: Key) -> KeyState
+ {
+ let bits = self.keys.get(key as usize);
+
+ let state = match bits & KEY_PREV_PRESSED_BITS {
+ KEY_PREV_PRESSED_BITS => KeyState::Pressed,
+ 0 => KeyState::Released,
+ _ => unreachable!(),
+ };
+
+ state
+ }
+
+ pub fn text_keys(&self) -> &str
+ {
+ &self.text_keys
+ }
+
+ pub fn set_key_state(&mut self, key: Key, key_state: KeyState)
+ {
+ self.keys.clear_and_set(
+ key as usize,
+ match key_state {
+ KeyState::Pressed => KEY_CURR_PRESSED_BITS,
+ KeyState::Released => 0,
+ },
+ KEY_CURR_PRESSED_BITS,
+ );
+ }
+
+ pub fn make_key_states_previous(&mut self)
+ {
+ for byte in self.keys.bytes_mut() {
+ *byte = (*byte >> 1) & 0b01010101 | *byte & 0b10101010;
+ }
+ }
+
+ pub fn set_text_keys(&mut self, text_keys: impl IntoIterator<Item = char>)
+ {
+ self.text_keys.clear();
+ self.text_keys.extend(text_keys);
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, VariantArr)]
+#[variant_arr(name = KEYS)]
+#[non_exhaustive]
+pub enum Key
+{
+ /// <kbd>`</kbd> on a US keyboard. This is also called a backtick or grave.
+ /// This is the <kbd>半角</kbd>/<kbd>全角</kbd>/<kbd>漢字</kbd>
+ /// (hankaku/zenkaku/kanji) key on Japanese keyboards
+ Backquote,
+ /// Used for both the US <kbd>\\</kbd> (on the 101-key layout) and also for the key
+ /// located between the <kbd>"</kbd> and <kbd>Enter</kbd> keys on row C of the 102-,
+ /// 104- and 106-key layouts.
+ /// Labeled <kbd>#</kbd> on a UK (102) keyboard.
+ Backslash,
+ /// <kbd>[</kbd> on a US keyboard.
+ BracketLeft,
+ /// <kbd>]</kbd> on a US keyboard.
+ BracketRight,
+ /// <kbd>,</kbd> on a US keyboard.
+ Comma,
+ /// <kbd>0</kbd> on a US keyboard.
+ Digit0,
+ /// <kbd>1</kbd> on a US keyboard.
+ Digit1,
+ /// <kbd>2</kbd> on a US keyboard.
+ Digit2,
+ /// <kbd>3</kbd> on a US keyboard.
+ Digit3,
+ /// <kbd>4</kbd> on a US keyboard.
+ Digit4,
+ /// <kbd>5</kbd> on a US keyboard.
+ Digit5,
+ /// <kbd>6</kbd> on a US keyboard.
+ Digit6,
+ /// <kbd>7</kbd> on a US keyboard.
+ Digit7,
+ /// <kbd>8</kbd> on a US keyboard.
+ Digit8,
+ /// <kbd>9</kbd> on a US keyboard.
+ Digit9,
+ /// <kbd>=</kbd> on a US keyboard.
+ Equal,
+ /// Located between the left <kbd>Shift</kbd> and <kbd>Z</kbd> keys.
+ /// Labeled <kbd>\\</kbd> on a UK keyboard.
+ IntlBackslash,
+ /// Located between the <kbd>/</kbd> and right <kbd>Shift</kbd> keys.
+ /// Labeled <kbd>\\</kbd> (ro) on a Japanese keyboard.
+ IntlRo,
+ /// Located between the <kbd>=</kbd> and <kbd>Backspace</kbd> keys.
+ /// Labeled <kbd>¥</kbd> (yen) on a Japanese keyboard. <kbd>\\</kbd> on a
+ /// Russian keyboard.
+ IntlYen,
+ /// <kbd>a</kbd> on a US keyboard.
+ /// Labeled <kbd>q</kbd> on an AZERTY (e.g., French) keyboard.
+ A,
+ /// <kbd>b</kbd> on a US keyboard.
+ B,
+ /// <kbd>c</kbd> on a US keyboard.
+ C,
+ /// <kbd>d</kbd> on a US keyboard.
+ D,
+ /// <kbd>e</kbd> on a US keyboard.
+ E,
+ /// <kbd>f</kbd> on a US keyboard.
+ F,
+ /// <kbd>g</kbd> on a US keyboard.
+ G,
+ /// <kbd>h</kbd> on a US keyboard.
+ H,
+ /// <kbd>i</kbd> on a US keyboard.
+ I,
+ /// <kbd>j</kbd> on a US keyboard.
+ J,
+ /// <kbd>k</kbd> on a US keyboard.
+ K,
+ /// <kbd>l</kbd> on a US keyboard.
+ L,
+ /// <kbd>m</kbd> on a US keyboard.
+ M,
+ /// <kbd>n</kbd> on a US keyboard.
+ N,
+ /// <kbd>o</kbd> on a US keyboard.
+ O,
+ /// <kbd>p</kbd> on a US keyboard.
+ P,
+ /// <kbd>q</kbd> on a US keyboard.
+ /// Labeled <kbd>a</kbd> on an AZERTY (e.g., French) keyboard.
+ Q,
+ /// <kbd>r</kbd> on a US keyboard.
+ R,
+ /// <kbd>s</kbd> on a US keyboard.
+ S,
+ /// <kbd>t</kbd> on a US keyboard.
+ T,
+ /// <kbd>u</kbd> on a US keyboard.
+ U,
+ /// <kbd>v</kbd> on a US keyboard.
+ V,
+ /// <kbd>w</kbd> on a US keyboard.
+ /// Labeled <kbd>z</kbd> on an AZERTY (e.g., French) keyboard.
+ W,
+ /// <kbd>x</kbd> on a US keyboard.
+ X,
+ /// <kbd>y</kbd> on a US keyboard.
+ /// Labeled <kbd>z</kbd> on a QWERTZ (e.g., German) keyboard.
+ Y,
+ /// <kbd>z</kbd> on a US keyboard.
+ /// Labeled <kbd>w</kbd> on an AZERTY (e.g., French) keyboard, and <kbd>y</kbd> on a
+ /// QWERTZ (e.g., German) keyboard.
+ Z,
+ /// <kbd>-</kbd> on a US keyboard.
+ Minus,
+ /// <kbd>.</kbd> on a US keyboard.
+ Period,
+ /// <kbd>'</kbd> on a US keyboard.
+ Quote,
+ /// <kbd>;</kbd> on a US keyboard.
+ Semicolon,
+ /// <kbd>/</kbd> on a US keyboard.
+ Slash,
+ /// <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>.
+ AltLeft,
+ /// <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>.
+ /// This is labeled <kbd>AltGr</kbd> on many keyboard layouts.
+ AltRight,
+ /// <kbd>Backspace</kbd> or <kbd>⌫</kbd>.
+ /// Labeled <kbd>Delete</kbd> on Apple keyboards.
+ Backspace,
+ /// <kbd>CapsLock</kbd> or <kbd>⇪</kbd>
+ CapsLock,
+ /// The application context menu key, which is typically found between the right
+ /// <kbd>Super</kbd> key and the right <kbd>Control</kbd> key.
+ ContextMenu,
+ /// <kbd>Control</kbd> or <kbd>⌃</kbd>
+ ControlLeft,
+ /// <kbd>Control</kbd> or <kbd>⌃</kbd>
+ ControlRight,
+ /// <kbd>Enter</kbd> or <kbd>↵</kbd>. Labeled <kbd>Return</kbd> on Apple keyboards.
+ Enter,
+ /// The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key.
+ SuperLeft,
+ /// The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key.
+ SuperRight,
+ /// <kbd>Shift</kbd> or <kbd>⇧</kbd>
+ ShiftLeft,
+ /// <kbd>Shift</kbd> or <kbd>⇧</kbd>
+ ShiftRight,
+ /// <kbd> </kbd> (space)
+ Space,
+ /// <kbd>Tab</kbd> or <kbd>⇥</kbd>
+ Tab,
+ /// Japanese: <kbd>変</kbd> (henkan)
+ Convert,
+ /// Japanese: <kbd>カタカナ</kbd>/<kbd>ひらがな</kbd>/<kbd>ローマ字</kbd>
+ /// (katakana/hiragana/romaji)
+ KanaMode,
+ /// Korean: HangulMode <kbd>한/영</kbd> (han/yeong)
+ ///
+ /// Japanese (Mac keyboard): <kbd>か</kbd> (kana)
+ Lang1,
+ /// Korean: Hanja <kbd>한</kbd> (hanja)
+ ///
+ /// Japanese (Mac keyboard): <kbd>英</kbd> (eisu)
+ Lang2,
+ /// Japanese (word-processing keyboard): Katakana
+ Lang3,
+ /// Japanese (word-processing keyboard): Hiragana
+ Lang4,
+ /// Japanese (word-processing keyboard): Zenkaku/Hankaku
+ Lang5,
+ /// Japanese: <kbd>無変換</kbd> (muhenkan)
+ NonConvert,
+ /// <kbd>⌦</kbd>. The forward delete key.
+ /// Note that on Apple keyboards, the key labelled <kbd>Delete</kbd> on the main part
+ /// of the keyboard is encoded as [`Backspace`].
+ ///
+ /// [`Backspace`]: Self::Backspace
+ Delete,
+ /// <kbd>Page Down</kbd>, <kbd>End</kbd>, or <kbd>↘</kbd>
+ End,
+ /// <kbd>Help</kbd>. Not present on standard PC keyboards.
+ Help,
+ /// <kbd>Home</kbd> or <kbd>↖</kbd>
+ Home,
+ /// <kbd>Insert</kbd> or <kbd>Ins</kbd>. Not present on Apple keyboards.
+ Insert,
+ /// <kbd>Page Down</kbd>, <kbd>PgDn</kbd>, or <kbd>⇟</kbd>
+ PageDown,
+ /// <kbd>Page Up</kbd>, <kbd>PgUp</kbd>, or <kbd>⇞</kbd>
+ PageUp,
+ /// <kbd>↓</kbd>
+ ArrowDown,
+ /// <kbd>←</kbd>
+ ArrowLeft,
+ /// <kbd>→</kbd>
+ ArrowRight,
+ /// <kbd>↑</kbd>
+ ArrowUp,
+ /// On the Mac, this is used for the numpad <kbd>Clear</kbd> key.
+ NumLock,
+ /// <kbd>0 Ins</kbd> on a keyboard. <kbd>0</kbd> on a phone or remote control
+ Numpad0,
+ /// <kbd>1 End</kbd> on a keyboard. <kbd>1</kbd> or <kbd>1 QZ</kbd> on a phone or
+ /// remote control
+ Numpad1,
+ /// <kbd>2 ↓</kbd> on a keyboard. <kbd>2 ABC</kbd> on a phone or remote control
+ Numpad2,
+ /// <kbd>3 PgDn</kbd> on a keyboard. <kbd>3 DEF</kbd> on a phone or remote control
+ Numpad3,
+ /// <kbd>4 ←</kbd> on a keyboard. <kbd>4 GHI</kbd> on a phone or remote control
+ Numpad4,
+ /// <kbd>5</kbd> on a keyboard. <kbd>5 JKL</kbd> on a phone or remote control
+ Numpad5,
+ /// <kbd>6 →</kbd> on a keyboard. <kbd>6 MNO</kbd> on a phone or remote control
+ Numpad6,
+ /// <kbd>7 Home</kbd> on a keyboard. <kbd>7 PQRS</kbd> or <kbd>7 PRS</kbd> on a phone
+ /// or remote control
+ Numpad7,
+ /// <kbd>8 ↑</kbd> on a keyboard. <kbd>8 TUV</kbd> on a phone or remote control
+ Numpad8,
+ /// <kbd>9 PgUp</kbd> on a keyboard. <kbd>9 WXYZ</kbd> or <kbd>9 WXY</kbd> on a phone
+ /// or remote control
+ Numpad9,
+ /// <kbd>+</kbd>
+ NumpadAdd,
+ /// Found on the Microsoft Natural Keyboard.
+ NumpadBackspace,
+ /// <kbd>C</kbd> or <kbd>A</kbd> (All Clear). Also for use with numpads that have a
+ /// <kbd>Clear</kbd> key that is separate from the <kbd>NumLock</kbd> key. On the
+ /// Mac, the numpad <kbd>Clear</kbd> key is encoded as [`NumLock`].
+ ///
+ /// [`NumLock`]: Self::NumLock
+ NumpadClear,
+ /// <kbd>C</kbd> (Clear Entry)
+ NumpadClearEntry,
+ /// <kbd>,</kbd> (thousands separator). For locales where the thousands separator
+ /// is a "." (e.g., Brazil), this key may generate a <kbd>.</kbd>.
+ NumpadComma,
+ /// <kbd>. Del</kbd>. For locales where the decimal separator is "," (e.g.,
+ /// Brazil), this key may generate a <kbd>,</kbd>.
+ NumpadDecimal,
+ /// <kbd>/</kbd>
+ NumpadDivide,
+ NumpadEnter,
+ /// <kbd>=</kbd>
+ NumpadEqual,
+ /// <kbd>#</kbd> on a phone or remote control device. This key is typically found
+ /// below the <kbd>9</kbd> key and to the right of the <kbd>0</kbd> key.
+ NumpadHash,
+ /// <kbd>M</kbd> Add current entry to the value stored in memory.
+ NumpadMemoryAdd,
+ /// <kbd>M</kbd> Clear the value stored in memory.
+ NumpadMemoryClear,
+ /// <kbd>M</kbd> Replace the current entry with the value stored in memory.
+ NumpadMemoryRecall,
+ /// <kbd>M</kbd> Replace the value stored in memory with the current entry.
+ NumpadMemoryStore,
+ /// <kbd>M</kbd> Subtract current entry from the value stored in memory.
+ NumpadMemorySubtract,
+ /// <kbd>*</kbd> on a keyboard. For use with numpads that provide mathematical
+ /// operations (<kbd>+</kbd>, <kbd>-</kbd> <kbd>*</kbd> and <kbd>/</kbd>).
+ ///
+ /// Use `NumpadStar` for the <kbd>*</kbd> key on phones and remote controls.
+ NumpadMultiply,
+ /// <kbd>(</kbd> Found on the Microsoft Natural Keyboard.
+ NumpadParenLeft,
+ /// <kbd>)</kbd> Found on the Microsoft Natural Keyboard.
+ NumpadParenRight,
+ /// <kbd>*</kbd> on a phone or remote control device.
+ ///
+ /// This key is typically found below the <kbd>7</kbd> key and to the left of
+ /// the <kbd>0</kbd> key.
+ ///
+ /// Use <kbd>"NumpadMultiply"</kbd> for the <kbd>*</kbd> key on
+ /// numeric keypads.
+ NumpadStar,
+ /// <kbd>-</kbd>
+ NumpadSubtract,
+ /// <kbd>Esc</kbd> or <kbd>⎋</kbd>
+ Escape,
+ /// <kbd>Fn</kbd> This is typically a hardware key that does not generate a separate
+ /// code.
+ Fn,
+ /// <kbd>FLock</kbd> or <kbd>FnLock</kbd>. Function Lock key. Found on the Microsoft
+ /// Natural Keyboard.
+ FnLock,
+ /// <kbd>PrtScr SysRq</kbd> or <kbd>Print Screen</kbd>
+ PrintScreen,
+ /// <kbd>Scroll Lock</kbd>
+ ScrollLock,
+ /// <kbd>Pause Break</kbd>
+ Pause,
+ /// Some laptops place this key to the left of the <kbd>↑</kbd> key.
+ ///
+ /// This also the "back" button (triangle) on Android.
+ BrowserBack,
+ BrowserFavorites,
+ /// Some laptops place this key to the right of the <kbd>↑</kbd> key.
+ BrowserForward,
+ /// The "home" button on Android.
+ BrowserHome,
+ BrowserRefresh,
+ BrowserSearch,
+ BrowserStop,
+ /// <kbd>Eject</kbd> or <kbd>⏏</kbd>. This key is placed in the function section on
+ /// some Apple keyboards.
+ Eject,
+ /// Sometimes labelled <kbd>My Computer</kbd> on the keyboard
+ LaunchApp1,
+ /// Sometimes labelled <kbd>Calculator</kbd> on the keyboard
+ LaunchApp2,
+ LaunchMail,
+ MediaPlayPause,
+ MediaSelect,
+ MediaStop,
+ MediaTrackNext,
+ MediaTrackPrevious,
+ /// This key is placed in the function section on some Apple keyboards, replacing the
+ /// <kbd>Eject</kbd> key.
+ Power,
+ Sleep,
+ AudioVolumeDown,
+ AudioVolumeMute,
+ AudioVolumeUp,
+ WakeUp,
+ // Legacy modifier key. Also called "Super" in certain places.
+ Meta,
+ // Legacy modifier key.
+ Hyper,
+ Turbo,
+ Abort,
+ Resume,
+ Suspend,
+ /// Found on Sun’s USB keyboard.
+ Again,
+ /// Found on Sun’s USB keyboard.
+ Copy,
+ /// Found on Sun’s USB keyboard.
+ Cut,
+ /// Found on Sun’s USB keyboard.
+ Find,
+ /// Found on Sun’s USB keyboard.
+ Open,
+ /// Found on Sun’s USB keyboard.
+ Paste,
+ /// Found on Sun’s USB keyboard.
+ Props,
+ /// Found on Sun’s USB keyboard.
+ Select,
+ /// Found on Sun’s USB keyboard.
+ Undo,
+ /// Use for dedicated <kbd>ひらがな</kbd> key found on some Japanese word processing
+ /// keyboards.
+ Hiragana,
+ /// Use for dedicated <kbd>カタカナ</kbd> key found on some Japanese word processing
+ /// keyboards.
+ Katakana,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F1,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F2,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F3,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F4,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F5,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F6,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F7,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F8,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F9,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F10,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F11,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F12,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F13,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F14,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F15,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F16,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F17,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F18,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F19,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F20,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F21,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F22,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F23,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F24,
+ /// General-purpose function key.
+ F25,
+ /// General-purpose function key.
+ F26,
+ /// General-purpose function key.
+ F27,
+ /// General-purpose function key.
+ F28,
+ /// General-purpose function key.
+ F29,
+ /// General-purpose function key.
+ F30,
+ /// General-purpose function key.
+ F31,
+ /// General-purpose function key.
+ F32,
+ /// General-purpose function key.
+ F33,
+ /// General-purpose function key.
+ F34,
+ /// General-purpose function key.
+ F35,
+}
+
+impl TryFrom<winit::keyboard::KeyCode> for Key
+{
+ type Error = UnknownKeyCodeError;
+
+ fn try_from(key_code: winit::keyboard::KeyCode) -> Result<Self, Self::Error>
+ {
+ match key_code {
+ winit::keyboard::KeyCode::Backquote => Ok(Self::Backquote),
+ winit::keyboard::KeyCode::Backslash => Ok(Self::Backslash),
+ winit::keyboard::KeyCode::BracketLeft => Ok(Self::BracketLeft),
+ winit::keyboard::KeyCode::BracketRight => Ok(Self::BracketRight),
+ winit::keyboard::KeyCode::Comma => Ok(Self::Comma),
+ winit::keyboard::KeyCode::Digit0 => Ok(Self::Digit0),
+ winit::keyboard::KeyCode::Digit1 => Ok(Self::Digit1),
+ winit::keyboard::KeyCode::Digit2 => Ok(Self::Digit2),
+ winit::keyboard::KeyCode::Digit3 => Ok(Self::Digit3),
+ winit::keyboard::KeyCode::Digit4 => Ok(Self::Digit4),
+ winit::keyboard::KeyCode::Digit5 => Ok(Self::Digit5),
+ winit::keyboard::KeyCode::Digit6 => Ok(Self::Digit6),
+ winit::keyboard::KeyCode::Digit7 => Ok(Self::Digit7),
+ winit::keyboard::KeyCode::Digit8 => Ok(Self::Digit8),
+ winit::keyboard::KeyCode::Digit9 => Ok(Self::Digit9),
+ winit::keyboard::KeyCode::Equal => Ok(Self::Equal),
+ winit::keyboard::KeyCode::IntlBackslash => Ok(Self::IntlBackslash),
+ winit::keyboard::KeyCode::IntlRo => Ok(Self::IntlRo),
+ winit::keyboard::KeyCode::IntlYen => Ok(Self::IntlYen),
+ winit::keyboard::KeyCode::KeyA => Ok(Self::A),
+ winit::keyboard::KeyCode::KeyB => Ok(Self::B),
+ winit::keyboard::KeyCode::KeyC => Ok(Self::C),
+ winit::keyboard::KeyCode::KeyD => Ok(Self::D),
+ winit::keyboard::KeyCode::KeyE => Ok(Self::E),
+ winit::keyboard::KeyCode::KeyF => Ok(Self::F),
+ winit::keyboard::KeyCode::KeyG => Ok(Self::G),
+ winit::keyboard::KeyCode::KeyH => Ok(Self::H),
+ winit::keyboard::KeyCode::KeyI => Ok(Self::I),
+ winit::keyboard::KeyCode::KeyJ => Ok(Self::J),
+ winit::keyboard::KeyCode::KeyK => Ok(Self::K),
+ winit::keyboard::KeyCode::KeyL => Ok(Self::L),
+ winit::keyboard::KeyCode::KeyM => Ok(Self::M),
+ winit::keyboard::KeyCode::KeyN => Ok(Self::N),
+ winit::keyboard::KeyCode::KeyO => Ok(Self::O),
+ winit::keyboard::KeyCode::KeyP => Ok(Self::P),
+ winit::keyboard::KeyCode::KeyQ => Ok(Self::Q),
+ winit::keyboard::KeyCode::KeyR => Ok(Self::R),
+ winit::keyboard::KeyCode::KeyS => Ok(Self::S),
+ winit::keyboard::KeyCode::KeyT => Ok(Self::T),
+ winit::keyboard::KeyCode::KeyU => Ok(Self::U),
+ winit::keyboard::KeyCode::KeyV => Ok(Self::V),
+ winit::keyboard::KeyCode::KeyW => Ok(Self::W),
+ winit::keyboard::KeyCode::KeyX => Ok(Self::X),
+ winit::keyboard::KeyCode::KeyY => Ok(Self::Y),
+ winit::keyboard::KeyCode::KeyZ => Ok(Self::Z),
+ winit::keyboard::KeyCode::Minus => Ok(Self::Minus),
+ winit::keyboard::KeyCode::Period => Ok(Self::Period),
+ winit::keyboard::KeyCode::Quote => Ok(Self::Quote),
+ winit::keyboard::KeyCode::Semicolon => Ok(Self::Semicolon),
+ winit::keyboard::KeyCode::Slash => Ok(Self::Slash),
+ winit::keyboard::KeyCode::AltLeft => Ok(Self::AltLeft),
+ winit::keyboard::KeyCode::AltRight => Ok(Self::AltRight),
+ winit::keyboard::KeyCode::Backspace => Ok(Self::Backspace),
+ winit::keyboard::KeyCode::CapsLock => Ok(Self::CapsLock),
+ winit::keyboard::KeyCode::ContextMenu => Ok(Self::ContextMenu),
+ winit::keyboard::KeyCode::ControlLeft => Ok(Self::ControlLeft),
+ winit::keyboard::KeyCode::ControlRight => Ok(Self::ControlRight),
+ winit::keyboard::KeyCode::Enter => Ok(Self::Enter),
+ winit::keyboard::KeyCode::SuperLeft => Ok(Self::SuperLeft),
+ winit::keyboard::KeyCode::SuperRight => Ok(Self::SuperRight),
+ winit::keyboard::KeyCode::ShiftLeft => Ok(Self::ShiftLeft),
+ winit::keyboard::KeyCode::ShiftRight => Ok(Self::ShiftRight),
+ winit::keyboard::KeyCode::Space => Ok(Self::Space),
+ winit::keyboard::KeyCode::Tab => Ok(Self::Tab),
+ winit::keyboard::KeyCode::Convert => Ok(Self::Convert),
+ winit::keyboard::KeyCode::KanaMode => Ok(Self::KanaMode),
+ winit::keyboard::KeyCode::Lang1 => Ok(Self::Lang1),
+ winit::keyboard::KeyCode::Lang2 => Ok(Self::Lang2),
+ winit::keyboard::KeyCode::Lang3 => Ok(Self::Lang3),
+ winit::keyboard::KeyCode::Lang4 => Ok(Self::Lang4),
+ winit::keyboard::KeyCode::Lang5 => Ok(Self::Lang5),
+ winit::keyboard::KeyCode::NonConvert => Ok(Self::NonConvert),
+ winit::keyboard::KeyCode::Delete => Ok(Self::Delete),
+ winit::keyboard::KeyCode::End => Ok(Self::End),
+ winit::keyboard::KeyCode::Help => Ok(Self::Help),
+ winit::keyboard::KeyCode::Home => Ok(Self::Home),
+ winit::keyboard::KeyCode::Insert => Ok(Self::Insert),
+ winit::keyboard::KeyCode::PageDown => Ok(Self::PageDown),
+ winit::keyboard::KeyCode::PageUp => Ok(Self::PageUp),
+ winit::keyboard::KeyCode::ArrowDown => Ok(Self::ArrowDown),
+ winit::keyboard::KeyCode::ArrowLeft => Ok(Self::ArrowLeft),
+ winit::keyboard::KeyCode::ArrowRight => Ok(Self::ArrowRight),
+ winit::keyboard::KeyCode::ArrowUp => Ok(Self::ArrowUp),
+ winit::keyboard::KeyCode::NumLock => Ok(Self::NumLock),
+ winit::keyboard::KeyCode::Numpad0 => Ok(Self::Numpad0),
+ winit::keyboard::KeyCode::Numpad1 => Ok(Self::Numpad1),
+ winit::keyboard::KeyCode::Numpad2 => Ok(Self::Numpad2),
+ winit::keyboard::KeyCode::Numpad3 => Ok(Self::Numpad3),
+ winit::keyboard::KeyCode::Numpad4 => Ok(Self::Numpad4),
+ winit::keyboard::KeyCode::Numpad5 => Ok(Self::Numpad5),
+ winit::keyboard::KeyCode::Numpad6 => Ok(Self::Numpad6),
+ winit::keyboard::KeyCode::Numpad7 => Ok(Self::Numpad7),
+ winit::keyboard::KeyCode::Numpad8 => Ok(Self::Numpad8),
+ winit::keyboard::KeyCode::Numpad9 => Ok(Self::Numpad9),
+ winit::keyboard::KeyCode::NumpadAdd => Ok(Self::NumpadAdd),
+ winit::keyboard::KeyCode::NumpadBackspace => Ok(Self::NumpadBackspace),
+ winit::keyboard::KeyCode::NumpadClear => Ok(Self::NumpadClear),
+ winit::keyboard::KeyCode::NumpadClearEntry => Ok(Self::NumpadClearEntry),
+ winit::keyboard::KeyCode::NumpadComma => Ok(Self::NumpadComma),
+ winit::keyboard::KeyCode::NumpadDecimal => Ok(Self::NumpadDecimal),
+ winit::keyboard::KeyCode::NumpadDivide => Ok(Self::NumpadDivide),
+ winit::keyboard::KeyCode::NumpadEnter => Ok(Self::NumpadEnter),
+ winit::keyboard::KeyCode::NumpadEqual => Ok(Self::NumpadEqual),
+ winit::keyboard::KeyCode::NumpadHash => Ok(Self::NumpadHash),
+ winit::keyboard::KeyCode::NumpadMemoryAdd => Ok(Self::NumpadMemoryAdd),
+ winit::keyboard::KeyCode::NumpadMemoryClear => Ok(Self::NumpadMemoryClear),
+ winit::keyboard::KeyCode::NumpadMemoryRecall => Ok(Self::NumpadMemoryRecall),
+ winit::keyboard::KeyCode::NumpadMemoryStore => Ok(Self::NumpadMemoryStore),
+ winit::keyboard::KeyCode::NumpadMemorySubtract => {
+ Ok(Self::NumpadMemorySubtract)
+ }
+ winit::keyboard::KeyCode::NumpadMultiply => Ok(Self::NumpadMultiply),
+ winit::keyboard::KeyCode::NumpadParenLeft => Ok(Self::NumpadParenLeft),
+ winit::keyboard::KeyCode::NumpadParenRight => Ok(Self::NumpadParenRight),
+ winit::keyboard::KeyCode::NumpadStar => Ok(Self::NumpadStar),
+ winit::keyboard::KeyCode::NumpadSubtract => Ok(Self::NumpadSubtract),
+ winit::keyboard::KeyCode::Escape => Ok(Self::Escape),
+ winit::keyboard::KeyCode::Fn => Ok(Self::Fn),
+ winit::keyboard::KeyCode::FnLock => Ok(Self::FnLock),
+ winit::keyboard::KeyCode::PrintScreen => Ok(Self::PrintScreen),
+ winit::keyboard::KeyCode::ScrollLock => Ok(Self::ScrollLock),
+ winit::keyboard::KeyCode::Pause => Ok(Self::Pause),
+ winit::keyboard::KeyCode::BrowserBack => Ok(Self::BrowserBack),
+ winit::keyboard::KeyCode::BrowserFavorites => Ok(Self::BrowserFavorites),
+ winit::keyboard::KeyCode::BrowserForward => Ok(Self::BrowserForward),
+ winit::keyboard::KeyCode::BrowserHome => Ok(Self::BrowserHome),
+ winit::keyboard::KeyCode::BrowserRefresh => Ok(Self::BrowserRefresh),
+ winit::keyboard::KeyCode::BrowserSearch => Ok(Self::BrowserSearch),
+ winit::keyboard::KeyCode::BrowserStop => Ok(Self::BrowserStop),
+ winit::keyboard::KeyCode::Eject => Ok(Self::Eject),
+ winit::keyboard::KeyCode::LaunchApp1 => Ok(Self::LaunchApp1),
+ winit::keyboard::KeyCode::LaunchApp2 => Ok(Self::LaunchApp2),
+ winit::keyboard::KeyCode::LaunchMail => Ok(Self::LaunchMail),
+ winit::keyboard::KeyCode::MediaPlayPause => Ok(Self::MediaPlayPause),
+ winit::keyboard::KeyCode::MediaSelect => Ok(Self::MediaSelect),
+ winit::keyboard::KeyCode::MediaStop => Ok(Self::MediaStop),
+ winit::keyboard::KeyCode::MediaTrackNext => Ok(Self::MediaTrackNext),
+ winit::keyboard::KeyCode::MediaTrackPrevious => Ok(Self::MediaTrackPrevious),
+ winit::keyboard::KeyCode::Power => Ok(Self::Power),
+ winit::keyboard::KeyCode::Sleep => Ok(Self::Sleep),
+ winit::keyboard::KeyCode::AudioVolumeDown => Ok(Self::AudioVolumeDown),
+ winit::keyboard::KeyCode::AudioVolumeMute => Ok(Self::AudioVolumeMute),
+ winit::keyboard::KeyCode::AudioVolumeUp => Ok(Self::AudioVolumeUp),
+ winit::keyboard::KeyCode::WakeUp => Ok(Self::WakeUp),
+ winit::keyboard::KeyCode::Meta => Ok(Self::Meta),
+ winit::keyboard::KeyCode::Hyper => Ok(Self::Hyper),
+ winit::keyboard::KeyCode::Turbo => Ok(Self::Turbo),
+ winit::keyboard::KeyCode::Abort => Ok(Self::Abort),
+ winit::keyboard::KeyCode::Resume => Ok(Self::Resume),
+ winit::keyboard::KeyCode::Suspend => Ok(Self::Suspend),
+ winit::keyboard::KeyCode::Again => Ok(Self::Again),
+ winit::keyboard::KeyCode::Copy => Ok(Self::Copy),
+ winit::keyboard::KeyCode::Cut => Ok(Self::Cut),
+ winit::keyboard::KeyCode::Find => Ok(Self::Find),
+ winit::keyboard::KeyCode::Open => Ok(Self::Open),
+ winit::keyboard::KeyCode::Paste => Ok(Self::Paste),
+ winit::keyboard::KeyCode::Props => Ok(Self::Props),
+ winit::keyboard::KeyCode::Select => Ok(Self::Select),
+ winit::keyboard::KeyCode::Undo => Ok(Self::Undo),
+ winit::keyboard::KeyCode::Hiragana => Ok(Self::Hiragana),
+ winit::keyboard::KeyCode::Katakana => Ok(Self::Katakana),
+ winit::keyboard::KeyCode::F1 => Ok(Self::F1),
+ winit::keyboard::KeyCode::F2 => Ok(Self::F2),
+ winit::keyboard::KeyCode::F3 => Ok(Self::F3),
+ winit::keyboard::KeyCode::F4 => Ok(Self::F4),
+ winit::keyboard::KeyCode::F5 => Ok(Self::F5),
+ winit::keyboard::KeyCode::F6 => Ok(Self::F6),
+ winit::keyboard::KeyCode::F7 => Ok(Self::F7),
+ winit::keyboard::KeyCode::F8 => Ok(Self::F8),
+ winit::keyboard::KeyCode::F9 => Ok(Self::F9),
+ winit::keyboard::KeyCode::F10 => Ok(Self::F10),
+ winit::keyboard::KeyCode::F11 => Ok(Self::F11),
+ winit::keyboard::KeyCode::F12 => Ok(Self::F12),
+ winit::keyboard::KeyCode::F13 => Ok(Self::F13),
+ winit::keyboard::KeyCode::F14 => Ok(Self::F14),
+ winit::keyboard::KeyCode::F15 => Ok(Self::F15),
+ winit::keyboard::KeyCode::F16 => Ok(Self::F16),
+ winit::keyboard::KeyCode::F17 => Ok(Self::F17),
+ winit::keyboard::KeyCode::F18 => Ok(Self::F18),
+ winit::keyboard::KeyCode::F19 => Ok(Self::F19),
+ winit::keyboard::KeyCode::F20 => Ok(Self::F20),
+ winit::keyboard::KeyCode::F21 => Ok(Self::F21),
+ winit::keyboard::KeyCode::F22 => Ok(Self::F22),
+ winit::keyboard::KeyCode::F23 => Ok(Self::F23),
+ winit::keyboard::KeyCode::F24 => Ok(Self::F24),
+ winit::keyboard::KeyCode::F25 => Ok(Self::F25),
+ winit::keyboard::KeyCode::F26 => Ok(Self::F26),
+ winit::keyboard::KeyCode::F27 => Ok(Self::F27),
+ winit::keyboard::KeyCode::F28 => Ok(Self::F28),
+ winit::keyboard::KeyCode::F29 => Ok(Self::F29),
+ winit::keyboard::KeyCode::F30 => Ok(Self::F30),
+ winit::keyboard::KeyCode::F31 => Ok(Self::F31),
+ winit::keyboard::KeyCode::F32 => Ok(Self::F32),
+ winit::keyboard::KeyCode::F33 => Ok(Self::F33),
+ winit::keyboard::KeyCode::F34 => Ok(Self::F34),
+ winit::keyboard::KeyCode::F35 => Ok(Self::F35),
+ _ => Err(UnknownKeyCodeError),
+ }
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error("Unknown key code")]
+pub struct UnknownKeyCodeError;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum KeyState
+{
+ Pressed,
+ Released,
+}
+
+impl KeyState
+{
+ #[must_use]
+ #[inline]
+ pub fn is_pressed(&self) -> bool
+ {
+ matches!(self, Self::Pressed)
+ }
+
+ #[must_use]
+ #[inline]
+ pub fn is_released(&self) -> bool
+ {
+ matches!(self, Self::Released)
+ }
+}
+
+impl From<winit::event::ElementState> for KeyState
+{
+ fn from(element_state: winit::event::ElementState) -> Self
+ {
+ match element_state {
+ winit::event::ElementState::Pressed => Self::Pressed,
+ winit::event::ElementState::Released => Self::Released,
+ }
+ }
+}
+
+const BITS_PER_KEY: usize = 2;
+
+const KEY_CURR_PRESSED_BITS: u8 = 0b10;
+const KEY_PREV_PRESSED_BITS: u8 = 0b01;
diff --git a/engine/src/windowing/monitor.rs b/engine/src/windowing/monitor.rs
new file mode 100644
index 0000000..894448a
--- /dev/null
+++ b/engine/src/windowing/monitor.rs
@@ -0,0 +1,50 @@
+use crate::windowing::dpi::{PhysicalPosition, PhysicalSize};
+
+/// Handle to a monitor that may or may not exist any longer.
+#[derive(Debug, Clone)]
+pub struct Handle
+{
+ inner: winit::monitor::MonitorHandle,
+}
+
+impl Handle
+{
+ /// Returns a human-readable name of the monitor.
+ #[inline]
+ pub fn name(&self) -> Option<String>
+ {
+ self.inner.name()
+ }
+
+ /// Returns the monitor's resolution.
+ #[inline]
+ pub fn size(&self) -> PhysicalSize<u32>
+ {
+ self.inner.size().into()
+ }
+
+ /// Returns the top-left corner position of the monitor relative to the larger full
+ /// screen area.
+ #[inline]
+ pub fn position(&self) -> PhysicalPosition<i32>
+ {
+ self.inner.position().into()
+ }
+
+ /// Returns the scale factor of the underlying monitor.
+ #[inline]
+ pub fn scale_factor(&self) -> f64
+ {
+ self.inner.scale_factor()
+ }
+}
+
+impl Handle
+{
+ pub(super) fn from_winit_monitor_handle(
+ monitor: winit::monitor::MonitorHandle,
+ ) -> Self
+ {
+ Self { inner: monitor }
+ }
+}
diff --git a/engine/src/windowing/mouse.rs b/engine/src/windowing/mouse.rs
new file mode 100644
index 0000000..3a43e79
--- /dev/null
+++ b/engine/src/windowing/mouse.rs
@@ -0,0 +1,197 @@
+use std::collections::HashMap;
+
+use crate::ecs::Sole;
+use crate::reflection::Reflection;
+use crate::vector::Vec2;
+use crate::windowing::dpi::PhysicalPosition;
+
+#[derive(Debug, Default, Clone, Sole, Reflection)]
+#[non_exhaustive]
+pub struct Mouse
+{
+ /// Change in coordinates this tick. Unit is unspecified and platforms may use
+ /// different units. Updated automatically by the [`windowing extension`].
+ ///
+ /// [`windowing extension`]: crate::windowing
+ pub curr_tick_position_delta: Vec2<f64>,
+
+ /// Coordinates in pixels relative to the top-left corner of the window. May have
+ /// been affected by cursor acceleration
+ pub position: PhysicalPosition<f64>,
+
+ pub curr_tick_scroll_delta: ScrollDelta,
+}
+
+#[derive(Debug, Clone, Default, Reflection)]
+pub struct ScrollDelta
+{
+ pub vert_lines: f32,
+ pub hor_lines: f32,
+}
+
+impl ScrollDelta
+{
+ pub fn is_zero(&self) -> bool
+ {
+ self.vert_lines == 0.0 && self.hor_lines == 0.0
+ }
+}
+
+/// Mouse buttons.
+#[derive(Debug, Default, Sole)]
+pub struct Buttons
+{
+ map: HashMap<Button, ButtonData>,
+}
+
+impl Buttons
+{
+ pub fn get(&self, button: Button) -> ButtonState
+ {
+ let Some(button_data) = self.map.get(&button) else {
+ return ButtonState::Released;
+ };
+
+ button_data.current_state
+ }
+
+ pub fn get_previous(&self, button: Button) -> ButtonState
+ {
+ let Some(button_data) = self.map.get(&button) else {
+ return ButtonState::Released;
+ };
+
+ button_data.previous_state
+ }
+
+ /// Returns a iterator that yields buttons and their current states. Only buttons with
+ /// states is included.
+ pub fn all_current(&self) -> impl Iterator<Item = (Button, ButtonState)> + use<'_>
+ {
+ self.map
+ .iter()
+ .map(|(button, button_data)| (button.clone(), button_data.current_state))
+ }
+
+ pub fn set(&mut self, button: Button, button_state: ButtonState)
+ {
+ let button_data = self.map.entry(button).or_default();
+
+ button_data.current_state = button_state;
+ }
+
+ pub fn set_previous(&mut self, button: Button, button_state: ButtonState)
+ {
+ let button_data = self.map.entry(button).or_default();
+
+ button_data.previous_state = button_state;
+ }
+
+ pub fn set_previous_to_current(&mut self, button: Button)
+ {
+ let button_data = self.map.entry(button).or_default();
+
+ button_data.previous_state = button_data.current_state;
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum Button
+{
+ Left,
+ Right,
+ Middle,
+ Back,
+ Forward,
+ Other(u16),
+}
+
+impl intmap::IntKey for Button
+{
+ type Int = u32;
+
+ const PRIME: Self::Int = <u32 as intmap::IntKey>::PRIME;
+
+ fn into_int(self) -> Self::Int
+ {
+ match &self {
+ Self::Left => 0,
+ Self::Right => 1,
+ Self::Middle => 2,
+ Self::Back => 3,
+ Self::Forward => 4,
+ Self::Other(other) => 5 + *other as u32,
+ }
+ }
+}
+
+impl From<winit::event::MouseButton> for Button
+{
+ fn from(mouse_button: winit::event::MouseButton) -> Self
+ {
+ match mouse_button {
+ winit::event::MouseButton::Left => Self::Left,
+ winit::event::MouseButton::Right => Self::Right,
+ winit::event::MouseButton::Middle => Self::Middle,
+ winit::event::MouseButton::Back => Self::Back,
+ winit::event::MouseButton::Forward => Self::Forward,
+ winit::event::MouseButton::Other(other_mouse_button) => {
+ Self::Other(other_mouse_button)
+ }
+ }
+ }
+}
+
+/// Mouse button state.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum ButtonState
+{
+ Pressed,
+ Released,
+}
+
+impl ButtonState
+{
+ #[must_use]
+ #[inline]
+ pub fn is_pressed(&self) -> bool
+ {
+ matches!(self, Self::Pressed)
+ }
+
+ #[must_use]
+ #[inline]
+ pub fn is_released(&self) -> bool
+ {
+ matches!(self, Self::Released)
+ }
+}
+
+impl From<winit::event::ElementState> for ButtonState
+{
+ fn from(element_state: winit::event::ElementState) -> Self
+ {
+ match element_state {
+ winit::event::ElementState::Pressed => Self::Pressed,
+ winit::event::ElementState::Released => Self::Released,
+ }
+ }
+}
+
+#[derive(Debug)]
+struct ButtonData
+{
+ current_state: ButtonState,
+ previous_state: ButtonState,
+}
+
+impl Default for ButtonData
+{
+ fn default() -> Self
+ {
+ Self {
+ current_state: ButtonState::Released,
+ previous_state: ButtonState::Released,
+ }
+ }
+}
diff --git a/engine/src/windowing/window.rs b/engine/src/windowing/window.rs
new file mode 100644
index 0000000..6d7a464
--- /dev/null
+++ b/engine/src/windowing/window.rs
@@ -0,0 +1,291 @@
+use std::borrow::Cow;
+
+use crate::ecs::Component;
+use crate::image::Image;
+use crate::reflection::Reflection;
+use crate::windowing::dpi::{PhysicalSize, Position, Size};
+
+pub mod platform;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Id
+{
+ inner: winit::window::WindowId,
+}
+
+impl intmap::IntKey for Id
+{
+ type Int = u64;
+
+ const PRIME: Self::Int = <u64 as intmap::IntKey>::PRIME;
+
+ fn into_int(self) -> Self::Int
+ {
+ self.inner.into()
+ }
+}
+
+impl Id
+{
+ pub(crate) fn from_inner(inner: winit::window::WindowId) -> Self
+ {
+ Self { inner }
+ }
+}
+
+#[derive(Debug, Component, Clone, Reflection)]
+#[non_exhaustive]
+pub struct CreationAttributes
+{
+ pub title: Cow<'static, str>,
+ pub transparent: bool,
+ pub maximized: bool,
+ pub fullscreen: Option<Fullscreen>,
+ pub visible: bool,
+ pub resizable: bool,
+ pub position: Option<Position>,
+ pub inner_size: Option<Size>,
+ pub icon: Option<Icon>,
+
+ pub x_visual_id: Option<XVisualID>,
+
+ // These do not have equivalents in winit::window::WindowAttributes
+ pub cursor_visible: bool,
+ pub cursor_grab_mode: CursorGrabMode,
+}
+
+macro_rules! gen_creation_attrs_with_fn {
+ ($field: ident, $field_type: ty) => {
+ paste::paste! {
+ impl CreationAttributes
+ {
+ pub fn [<with_ $field>](mut self, new: impl Into<$field_type>) -> Self
+ {
+ self.$field = new.into();
+ self
+ }
+ }
+ }
+ };
+}
+
+gen_creation_attrs_with_fn!(title, Cow<'static, str>);
+gen_creation_attrs_with_fn!(transparent, bool);
+gen_creation_attrs_with_fn!(maximized, bool);
+gen_creation_attrs_with_fn!(fullscreen, Option<Fullscreen>);
+gen_creation_attrs_with_fn!(visible, bool);
+gen_creation_attrs_with_fn!(resizable, bool);
+gen_creation_attrs_with_fn!(position, Option<Position>);
+gen_creation_attrs_with_fn!(inner_size, Option<Size>);
+gen_creation_attrs_with_fn!(icon, Option<Icon>);
+
+gen_creation_attrs_with_fn!(x_visual_id, Option<XVisualID>);
+
+gen_creation_attrs_with_fn!(cursor_visible, bool);
+gen_creation_attrs_with_fn!(cursor_grab_mode, CursorGrabMode);
+
+impl CreationAttributes
+{
+ #[tracing::instrument(skip_all)]
+ pub(crate) fn into_window_attrs(self) -> winit::window::WindowAttributes
+ {
+ let mut window_attrs = winit::window::WindowAttributes::default()
+ .with_title(self.title.into_owned())
+ .with_transparent(self.transparent)
+ .with_maximized(self.maximized)
+ .with_fullscreen(match self.fullscreen {
+ Some(Fullscreen::Borderless) => {
+ Some(winit::window::Fullscreen::Borderless(None))
+ }
+ None => None,
+ })
+ .with_visible(self.visible)
+ .with_resizable(self.resizable)
+ .with_window_icon(match self.icon {
+ Some(Icon::Image(image)) => {
+ let image_dimens = image.dimensions();
+
+ let result = winit::window::Icon::from_rgba(
+ image.into_rgba8().into_bytes(),
+ image_dimens.width,
+ image_dimens.height,
+ );
+
+ if let Err(err) = &result {
+ tracing::error!("Invalid icon image: {err}");
+ }
+
+ result.ok()
+ }
+ #[cfg(windows)]
+ Some(Icon::WindowsResource(resource_name)) => {
+ use winit::platform::windows::IconExtWindows;
+
+ let result = winit::window::Icon::from_resource_name(
+ resource_name.as_ref(),
+ None,
+ );
+
+ if let Err(err) = &result {
+ tracing::error!(
+ "Invalid icon image in resource {resource_name}: {err}"
+ );
+ }
+
+ result.ok()
+ }
+ None => None,
+ });
+
+ window_attrs.position = self.position.map(Into::into);
+ window_attrs.inner_size = self.inner_size.map(Into::into);
+
+ #[cfg(target_os = "linux")]
+ if let Some(visual_id) = self.x_visual_id {
+ use winit::platform::x11::WindowAttributesExtX11;
+
+ return window_attrs.with_x11_visual(visual_id);
+ }
+
+ window_attrs
+ }
+
+ pub(crate) fn apply_extra_attrs_to_window(&self, window: &winit::window::Window)
+ {
+ window.set_cursor_visible(self.cursor_visible);
+ }
+}
+
+impl Default for CreationAttributes
+{
+ fn default() -> Self
+ {
+ Self {
+ title: "Unnamed window".into(),
+ transparent: false,
+ maximized: false,
+ fullscreen: None,
+ visible: true,
+ resizable: true,
+ position: None,
+ inner_size: None,
+ icon: None,
+ x_visual_id: None,
+ cursor_visible: true,
+ cursor_grab_mode: CursorGrabMode::None,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Reflection)]
+#[non_exhaustive]
+pub enum Fullscreen
+{
+ Borderless,
+}
+
+/// Window icon
+#[derive(Debug, Clone, Reflection)]
+#[non_exhaustive]
+pub enum Icon
+{
+ Image(Image),
+
+ #[cfg(windows)]
+ WindowsResource(Cow<'static, str>),
+}
+
+#[derive(Debug, Default, Component, Clone, Copy, Reflection)]
+pub struct CreationReady;
+
+#[derive(Debug, Component, Reflection)]
+#[non_exhaustive]
+pub struct Window
+{
+ pub title: String,
+ pub cursor_visible: bool,
+ pub cursor_grab_mode: CursorGrabMode,
+ pub inner_size: PhysicalSize<u32>,
+ wid: Id,
+ scale_factor: f64,
+}
+
+impl Window
+{
+ pub fn wid(&self) -> Id
+ {
+ self.wid
+ }
+
+ pub fn scale_factor(&self) -> f64
+ {
+ self.scale_factor
+ }
+
+ pub(crate) fn new(
+ winit_window: &winit::window::Window,
+ creation_attrs: &CreationAttributes,
+ ) -> Self
+ {
+ Self {
+ title: creation_attrs.title.clone().into_owned(),
+ cursor_visible: creation_attrs.cursor_visible,
+ cursor_grab_mode: creation_attrs.cursor_grab_mode,
+ wid: Id::from_inner(winit_window.id()),
+ inner_size: winit_window.inner_size().into(),
+ scale_factor: winit_window.scale_factor(),
+ }
+ }
+
+ #[must_use]
+ pub(crate) fn apply(&self, winit_window: &winit::window::Window) -> ApplyResults
+ {
+ winit_window.set_title(&self.title);
+ winit_window.set_cursor_visible(self.cursor_visible);
+
+ let curr_inner_size = winit_window.inner_size().clone().into();
+
+ let inner_size_request_result = match winit_window.request_inner_size(
+ winit::dpi::Size::Physical(self.inner_size.clone().into()),
+ ) {
+ // The comparison of curr_inner_size is in case the user's windowing system
+ // lies about using the requested inner size
+ None if curr_inner_size == self.inner_size => Ok(()),
+ None => Err(curr_inner_size),
+ Some(inner_size) => Err(inner_size.into()),
+ };
+
+ ApplyResults {
+ inner_size_request: inner_size_request_result,
+ }
+ }
+
+ pub(crate) fn set_scale_factor(&mut self, scale_factor: f64)
+ {
+ self.scale_factor = scale_factor;
+ }
+}
+
+#[derive(Debug, Component, Reflection)]
+pub struct Closed;
+
+#[derive(
+ Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Reflection,
+)]
+pub enum CursorGrabMode
+{
+ #[default]
+ None,
+
+ /// The cursor is locked to a specific position in the window.
+ Locked,
+}
+
+/// A unique identifier for an X11 visual.
+pub type XVisualID = u32;
+
+#[derive(Debug)]
+pub(crate) struct ApplyResults
+{
+ pub inner_size_request: Result<(), PhysicalSize<u32>>,
+}
diff --git a/engine/src/windowing/window/platform.rs b/engine/src/windowing/window/platform.rs
new file mode 100644
index 0000000..1f38f3b
--- /dev/null
+++ b/engine/src/windowing/window/platform.rs
@@ -0,0 +1,12 @@
+#[cfg(target_os = "linux")]
+pub mod x11
+{
+ use std::ffi::c_void;
+
+ pub type XlibErrorHook = Box<dyn Fn(*mut c_void, *mut c_void) -> bool + Send + Sync>;
+
+ pub fn register_xlib_error_hook(hook: XlibErrorHook)
+ {
+ winit::platform::x11::register_xlib_error_hook(hook);
+ }
+}
diff --git a/engine/src/work_queue.rs b/engine/src/work_queue.rs
new file mode 100644
index 0000000..494d2b5
--- /dev/null
+++ b/engine/src/work_queue.rs
@@ -0,0 +1,77 @@
+use std::borrow::Cow;
+use std::marker::PhantomData;
+use std::panic::catch_unwind;
+use std::sync::mpsc::{channel as mpsc_channel, Sender as MpscSender};
+use std::sync::{Arc, OnceLock};
+use std::thread::{Builder as ThreadBuilder, JoinHandle as ThreadJoinHandle};
+
+pub struct Work<UserData: Send + Sync + 'static>
+{
+ pub func: fn(UserData),
+ pub user_data: UserData,
+}
+
+#[derive(Debug)]
+pub struct WorkQueue<UserData: Send + Sync + 'static>
+{
+ work_sender: MpscSender<Work<UserData>>,
+ thread_panic: Arc<OnceLock<Box<str>>>,
+ _thread: ThreadJoinHandle<()>,
+ _pd: PhantomData<UserData>,
+}
+
+impl<UserData: Send + Sync + 'static> WorkQueue<UserData>
+{
+ pub fn new(name: &str) -> Self
+ {
+ let (work_sender, work_receiver) = mpsc_channel::<Work<UserData>>();
+
+ let thread_panic = Arc::new(OnceLock::new());
+
+ let thread_panic_b = thread_panic.clone();
+
+ Self {
+ work_sender,
+ thread_panic: thread_panic,
+ _thread: ThreadBuilder::new()
+ .name(name.to_string())
+ .spawn(move || {
+ if let Err(panic_err) = catch_unwind(|| {
+ while let Ok(work) = work_receiver.recv() {
+ (work.func)(work.user_data);
+ }
+ }) {
+ let panic_message: Cow<'static, str> =
+ if let Some(panic_message) =
+ panic_err.downcast_ref::<&'static str>()
+ {
+ (*panic_message).into()
+ } else if let Some(panic_message) =
+ panic_err.downcast_ref::<String>()
+ {
+ panic_message.clone().into()
+ } else {
+ "(unknown panic payload type)".into()
+ };
+
+ let _ = thread_panic_b
+ .set(panic_message.into_owned().into_boxed_str());
+ }
+ })
+ .expect("Failed to create work queue thread"),
+ _pd: PhantomData,
+ }
+ }
+
+ pub fn get_thread_panic(&self) -> Option<&str>
+ {
+ self.thread_panic.get().map(|thread_panic| &**thread_panic)
+ }
+
+ pub fn add_work(&self, work: Work<UserData>)
+ {
+ if self.work_sender.send(work).is_err() {
+ tracing::error!("Cannot add work to work queue. Work queue thread is dead");
+ }
+ }
+}