diff options
Diffstat (limited to 'engine')
57 files changed, 7957 insertions, 3907 deletions
diff --git a/engine/Cargo.toml b/engine/Cargo.toml index f6cd5cf..9461ee9 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -1,20 +1,46 @@ [package] name = "engine" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] -glfw = { path = "../glfw", features = ["opengl"] } +glutin = "0.32.3" +raw-window-handle = "0.6.2" thiserror = "1.0.49" -gl = "0.14.0" bitflags = "2.4.0" tracing = "0.1.39" seq-macro = "0.3.5" paste = "1.0.14" +parking_lot = "0.12.3" +crossbeam-channel = "0.5.15" +safer-ffi = "0.1.13" +nu-ansi-term = "0.46.0" ecs = { path = "../ecs" } util-macros = { path = "../util-macros" } +opengl-bindings = { path = "../opengl-bindings" } +engine-macros = { path = "../engine-macros" } -[dependencies.image] +[dependencies.winit] +version = "0.30.11" +default-features = false +features = ["rwh_06", "wayland", "wayland-dlopen", "x11"] + +[dependencies.image_rs] version = "0.24.7" default-features = false features = ["png", "jpeg"] +package = "image" + +[dependencies.zerocopy] +version = "0.8.42" +default-features = false +features = ["derive"] + +[dependencies.shader-slang] +git = "https://github.com/HampusMat/slang-rs" +branch = "with_static_linking" +default-features = false +features = ["static"] + +[build-dependencies] +cfg_aliases = "0.2.1" diff --git a/engine/build.rs b/engine/build.rs new file mode 100644 index 0000000..58029fc --- /dev/null +++ b/engine/build.rs @@ -0,0 +1,63 @@ +// Original file: +// https://github.com/rust-windowing/glutin/blob/ +// 0433af9018febe0696c485ed9d66c40dad41f2d4/glutin-winit/build.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. + +// Keep this in sync with glutin's build.rs. + +use cfg_aliases::cfg_aliases; + +fn main() +{ + // Setup alias to reduce `cfg` boilerplate. + cfg_aliases! { + // Systems. + android_platform: { target_os = "android" }, + wasm_platform: { target_family = "wasm" }, + macos_platform: { target_os = "macos" }, + ios_platform: { target_os = "ios" }, + apple: { any(ios_platform, macos_platform) }, + free_unix: { all(unix, not(apple), not(android_platform)) }, + + // Native displays. + x11_platform: { all(free_unix, not(wasm_platform)) }, + wayland_platform: { all(free_unix, not(wasm_platform)) }, + // x11_platform: { all(feature = "x11", free_unix, not(wasm_platform)) }, + // wayland_platform: { all(feature = "wayland", free_unix, not(wasm_platform)) }, + + // Backends. + egl_backend: { + all(any(windows, unix), not(apple), not(wasm_platform)) + }, + glx_backend: { all(x11_platform, not(wasm_platform)) }, + wgl_backend: { all(windows, not(wasm_platform)) }, + cgl_backend: { all(macos_platform, not(wasm_platform)) }, + + // Backends. + // egl_backend: { + // all(feature = "egl", any(windows, unix), not(apple), not(wasm_platform)) + // }, + // glx_backend: { all(feature = "glx", x11_platform, not(wasm_platform)) }, + // wgl_backend: { all(feature = "wgl", windows, not(wasm_platform)) }, + // cgl_backend: { all(macos_platform, not(wasm_platform)) }, + } +} diff --git a/engine/res/default_shader.slang b/engine/res/default_shader.slang new file mode 100644 index 0000000..dd6a6a9 --- /dev/null +++ b/engine/res/default_shader.slang @@ -0,0 +1,289 @@ +#define MAX_LIGHT_CNT 64 + + +struct Material +{ + float3 ambient; + float3 diffuse; + float3 specular; + // Sampler2D ambient_map; + // Sampler2D diffuse_map; + // Sampler2D specular_map; + float shininess; +}; + +struct LightPhong +{ + float3 diffuse; + float3 specular; +}; + +struct AttenuationProperties +{ + float constant; + float linear; + float quadratic; +}; + +struct PointLight +{ + LightPhong phong; + float3 position; + AttenuationProperties attenuation_props; +}; + +struct DirectionalLight +{ + LightPhong phong; + float3 direction; +}; + +struct CalculatedLight +{ + float3 diffuse; + float3 specular; +}; + +struct BlinnPhongLighting +{ + float3 view_pos; + Material material; + + DirectionalLight directional_lights[MAX_LIGHT_CNT]; + uint directional_light_cnt; + + PointLight point_lights[MAX_LIGHT_CNT]; + uint point_light_cnt; + + float4 evaluate(in VertexData vertex_data) + { + float3 ambient_light = + this.calc_ambient_light(vertex_data.texture_coords); + + float3 directional_light_sum = this.calc_dir_light_sum(vertex_data); + float3 point_light_sum = this.calc_point_light_sum(vertex_data); + + return float4((ambient_light + directional_light_sum + point_light_sum), 1.0); + } + + float3 calc_dir_light_sum(in VertexData vertex_data) + { + float3 directional_light_sum = float3(0.0, 0.0, 0.0); + + for (uint index = 0; index < this.directional_light_cnt; index++) + { + CalculatedLight calculated_dir_light; + + this.calc_light( + // Negated since we want the light to point from the light direction + normalize(-this.directional_lights[index].direction), + this.directional_lights[index].phong, + vertex_data, + calculated_dir_light); + + directional_light_sum += + calculated_dir_light.diffuse + calculated_dir_light.specular; + } + + return directional_light_sum; + } + + float3 calc_point_light_sum(in VertexData vertex_data) + { + float3 point_light_sum = float3(0.0, 0.0, 0.0); + + for (uint index = 0; index < this.point_light_cnt; index++) + { + float3 light_direction = + normalize(this.point_lights[index].position - vertex_data.world_space_pos); + + CalculatedLight calculated_point_light; + + this.calc_light( + light_direction, + this.point_lights[index].phong, + vertex_data, + calculated_point_light); + + float attenuation = + this.calc_attenuation(this.point_lights[index], vertex_data.world_space_pos); + + calculated_point_light.diffuse *= attenuation; + calculated_point_light.specular *= attenuation; + + point_light_sum += + calculated_point_light.diffuse + calculated_point_light.specular; + } + + return point_light_sum; + } + + void calc_light( + in float3 light_direction, + in LightPhong light_phong, + in VertexData vertex_data, + out CalculatedLight calculated_light) + { + float3 norm = normalize(vertex_data.world_space_normal); + + calculated_light.diffuse = this.calc_diffuse_light( + light_phong, + light_direction, + norm, + vertex_data.texture_coords); + + calculated_light.specular = this.calc_specular_light( + light_phong, + light_direction, + norm, + vertex_data.world_space_pos, + vertex_data.texture_coords); + } + + float3 calc_ambient_light(in float2 texture_coords) + { + return ambient_map.Sample(texture_coords).xyz * this.material.ambient; + // return this.material.ambient_map.Sample(texture_coords).xyz * this.material.ambient; + } + + float3 calc_diffuse_light( + in LightPhong light_phong, + in float3 light_dir, + in float3 norm, + in float2 texture_coords) + { + float diff = max(dot(norm, light_dir), 0.0); + + return light_phong.diffuse * (diff * (diffuse_map.Sample(texture_coords).xyz * this.material.diffuse)); + // return light_phong.diffuse * (diff * (this.material.diffuse_map.Sample(texture_coords).xyz * this.material.diffuse)); + } + + float3 calc_specular_light( + in LightPhong light_phong, + in float3 light_dir, + in float3 norm, + in float3 frag_pos, + in float2 texture_coords) + { + float3 view_direction = normalize(this.view_pos - frag_pos); + + float3 halfway_direction = normalize(light_dir + view_direction); + + float spec = + pow(max(dot(norm, halfway_direction), 0.0), this.material.shininess); + + return light_phong.specular * (spec * (specular_map.Sample(texture_coords).xyz * this.material.specular)); + // return light_phong.specular * (spec * (this.material.specular_map.Sample(texture_coords).xyz * this.material.specular)); + } + + float calc_attenuation(in PointLight point_light, in float3 position) + { + float light_distance = length(point_light.position - position); + + return 1.0 / (point_light.attenuation_props.constant + point_light.attenuation_props.linear * light_distance + point_light.attenuation_props.quadratic * pow(light_distance, 2)); + } +}; + +struct Model3D +{ + float4x4 model; + float4x4 model_inverted; + float4x4 view; + float4x4 projection; +} + +// ParameterBlock<BlinnPhongLighting> blinn_phong_lighting; + +// ConstantBuffer<BlinnPhongLighting> blinn_phong_lighting; + +// ConstantBuffer<Model3D> model_3d; + +// ParameterBlock<Model3D> model_3d; + +cbuffer Uniforms { + Model3D model_3d; + BlinnPhongLighting lighting; +} + +Sampler2D ambient_map; +Sampler2D diffuse_map; +Sampler2D specular_map; + + +struct VertexData +{ + float2 texture_coords; + float3 world_space_pos; + float3 world_space_normal; +}; + +struct VertexStageOutput +{ + VertexData vertex_data : VertexData; + float4 sv_position : SV_Position; +}; + +struct Vertex +{ + float3 pos; + float2 texture_coords; + float3 normal; +}; + +struct Fragment +{ + float4 color; +}; + +[shader("vertex")] +VertexStageOutput vertex_main( + Vertex vertex, + // uniform ConstantBuffer<Model3D> model_3d +) +{ + VertexStageOutput stage_output; + + // TODO: Investigate why mul arguments need to be ordered this way. + // The mul arguments are reordered in the GLSL output + + // float4x4 proj_view = mul(model_3d.projection, model_3d.view); + float4x4 proj_view = mul(model_3d.view, model_3d.projection); + + // float4x4 proj_view_model = + // mul(proj_view, model_3d.model); + + float4x4 proj_view_model = + mul(model_3d.model, proj_view); + + // stage_output.sv_position = mul(proj_view_model, float4(vertex.pos, 1.0)); + + stage_output.sv_position = mul(float4(vertex.pos, 1.0), proj_view_model); + + float4 vertex_pos = float4(vertex.pos, 1.0); + + stage_output.vertex_data.world_space_pos = + float3(mul(model_3d.model, vertex_pos).xyz); + + stage_output.vertex_data.texture_coords = vertex.texture_coords; + + stage_output.vertex_data.world_space_normal = + mul(float3x3(transpose(model_3d.model_inverted)), vertex.normal); + + return stage_output; +} + +[shader("fragment")] +Fragment fragment_main( + VertexData vertex_data: VertexData, + // uniform ConstantBuffer<BlinnPhongLighting> lighting +) : SV_Target +{ + Fragment fragment; + + fragment.color = lighting.evaluate(vertex_data); + + // fragment.color = float4(1.0, 1.0, 1.0, 1.0); + + return fragment; +} + diff --git a/engine/src/asset.rs b/engine/src/asset.rs new file mode 100644 index 0000000..b089b73 --- /dev/null +++ b/engine/src/asset.rs @@ -0,0 +1,924 @@ +use std::any::{Any, type_name}; +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::HashMap; +use std::convert::Infallible; +use std::ffi::{OsStr, OsString}; +use std::fmt::{Debug, Display}; +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::sync::mpsc::{ + Receiver as MpscReceiver, + Sender as MpscSender, + channel as mpsc_channel, +}; + +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{PRE_UPDATE as PRE_UPDATE_PHASE, Phase}; +use ecs::sole::Single; +use ecs::{Sole, declare_entity}; + +use crate::work_queue::{Work, WorkQueue}; + +declare_entity!( + pub HANDLE_ASSETS_PHASE, + ( + Phase, + Pair::builder() + .relation::<ChildOf>() + .target_id(*PRE_UPDATE_PHASE) + .build() + ) +); + +/// Asset label. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Label<'a> +{ + pub path: Cow<'a, Path>, + pub name: Option<Cow<'a, str>>, +} + +impl Label<'_> +{ + pub fn to_owned(&self) -> LabelOwned + { + LabelOwned { + path: self.path.to_path_buf(), + name: self.name.as_ref().map(|name| name.to_string()), + } + } +} + +impl<'a> From<&'a Path> for Label<'a> +{ + fn from(path: &'a Path) -> Self + { + Self { path: path.into(), name: None } + } +} + +impl From<PathBuf> for Label<'_> +{ + fn from(path: PathBuf) -> Self + { + Self { path: path.into(), name: None } + } +} + +impl<'a> From<&'a LabelOwned> for Label<'a> +{ + fn from(label: &'a LabelOwned) -> Self + { + Self { + path: (&label.path).into(), + name: label.name.as_ref().map(|name| Cow::Borrowed(name.as_str())), + } + } +} + +impl Display for Label<'_> +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + write!(formatter, "{}", self.path.display())?; + + if let Some(name) = &self.name { + formatter.write_str("::")?; + formatter.write_str(&name)?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LabelOwned +{ + pub path: PathBuf, + pub name: Option<String>, +} + +impl LabelOwned +{ + pub fn to_label(&self) -> Label<'_> + { + Label { + path: (&self.path).into(), + name: self.name.as_ref().map(|name| Cow::Borrowed(name.as_str())), + } + } +} + +impl Display for LabelOwned +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + write!(formatter, "{}", self.path.display())?; + + if let Some(name) = &self.name { + formatter.write_str("::")?; + formatter.write_str(&name)?; + } + + Ok(()) + } +} + +#[derive(Debug, Sole)] +pub struct Assets +{ + assets: Vec<StoredAsset>, + asset_lookup: RefCell<HashMap<LabelHash, LookupEntry>>, + importers: Vec<WrappedImporterFn>, + importer_lookup: HashMap<OsString, usize>, + import_work_queue: WorkQueue<ImportWorkUserData>, + import_work_msg_receiver: MpscReceiver<ImportWorkMessage>, + import_work_msg_sender: MpscSender<ImportWorkMessage>, + events: Events, +} + +impl Assets +{ + pub fn with_capacity(capacity: usize) -> Self + { + let (import_work_msg_sender, import_work_msg_receiver) = + mpsc_channel::<ImportWorkMessage>(); + + Self { + assets: Vec::with_capacity(capacity), + asset_lookup: RefCell::new(HashMap::with_capacity(capacity)), + importers: Vec::new(), + importer_lookup: HashMap::new(), + import_work_queue: WorkQueue::new(), + import_work_msg_receiver, + import_work_msg_sender, + events: Events::default(), + } + } + + pub fn set_importer<'file_ext, AssetSettings, Err>( + &mut self, + file_extensions: impl IntoIterator<Item: Into<Cow<'file_ext, str>>>, + func: impl Fn(&mut Submitter<'_>, &Path, Option<&AssetSettings>) -> Result<(), Err>, + ) where + AssetSettings: 'static, + Err: std::error::Error + 'static, + { + self.importers.push(WrappedImporterFn::new(func)); + + let importer_index = self.importers.len() - 1; + + self.importer_lookup + .extend(file_extensions.into_iter().map(|file_ext| { + let file_ext: Cow<str> = file_ext.into(); + + (file_ext.into_owned().into(), importer_index) + })); + } + + #[tracing::instrument(skip_all, fields(asset_type=type_name::<Asset>()))] + pub fn get<'this, 'handle, Asset: 'static + Send + Sync>( + &'this self, + handle: &'handle Handle<Asset>, + ) -> Option<&'handle Asset> + where + 'this: 'handle, + { + let asset_lookup = self.asset_lookup.borrow(); + + let LookupEntry::Occupied(asset_index, _) = + asset_lookup.get(&handle.id.label_hash)? + else { + return None; + }; + + let stored_asset = self.assets.get(*asset_index).expect("Not possible"); + + let Some(asset) = stored_asset.strong.downcast_ref::<Asset>() else { + tracing::error!("Wrong asset type"); + return None; + }; + + Some(asset) + } + + #[tracing::instrument(skip_all, fields(asset_type=type_name::<Asset>()))] + pub fn get_handle_to_loaded<'label, Asset: 'static + Send + Sync>( + &self, + label: impl Into<Label<'label>>, + ) -> Option<Handle<Asset>> + { + let label = label.into(); + + let label_hash = LabelHash::new(&label); + + let asset_lookup = self.asset_lookup.borrow(); + + let LookupEntry::Occupied(asset_index, _) = asset_lookup.get(&label_hash)? else { + return None; + }; + + let stored_asset = self.assets.get(*asset_index).expect("Not possible"); + + if stored_asset.strong.downcast_ref::<Asset>().is_none() { + tracing::error!("Wrong asset type"); + return None; + }; + + Some(Handle::new(label_hash)) + } + + pub fn is_loaded_and_has_type<Asset: 'static + Send + Sync>( + &self, + handle: &Handle<Asset>, + ) -> bool + { + let asset_lookup = self.asset_lookup.borrow(); + + let Some(LookupEntry::Occupied(asset_index, _)) = + asset_lookup.get(&handle.id.label_hash) + else { + return false; + }; + + let stored_asset = self.assets.get(*asset_index).expect("Not possible"); + + stored_asset.strong.downcast_ref::<Asset>().is_some() + } + + pub fn get_label<Asset: 'static + Send + Sync>( + &self, + handle: &Handle<Asset>, + ) -> Option<LabelOwned> + { + let lookup_entry = self + .asset_lookup + .borrow() + .get(&handle.id.label_hash)? + .clone(); + + let LookupEntry::Occupied(_, label) = lookup_entry else { + return None; + }; + + Some(label) + } + + #[tracing::instrument(skip(self))] + pub fn load<'i, Asset: 'static + Send + Sync>( + &self, + label: impl Into<Label<'i>> + Debug, + ) -> Handle<Asset> + { + let label = label.into(); + + let label_hash = LabelHash::new(&label); + + let mut asset_lookup = self.asset_lookup.borrow_mut(); + + if Self::is_pending(&asset_lookup, &label) { + return Handle::new(label_hash); + } + + let Some(lookup_entry) = asset_lookup.get(&label_hash) else { + self.add_import_work::<Infallible>( + &label, + label_hash, + None, + &mut asset_lookup, + ); + + return Handle::new(label_hash); + }; + + match lookup_entry { + LookupEntry::Occupied(asset_index, _) => { + let stored_asset = self.assets.get(*asset_index).expect("Not possible"); + + if stored_asset.strong.downcast_ref::<Asset>().is_none() { + tracing::error!("Wrong asset type {}", type_name::<Asset>()); + } + } + LookupEntry::Pending => {} + } + + Handle::new(label_hash) + } + + #[tracing::instrument(skip(self))] + pub fn load_with_settings<'i, Asset, AssetSettings>( + &self, + label: impl Into<Label<'i>> + Debug, + asset_settings: AssetSettings, + ) -> Handle<Asset> + where + Asset: Send + Sync + 'static, + AssetSettings: Send + Sync + Debug + 'static, + { + let label = label.into(); + + let label_hash = LabelHash::new(&label); + + let mut asset_lookup = self.asset_lookup.borrow_mut(); + + if Self::is_pending(&asset_lookup, &label) { + return Handle::new(label_hash); + } + + let Some(lookup_entry) = asset_lookup.get(&label_hash) else { + self.add_import_work::<AssetSettings>( + &label, + label_hash, + Some(asset_settings), + &mut asset_lookup, + ); + + return Handle::new(label_hash); + }; + + match lookup_entry { + LookupEntry::Occupied(asset_index, _) => { + let stored_asset = self.assets.get(*asset_index).expect("Not possible"); + + if stored_asset.strong.downcast_ref::<Asset>().is_none() { + tracing::error!( + "Wrong asset type {} for asset", + type_name::<Asset>() + ); + } + } + LookupEntry::Pending => {} + } + + Handle::new(label_hash) + } + + pub fn store_with_name<'name, Asset: 'static + Send + Sync>( + &mut self, + name: impl Into<Cow<'name, str>>, + asset: Asset, + ) -> Handle<Asset> + { + self.store_with_label( + Label { + path: Path::new("").into(), + name: Some(name.into()), + }, + asset, + ) + } + + pub fn store_with_name_with<'name, Asset: 'static + Send + Sync>( + &mut self, + name: impl Into<Cow<'name, str>>, + func: impl FnOnce(&mut Self) -> Asset, + ) -> Handle<Asset> + { + let asset = func(self); + + self.store_with_label( + Label { + path: Path::new("").into(), + name: Some(name.into()), + }, + asset, + ) + } + + #[tracing::instrument(skip(self, asset), fields(asset_type=type_name::<Asset>()))] + pub fn store_with_label<'i, Asset: 'static + Send + Sync>( + &mut self, + label: impl Into<Label<'i>> + Debug, + asset: Asset, + ) -> Handle<Asset> + { + let label = label.into(); + + let label_hash = LabelHash::new(&label); + + if matches!( + self.asset_lookup.get_mut().get(&label_hash), + Some(LookupEntry::Occupied(_, _)) + ) { + tracing::error!("Asset already exists"); + + return Handle::new(label_hash); + } + + tracing::debug!("Storing asset"); + + self.assets.push(StoredAsset::new(asset)); + + let index = self.assets.len() - 1; + + self.asset_lookup + .get_mut() + .insert(label_hash, LookupEntry::Occupied(index, label.to_owned())); + + if label.name.is_some() { + let parent_asset_label = Label { + path: label.path.as_ref().into(), + name: None, + }; + + let parent_asset_label_hash = LabelHash::new(&parent_asset_label); + + if matches!( + self.asset_lookup.get_mut().get(&parent_asset_label_hash), + Some(LookupEntry::Pending) + ) { + self.asset_lookup.get_mut().remove(&parent_asset_label_hash); + } else if self + .asset_lookup + .get_mut() + .get(&parent_asset_label_hash) + .is_none() + { + self.assets + .push(StoredAsset::new::<Option<Infallible>>(None)); + + self.asset_lookup.get_mut().insert( + parent_asset_label_hash, + LookupEntry::Occupied( + self.assets.len() - 1, + parent_asset_label.to_owned(), + ), + ); + } + } + + self.events + .curr_tick_events + .push(Event::Stored(Id { label_hash }, label.to_owned())); + + Handle::new(label_hash) + } + + pub fn events(&self) -> &Events + { + &self.events + } + + fn is_pending(asset_lookup: &HashMap<LabelHash, LookupEntry>, label: &Label) -> bool + { + if label.name.is_some() { + if let Some(LookupEntry::Pending) = + asset_lookup.get(&LabelHash::new(&Label { + path: label.path.as_ref().into(), + name: None, + })) + { + return true; + } + } + + if let Some(LookupEntry::Pending) = asset_lookup.get(&LabelHash::new(label)) { + return true; + }; + + false + } + + fn add_import_work<AssetSettings>( + &self, + label: &Label<'_>, + label_hash: LabelHash, + asset_settings: Option<AssetSettings>, + asset_lookup: &mut HashMap<LabelHash, LookupEntry>, + ) where + AssetSettings: Any + Send + Sync, + { + let Some(file_ext) = label.path.extension() else { + tracing::error!("Asset file is missing a file extension"); + return; + }; + + let Some(importer) = self.get_importer(file_ext) else { + tracing::error!( + "No importer exists for asset file extension {}", + file_ext.to_string_lossy() + ); + return; + }; + + self.import_work_queue.add_work(Work { + func: |ImportWorkUserData { + import_work_msg_sender, + asset_path, + asset_settings, + importer, + }| { + if let Err(err) = importer.call( + import_work_msg_sender, + asset_path.as_path(), + asset_settings.as_deref(), + ) { + tracing::error!( + "Failed to load asset {}: {err}", + asset_path.display() + ); + } + }, + user_data: ImportWorkUserData { + import_work_msg_sender: self.import_work_msg_sender.clone(), + asset_path: label.path.to_path_buf(), + asset_settings: asset_settings.map(|asset_settings| { + Box::new(asset_settings) as Box<dyn Any + Send + Sync> + }), + importer: importer.clone(), + }, + }); + + asset_lookup.insert(label_hash, LookupEntry::Pending); + + if label.name.is_some() { + asset_lookup.insert( + LabelHash::new(&Label { + path: label.path.as_ref().into(), + name: None, + }), + LookupEntry::Pending, + ); + } + } + + fn get_importer(&self, file_ext: &OsStr) -> Option<&WrappedImporterFn> + { + let index = *self.importer_lookup.get(file_ext)?; + + Some(self.importers.get(index).expect("Not possible")) + } +} + +impl Default for Assets +{ + fn default() -> Self + { + Self::with_capacity(0) + } +} + +pub struct Submitter<'path> +{ + import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: &'path Path, +} + +impl Submitter<'_> +{ + pub fn submit_load_other<'label, Asset: Send + Sync + 'static>( + &self, + label: impl Into<Label<'label>>, + ) -> Handle<Asset> + { + let label = label.into(); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Load { + do_load: |assets, label, _asset_settings| { + let _ = assets.load::<Asset>(label); + }, + label: label.to_owned(), + asset_settings: None, + }); + + Handle::new(LabelHash::new(&label)) + } + + pub fn submit_load_other_with_settings<'label, Asset, AssetSettings>( + &self, + label: impl Into<Label<'label>>, + asset_settings: AssetSettings, + ) -> Handle<Asset> + where + Asset: Send + Sync + 'static, + AssetSettings: Send + Sync + Debug + 'static, + { + let label = label.into(); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Load { + do_load: |assets, label, asset_settings| { + let asset_settings = *asset_settings + .expect("Not possible") + .downcast::<AssetSettings>() + .expect("Not possible"); + + let _ = assets + .load_with_settings::<Asset, AssetSettings>(label, asset_settings); + }, + label: label.to_owned(), + asset_settings: Some(Box::new(asset_settings)), + }); + + Handle::new(LabelHash::new(&label)) + } + + pub fn submit_store<Asset: Send + Sync + 'static>( + &self, + asset: Asset, + ) -> Handle<Asset> + { + let label = LabelOwned { + path: self.asset_path.into(), + name: None, + }; + + let label_hash = LabelHash::new(&label.to_label()); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Store { + do_store: |assets, label, boxed_asset| { + let Ok(asset) = boxed_asset.downcast::<Asset>() else { + unreachable!(); + }; + + assets.store_with_label::<Asset>(&label, *asset); + }, + label, + asset: Box::new(asset), + }); + + Handle::new(label_hash) + } + + pub fn submit_store_named<Asset: Send + Sync + 'static>( + &self, + name: impl AsRef<str>, + asset: Asset, + ) -> Handle<Asset> + { + let label = LabelOwned { + path: self.asset_path.into(), + name: Some(name.as_ref().into()), + }; + + let label_hash = LabelHash::new(&label.to_label()); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Store { + do_store: |assets, label, boxed_asset| { + let Ok(asset) = boxed_asset.downcast::<Asset>() else { + unreachable!(); + }; + + assets.store_with_label::<Asset>(&label, *asset); + }, + label, + asset: Box::new(asset), + }); + + Handle::new(label_hash) + } +} + +/// Asset handle. +#[derive(Debug)] +pub struct Handle<Asset: 'static> +{ + id: Id, + _pd: PhantomData<Asset>, +} + +impl<Asset: 'static> Handle<Asset> +{ + pub fn from_id(id: Id) -> Self + { + Self { id, _pd: PhantomData } + } + + pub fn id(&self) -> Id + { + self.id + } + + fn new(label_hash: LabelHash) -> Self + { + Self { + id: Id { label_hash }, + _pd: PhantomData, + } + } +} + +impl<Asset: 'static> Clone for Handle<Asset> +{ + fn clone(&self) -> Self + { + Self { id: self.id, _pd: PhantomData } + } +} + +/// Asset ID. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id +{ + label_hash: LabelHash, +} + +#[derive(Debug, Default)] +pub struct Events +{ + curr_tick_events: Vec<Event>, + last_tick_events: Vec<Event>, +} + +impl Events +{ + pub fn last_tick_events(&self) -> impl Iterator<Item = &Event> + { + self.last_tick_events.iter() + } +} + +/// Asset event. +#[derive(Debug)] +pub enum Event +{ + /// Asset stored. + Stored(Id, LabelOwned), +} + +#[derive(Debug, thiserror::Error)] +enum ImporterError +{ + #[error("Settings has a incorrect type")] + IncorrectAssetSettingsType(PathBuf), + + #[error(transparent)] + Other(Box<dyn std::error::Error>), +} + +#[derive(Debug, Clone)] +struct WrappedImporterFn +{ + wrapper_func: fn( + MpscSender<ImportWorkMessage>, + &Path, + Option<&(dyn Any + Send + Sync)>, + ) -> Result<(), ImporterError>, +} + +impl WrappedImporterFn +{ + fn new<InnerFunc, AssetSettings, Err>(inner_func_param: InnerFunc) -> Self + where + InnerFunc: + Fn(&mut Submitter<'_>, &Path, Option<&AssetSettings>) -> Result<(), Err>, + AssetSettings: 'static, + Err: std::error::Error + 'static, + { + assert_eq!(size_of::<InnerFunc>(), 0); + + let wrapper_func = + |import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: &Path, + asset_settings: Option<&(dyn Any + Send + Sync)>| { + let inner_func = unsafe { std::mem::zeroed::<InnerFunc>() }; + + let asset_settings = asset_settings + .map(|asset_settings| { + asset_settings + .downcast_ref::<AssetSettings>() + .ok_or_else(|| { + ImporterError::IncorrectAssetSettingsType( + asset_path.to_path_buf(), + ) + }) + }) + .transpose()?; + + inner_func( + &mut Submitter { import_work_msg_sender, asset_path }, + asset_path, + asset_settings, + ) + .map_err(|err| ImporterError::Other(Box::new(err)))?; + + Ok(()) + }; + + std::mem::forget(inner_func_param); + + Self { wrapper_func } + } + + fn call( + &self, + import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: &Path, + asset_settings: Option<&(dyn Any + Send + Sync)>, + ) -> Result<(), ImporterError> + { + (self.wrapper_func)(import_work_msg_sender, asset_path, asset_settings) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct LabelHash(u64); + +impl LabelHash +{ + fn new(label: &Label<'_>) -> Self + { + let mut hasher = DefaultHasher::new(); + + label.hash(&mut hasher); + + Self(hasher.finish()) + } +} + +#[derive(Debug, Default)] +pub(crate) struct Extension +{ + pub assets: Assets, +} + +impl ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: ecs::extension::Collector<'_>) + { + let _ = collector.add_sole(self.assets); + + collector.add_declared_entity(&HANDLE_ASSETS_PHASE); + + collector.add_system(*HANDLE_ASSETS_PHASE, add_received_assets); + } +} + +fn add_received_assets(mut assets: Single<Assets>) +{ + let Events { curr_tick_events, last_tick_events } = &mut assets.events; + + std::mem::swap(last_tick_events, curr_tick_events); + + curr_tick_events.clear(); + + while let Some(import_work_msg) = assets.import_work_msg_receiver.try_recv().ok() { + match import_work_msg { + ImportWorkMessage::Store { do_store, label, asset } => { + do_store(&mut assets, label, asset); + } + ImportWorkMessage::Load { do_load, label, asset_settings } => { + do_load( + &assets, + Label { + path: label.path.as_path().into(), + name: label.name.as_deref().map(|name| name.into()), + }, + asset_settings, + ); + } + } + } +} + +#[derive(Debug)] +struct ImportWorkUserData +{ + import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: PathBuf, + asset_settings: Option<Box<dyn Any + Send + Sync>>, + importer: WrappedImporterFn, +} + +#[derive(Debug)] +enum ImportWorkMessage +{ + Store + { + do_store: fn(&mut Assets, LabelOwned, Box<dyn Any + Send + Sync>), + label: LabelOwned, + asset: Box<dyn Any + Send + Sync>, + }, + + Load + { + do_load: fn(&Assets, Label<'_>, Option<Box<dyn Any + Send + Sync>>), + label: LabelOwned, + asset_settings: Option<Box<dyn Any + Send + Sync>>, + }, +} + +#[derive(Debug, Clone)] +enum LookupEntry +{ + Occupied(usize, LabelOwned), + Pending, +} + +#[derive(Debug)] +struct StoredAsset +{ + strong: Arc<dyn Any + Send + Sync>, +} + +impl StoredAsset +{ + fn new<Asset: Any + Send + Sync>(asset: Asset) -> Self + { + let strong = Arc::new(asset); + + Self { strong } + } +} diff --git a/engine/src/camera.rs b/engine/src/camera.rs index 66150af..1ecb4f3 100644 --- a/engine/src/camera.rs +++ b/engine/src/camera.rs @@ -5,7 +5,7 @@ use crate::vector::Vec3; pub mod fly; -#[derive(Debug, Component)] +#[derive(Debug, Clone, Component)] pub struct Camera { pub target: Vec3<f32>, diff --git a/engine/src/camera/fly.rs b/engine/src/camera/fly.rs index 254d020..7996b4d 100644 --- a/engine/src/camera/fly.rs +++ b/engine/src/camera/fly.rs @@ -1,14 +1,16 @@ use ecs::component::local::Local; use ecs::phase::UPDATE as UPDATE_PHASE; use ecs::sole::Single; -use ecs::system::{Into, System}; +use ecs::system::initializable::Initializable; +use ecs::system::Into; use ecs::{Component, Query}; +use crate::builder; use crate::camera::{Active as ActiveCamera, Camera}; use crate::delta_time::DeltaTime; -use crate::input::{Cursor, CursorFlags, Key, KeyState, Keys}; +use crate::input::keyboard::{Key, Keyboard}; +use crate::input::mouse::Motion as MouseMotion; use crate::transform::WorldPosition; -use crate::util::builder; use crate::vector::{Vec2, Vec3}; builder! { @@ -59,12 +61,7 @@ impl ecs::extension::Extension for Extension { fn collect(self, mut collector: ecs::extension::Collector<'_>) { - collector.add_system( - *UPDATE_PHASE, - update - .into_system() - .initialize((CursorState::default(), self.0)), - ); + collector.add_system(*UPDATE_PHASE, update.into_system().initialize((self.0,))); } } @@ -76,35 +73,29 @@ pub struct Options fn update( camera_query: Query<(&mut Camera, &mut WorldPosition, &mut Fly, &ActiveCamera)>, - keys: Single<Keys>, - cursor: Single<Cursor>, - cursor_flags: Single<CursorFlags>, + keyboard: Single<Keyboard>, + mouse_motion: Single<MouseMotion>, delta_time: Single<DeltaTime>, - mut cursor_state: Local<CursorState>, options: Local<Options>, ) { for (mut camera, mut camera_world_pos, mut fly_camera, _) in &camera_query { - if cursor.has_moved && cursor_flags.is_first_move.flag { - tracing::debug!("First cursor move"); - - cursor_state.last_pos = cursor.position; - } - let delta_time = delta_time.duration; - let mut x_offset = cursor.position.x - cursor_state.last_pos.x; - let mut y_offset = cursor_state.last_pos.y - cursor.position.y; + // tracing::info!("Mouse motion: {:?}", mouse_motion.position_delta); - cursor_state.last_pos = cursor.position; + if mouse_motion.position_delta != (Vec2 { x: 0.0, y: 0.0 }) { + let x_offset = + mouse_motion.position_delta.x * f64::from(options.mouse_sensitivity); - x_offset *= f64::from(options.mouse_sensitivity); - y_offset *= f64::from(options.mouse_sensitivity); + let y_offset = + (-mouse_motion.position_delta.y) * f64::from(options.mouse_sensitivity); - fly_camera.current_yaw += x_offset; - fly_camera.current_pitch += y_offset; + fly_camera.current_yaw += x_offset; + fly_camera.current_pitch += y_offset; - fly_camera.current_pitch = fly_camera.current_pitch.clamp(-89.0, 89.0); + fly_camera.current_pitch = fly_camera.current_pitch.clamp(-89.0, 89.0); + } // TODO: This casting to a f32 from a f64 is horrible. fix it #[allow(clippy::cast_possible_truncation)] @@ -121,24 +112,24 @@ fn update( camera.global_up = cam_right.cross(&direction).normalize(); - if keys.get_key_state(Key::W) == KeyState::Pressed { + if keyboard.pressed(Key::W) { camera_world_pos.position += direction * fly_camera.speed * delta_time.as_secs_f32(); } - if keys.get_key_state(Key::S) == KeyState::Pressed { + if keyboard.pressed(Key::S) { camera_world_pos.position -= direction * fly_camera.speed * delta_time.as_secs_f32(); } - if keys.get_key_state(Key::A) == KeyState::Pressed { + if keyboard.pressed(Key::A) { let cam_left = -direction.cross(&Vec3::UP).normalize(); camera_world_pos.position += cam_left * fly_camera.speed * delta_time.as_secs_f32(); } - if keys.get_key_state(Key::D) == KeyState::Pressed { + if keyboard.pressed(Key::D) { let cam_right = direction.cross(&Vec3::UP).normalize(); camera_world_pos.position += @@ -148,9 +139,3 @@ fn update( camera.target = camera_world_pos.position + direction; } } - -#[derive(Debug, Default, Component)] -struct CursorState -{ - last_pos: Vec2<f64>, -} diff --git a/engine/src/data_types/color.rs b/engine/src/data_types/color.rs index cef3b92..c5316e6 100644 --- a/engine/src/data_types/color.rs +++ b/engine/src/data_types/color.rs @@ -1,7 +1,6 @@ use std::ops::{Add, Div, Mul, Neg, Sub}; #[derive(Debug, Clone, Default)] -#[repr(C)] pub struct Color<Value> { pub red: Value, diff --git a/engine/src/data_types/dimens.rs b/engine/src/data_types/dimens.rs index d8d0247..8bf239f 100644 --- a/engine/src/data_types/dimens.rs +++ b/engine/src/data_types/dimens.rs @@ -1,3 +1,5 @@ +use std::num::NonZeroU32; + /// 2D dimensions. #[derive(Debug, Clone, Copy)] pub struct Dimens<Value> @@ -22,6 +24,18 @@ impl<Value> From<(Value, Value)> for Dimens<Value> } } +impl Dimens<u32> +{ + #[must_use] + pub fn try_into_nonzero(self) -> Option<Dimens<NonZeroU32>> + { + Some(Dimens { + width: NonZeroU32::new(self.width)?, + height: NonZeroU32::new(self.height)?, + }) + } +} + /// 3D dimensions. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct Dimens3<Value> diff --git a/engine/src/data_types/matrix.rs b/engine/src/data_types/matrix.rs index 3a29ae2..39aeea0 100644 --- a/engine/src/data_types/matrix.rs +++ b/engine/src/data_types/matrix.rs @@ -1,10 +1,12 @@ -use crate::vector::Vec3; +use std::ops::Mul; + +use crate::vector::{Vec3, Vec4, VecN}; #[derive(Debug, Clone)] pub struct Matrix<Value, const ROWS: usize, const COLUMNS: usize> { /// Items must be layed out this way for it to work with OpenGL shaders. - items: [[Value; ROWS]; COLUMNS], + pub items: [[Value; ROWS]; COLUMNS], } impl<Value, const ROWS: usize, const COLUMNS: usize> Matrix<Value, ROWS, COLUMNS> @@ -20,17 +22,24 @@ impl<Value, const ROWS: usize, const COLUMNS: usize> Matrix<Value, ROWS, COLUMNS } } + pub fn from_columns<ColumnVec>(columns: [ColumnVec; COLUMNS]) -> Self + where + ColumnVec: VecN<Value, ROWS>, + { + Self { + items: columns.map(|column| column.into_array()), + } + } + /// Sets the value at the specified cell. pub fn set_cell(&mut self, row: usize, column: usize, value: Value) { self.items[column][row] = value; } - /// Returns the internal 2D array as a pointer. - #[must_use] - pub fn as_ptr(&self) -> *const Value + pub fn items(&self) -> &[[Value; ROWS]; COLUMNS] { - self.items[0].as_ptr() + &self.items } } @@ -119,4 +128,155 @@ impl Matrix<f32, 4, 4> self.set_cell(3, 3, 1.0); } + + pub fn inverse(&self) -> Self + { + let coef_00 = + self.items[2][2] * self.items[3][3] - self.items[3][2] * self.items[2][3]; + let coef_02 = + self.items[1][2] * self.items[3][3] - self.items[3][2] * self.items[1][3]; + let coef_03 = + self.items[1][2] * self.items[2][3] - self.items[2][2] * self.items[1][3]; + + let coef_04 = + self.items[2][1] * self.items[3][3] - self.items[3][1] * self.items[2][3]; + let coef_06 = + self.items[1][1] * self.items[3][3] - self.items[3][1] * self.items[1][3]; + let coef_07 = + self.items[1][1] * self.items[2][3] - self.items[2][1] * self.items[1][3]; + + let coef_08 = + self.items[2][1] * self.items[3][2] - self.items[3][1] * self.items[2][2]; + let coef_10 = + self.items[1][1] * self.items[3][2] - self.items[3][1] * self.items[1][2]; + let coef_11 = + self.items[1][1] * self.items[2][2] - self.items[2][1] * self.items[1][2]; + + let coef_12 = + self.items[2][0] * self.items[3][3] - self.items[3][0] * self.items[2][3]; + let coef_14 = + self.items[1][0] * self.items[3][3] - self.items[3][0] * self.items[1][3]; + let coef_15 = + self.items[1][0] * self.items[2][3] - self.items[2][0] * self.items[1][3]; + + let coef_16 = + self.items[2][0] * self.items[3][2] - self.items[3][0] * self.items[2][2]; + let coef_18 = + self.items[1][0] * self.items[3][2] - self.items[3][0] * self.items[1][2]; + let coef_19 = + self.items[1][0] * self.items[2][2] - self.items[2][0] * self.items[1][2]; + + let coef_20 = + self.items[2][0] * self.items[3][1] - self.items[3][0] * self.items[2][1]; + let coef_22 = + self.items[1][0] * self.items[3][1] - self.items[3][0] * self.items[1][1]; + let coef_23 = + self.items[1][0] * self.items[2][1] - self.items[2][0] * self.items[1][1]; + + let fac_0 = Vec4 { + x: coef_00, + y: coef_00, + z: coef_02, + w: coef_03, + }; + let fac_1 = Vec4 { + x: coef_04, + y: coef_04, + z: coef_06, + w: coef_07, + }; + let fac_2 = Vec4 { + x: coef_08, + y: coef_08, + z: coef_10, + w: coef_11, + }; + let fac_3 = Vec4 { + x: coef_12, + y: coef_12, + z: coef_14, + w: coef_15, + }; + let fac_4 = Vec4 { + x: coef_16, + y: coef_16, + z: coef_18, + w: coef_19, + }; + let fac_5 = Vec4 { + x: coef_20, + y: coef_20, + z: coef_22, + w: coef_23, + }; + + let vec_0 = Vec4 { + x: self.items[1][0], + y: self.items[0][0], + z: self.items[0][0], + w: self.items[0][0], + }; + let vec_1 = Vec4 { + x: self.items[1][1], + y: self.items[0][1], + z: self.items[0][1], + w: self.items[0][1], + }; + let vec_2 = Vec4 { + x: self.items[1][2], + y: self.items[0][2], + z: self.items[0][2], + w: self.items[0][2], + }; + let vec_3 = Vec4 { + x: self.items[1][3], + y: self.items[0][3], + z: self.items[0][3], + w: self.items[0][3], + }; + + let inv_0 = vec_1 * fac_0 - vec_2 * fac_1 + vec_3 * fac_2; + let inv_1 = vec_0 * fac_0 - vec_2 * fac_3 + vec_3 * fac_4; + let inv_2 = vec_0 * fac_1 - vec_1 * fac_3 + vec_3 * fac_5; + let inv_3 = vec_0 * fac_2 - vec_1 * fac_4 + vec_2 * fac_5; + + let sign_a = Vec4 { x: 1.0, y: -1.0, z: 1.0, w: -1.0 }; + let sign_b = Vec4 { x: -1.0, y: 1.0, z: -1.0, w: 1.0 }; + + let inverse = Self::from_columns([ + inv_0 * sign_a, + inv_1 * sign_b, + inv_2 * sign_a, + inv_3 * sign_b, + ]); + + let row_0 = Vec4 { + x: inverse.items[0][0], + y: inverse.items[1][0], + z: inverse.items[2][0], + w: inverse.items[3][0], + }; + + let dot_0 = Vec4::<f32>::from(self.items[0]) * row_0; + + let dot_1 = (dot_0.x + dot_0.y) + (dot_0.z + dot_0.w); + + let one_over_determinant = 1.0 / dot_1; + + inverse * one_over_determinant + } +} + +impl Mul<f32> for Matrix<f32, 4, 4> +{ + type Output = Self; + + fn mul(self, scalar: f32) -> Self::Output + { + Self { + items: self + .items + .map(|column| (Vec4::from(column) * scalar).into_array()), + } + } } diff --git a/engine/src/data_types/vector.rs b/engine/src/data_types/vector.rs index 100c709..1a4e49e 100644 --- a/engine/src/data_types/vector.rs +++ b/engine/src/data_types/vector.rs @@ -2,8 +2,13 @@ use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign}; use crate::color::Color; +/// A vector of `Value`s with `N` number of elements. +pub trait VecN<Value, const N: usize>: sealed::Sealed +{ + fn into_array(self) -> [Value; N]; +} + #[derive(Debug, Default, Clone, Copy, PartialEq)] -#[repr(C)] pub struct Vec2<Value> { pub x: Value, @@ -15,6 +20,29 @@ impl Vec2<u32> pub const ZERO: Self = Self { x: 0, y: 0 }; } +impl<Value> Add for Vec2<Value> +where + Value: Add<Value, Output = Value>, +{ + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output + { + Self::Output { x: self.x + rhs.x, y: self.y + rhs.y } + } +} + +impl<Value> AddAssign for Vec2<Value> +where + Value: Add<Value, Output = Value> + Clone, +{ + fn add_assign(&mut self, rhs: Self) + { + self.x = self.x.clone() + rhs.x; + self.y = self.y.clone() + rhs.y; + } +} + impl<Value> Add<Value> for Vec2<Value> where Value: Add<Output = Value> + Clone, @@ -75,8 +103,17 @@ where } } +impl<Value> VecN<Value, 2> for Vec2<Value> +{ + fn into_array(self) -> [Value; 2] + { + [self.x, self.y] + } +} + +impl<Value> sealed::Sealed for Vec2<Value> {} + #[derive(Debug, Default, Clone, Copy, PartialEq)] -#[repr(C)] pub struct Vec3<Value> { pub x: Value, @@ -344,3 +381,128 @@ impl<Value> From<Color<Value>> for Vec3<Value> } } } + +impl<Value> VecN<Value, 3> for Vec3<Value> +{ + fn into_array(self) -> [Value; 3] + { + [self.x, self.y, self.z] + } +} + +impl<Value> sealed::Sealed for Vec3<Value> {} + +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub struct Vec4<Value> +{ + pub x: Value, + pub y: Value, + pub z: Value, + pub w: Value, +} + +impl<Value> Mul for Vec4<Value> +where + Value: Mul<Value, Output = Value>, +{ + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output + { + Self::Output { + x: self.x * rhs.x, + y: self.y * rhs.y, + z: self.z * rhs.z, + w: self.w * rhs.w, + } + } +} + +impl<Value> Add for Vec4<Value> +where + Value: Add<Value, Output = Value>, +{ + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output + { + Self::Output { + x: self.x + rhs.x, + y: self.y + rhs.y, + z: self.z + rhs.z, + w: self.w + rhs.w, + } + } +} + +impl<Value> Sub for Vec4<Value> +where + Value: Sub<Value, Output = Value>, +{ + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output + { + Self::Output { + x: self.x - rhs.x, + y: self.y - rhs.y, + z: self.z - rhs.z, + w: self.w - rhs.w, + } + } +} + +impl<Value> Mul<Value> for Vec4<Value> +where + Value: Mul<Value, Output = Value> + Clone, +{ + type Output = Self; + + fn mul(mut self, rhs: Value) -> Self::Output + { + self.x = self.x * rhs.clone(); + self.y = self.y * rhs.clone(); + self.z = self.z * rhs.clone(); + self.w = self.w * rhs.clone(); + + self + } +} + +impl<Value: Clone> From<Value> for Vec4<Value> +{ + fn from(value: Value) -> Self + { + Self { + x: value.clone(), + y: value.clone(), + z: value.clone(), + w: value, + } + } +} + +impl<Value> From<[Value; 4]> for Vec4<Value> +{ + fn from(values: [Value; 4]) -> Self + { + let [x, y, z, w] = values; + + Self { x, y, z, w } + } +} + +impl<Value> VecN<Value, 4> for Vec4<Value> +{ + fn into_array(self) -> [Value; 4] + { + [self.x, self.y, self.z, self.w] + } +} + +impl<Value> sealed::Sealed for Vec4<Value> {} + +mod sealed +{ + pub trait Sealed {} +} diff --git a/engine/src/draw_flags.rs b/engine/src/draw_flags.rs index df5eed1..8328669 100644 --- a/engine/src/draw_flags.rs +++ b/engine/src/draw_flags.rs @@ -1,6 +1,6 @@ use ecs::Component; -use crate::util::builder; +use crate::builder; builder! { /// Flags for how a object should be drawn. @@ -22,7 +22,7 @@ impl DrawFlags } } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct PolygonModeConfig { pub face: PolygonModeFace, diff --git a/engine/src/file_format/wavefront/mtl.rs b/engine/src/file_format/wavefront/mtl.rs index 7d1c570..f3c7a64 100644 --- a/engine/src/file_format/wavefront/mtl.rs +++ b/engine/src/file_format/wavefront/mtl.rs @@ -234,6 +234,24 @@ fn statements_to_materials( path: Path::new(texture_file_path).to_path_buf(), }); } + Keyword::Ns => { + if statement.arguments.len() != 1 { + return Err(Error::UnsupportedArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no, + }); + } + + let shininess = statement.get_float_arg(0, line_no)?; + + tracing::debug!( + "Adding shininess {shininess} to material {}", + curr_material.name + ); + + curr_material.shininess = shininess; + } Keyword::Newmtl => {} } } @@ -279,5 +297,7 @@ keyword! { #[keyword(rename = "map_Ks")] MapKs, + + Ns, } } diff --git a/engine/src/image.rs b/engine/src/image.rs new file mode 100644 index 0000000..9296167 --- /dev/null +++ b/engine/src/image.rs @@ -0,0 +1,184 @@ +use std::fs::File; +use std::io::BufReader; +use std::path::Path; + +use image_rs::GenericImageView as _; + +use crate::asset::{Assets, Submitter as AssetSubmitter}; +use crate::builder; +use crate::color::Color; +use crate::data_types::dimens::Dimens; + +#[derive(Debug)] +pub struct Image +{ + inner: image_rs::DynamicImage, +} + +impl Image +{ + pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> + { + let buffered_reader = + BufReader::new(File::open(&path).map_err(Error::ReadFailed)?); + + let image_reader = image_rs::io::Reader::with_format( + buffered_reader, + image_rs::ImageFormat::from_path(path) + .map_err(|_| Error::UnsupportedFormat)?, + ); + + Ok(Self { + inner: image_reader + .decode() + .map_err(|err| Error::DecodeFailed(DecodeError(err)))?, + }) + } + + pub fn from_color(dimens: impl Into<Dimens<u32>>, color: impl Into<Color<u8>>) + -> Self + { + let dimens: Dimens<u32> = dimens.into(); + + let color: Color<u8> = color.into(); + + Self { + inner: image_rs::RgbImage::from_pixel( + dimens.width, + dimens.height, + image_rs::Rgb([color.red, color.green, color.blue]), + ) + .into(), + } + } + + pub fn dimensions(&self) -> Dimens<u32> + { + self.inner.dimensions().into() + } + + pub fn color_type(&self) -> ColorType + { + self.inner.color().into() + } + + pub fn as_bytes(&self) -> &[u8] + { + self.inner.as_bytes() + } +} + +builder! { +#[builder(name = SettingsBuilder, derives=(Debug, Clone))] +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct Settings { +} +} + +impl Settings +{ + pub fn builder() -> SettingsBuilder + { + SettingsBuilder::default() + } +} + +impl Default for SettingsBuilder +{ + fn default() -> Self + { + Settings::default().into() + } +} + +/// An enumeration over supported color types and bit depths +#[derive(Copy, PartialEq, Eq, Debug, Clone, Hash)] +#[non_exhaustive] +pub enum ColorType +{ + /// Pixel is 8-bit luminance + L8, + + /// Pixel is 8-bit luminance with an alpha channel + La8, + + /// Pixel contains 8-bit R, G and B channels + Rgb8, + + /// Pixel is 8-bit RGB with an alpha channel + Rgba8, + + /// Pixel is 16-bit luminance + L16, + + /// Pixel is 16-bit luminance with an alpha channel + La16, + + /// Pixel is 16-bit RGB + Rgb16, + + /// Pixel is 16-bit RGBA + Rgba16, + + /// Pixel is 32-bit float RGB + Rgb32F, + + /// Pixel is 32-bit float RGBA + Rgba32F, +} + +impl From<image_rs::ColorType> for ColorType +{ + fn from(color_type: image_rs::ColorType) -> Self + { + match color_type { + image_rs::ColorType::L8 => Self::L8, + image_rs::ColorType::La8 => Self::La8, + image_rs::ColorType::Rgb8 => Self::Rgb8, + image_rs::ColorType::Rgba8 => Self::Rgba8, + image_rs::ColorType::L16 => Self::L16, + image_rs::ColorType::La16 => Self::La16, + image_rs::ColorType::Rgb16 => Self::Rgb16, + image_rs::ColorType::Rgba16 => Self::Rgba16, + image_rs::ColorType::Rgb32F => Self::Rgb32F, + image_rs::ColorType::Rgba32F => Self::Rgba32F, + _ => { + panic!("Unrecognized image_rs::ColorType variant"); + } + } + } +} + +pub fn set_asset_importers(assets: &mut Assets) +{ + assets.set_importer::<_, _>(["png", "jpg"], import_asset); +} + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error("Failed to read file")] + ReadFailed(#[source] std::io::Error), + + #[error("Failed to decode image")] + DecodeFailed(DecodeError), + + #[error("Unsupported image format")] + UnsupportedFormat, +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct DecodeError(image_rs::ImageError); + +fn import_asset( + asset_submitter: &mut AssetSubmitter<'_>, + path: &Path, + _settings: Option<&'_ Settings>, +) -> Result<(), Error> +{ + asset_submitter.submit_store::<Image>(Image::open(path)?); + + Ok(()) +} diff --git a/engine/src/input.rs b/engine/src/input.rs index d6a82f6..f8c9dfd 100644 --- a/engine/src/input.rs +++ b/engine/src/input.rs @@ -1,249 +1,32 @@ -use std::collections::HashMap; - +use ecs::declare_entity; use ecs::extension::Collector as ExtensionCollector; -use ecs::pair::{ChildOf, Pair}; -use ecs::phase::{Phase, PRE_UPDATE as PRE_UPDATE_PHASE, START as START_PHASE}; -use ecs::sole::Single; -use ecs::{static_entity, Sole}; - -use crate::vector::Vec2; -use crate::window::{Window, UPDATE_PHASE as WINDOW_UPDATE_PHASE}; - -mod reexports -{ - pub use crate::window::{Key, KeyState}; -} - -pub use reexports::*; - -static_entity!( - SET_PREV_KEY_STATE_PHASE, - (Phase, Pair::new::<ChildOf>(*WINDOW_UPDATE_PHASE)) +use ecs::pair::{DependsOn, Pair}; +use ecs::phase::Phase; + +use crate::windowing::PHASE as WINDOWING_PHASE; + +pub mod keyboard; +pub mod mouse; + +declare_entity!( + pub PHASE, + ( + Phase, + Pair::builder() + .relation::<DependsOn>() + .target_id(*WINDOWING_PHASE) + .build() + ) ); -#[derive(Debug, Sole)] -pub struct Keys -{ - map: HashMap<Key, KeyData>, - pending: Vec<(Key, KeyState)>, -} - -impl Keys -{ - #[must_use] - pub fn new() -> Self - { - Self { - map: Key::KEYS - .iter() - .map(|key| { - ( - *key, - KeyData { - state: KeyState::Released, - prev_tick_state: KeyState::Released, - }, - ) - }) - .collect(), - pending: Vec::with_capacity(Key::KEYS.len()), - } - } - - #[must_use] - pub fn get_key_state(&self, key: Key) -> KeyState - { - let Some(key_data) = self.map.get(&key) else { - unreachable!(); - }; - - key_data.state - } - - #[must_use] - pub fn get_prev_key_state(&self, key: Key) -> KeyState - { - let Some(key_data) = self.map.get(&key) else { - unreachable!(); - }; - - key_data.prev_tick_state - } - - pub fn set_key_state(&mut self, key: Key, new_key_state: KeyState) - { - let Some(key_data) = self.map.get_mut(&key) else { - unreachable!(); - }; - - key_data.state = new_key_state; - } - - #[must_use] - pub fn is_anything_pressed(&self) -> bool - { - self.map - .values() - .any(|key_data| matches!(key_data.state, KeyState::Pressed)) - } -} - -impl Default for Keys -{ - fn default() -> Self - { - Self::new() - } -} - -#[derive(Debug, Default, Clone, Sole)] -pub struct Cursor -{ - pub position: Vec2<f64>, - pub has_moved: bool, -} - -#[derive(Debug, Clone, Sole)] -pub struct CursorFlags -{ - /// This flag is set in two situations: - /// A: The window has just started - /// B: The window has gained focus again after losing focus. - /// - /// This flag only lasts a single tick then it is cleared (at the beginning of the - /// next tick). - pub is_first_move: CursorFlag, -} - -impl Default for CursorFlags -{ - fn default() -> Self - { - Self { - is_first_move: CursorFlag { flag: true, ..Default::default() }, - } - } -} - -#[derive(Debug, Default, Clone)] -pub struct CursorFlag -{ - pub flag: bool, - pub pending_clear: bool, -} - -impl CursorFlag -{ - pub fn clear(&mut self) - { - self.flag = false; - self.pending_clear = false; - } -} - /// Input extension. #[derive(Debug, Default)] pub struct Extension {} impl ecs::extension::Extension for Extension { - fn collect(self, mut collector: ExtensionCollector<'_>) + fn collect(self, _collector: ExtensionCollector<'_>) { - collector.add_system(*START_PHASE, initialize); - collector.add_system(*PRE_UPDATE_PHASE, maybe_clear_cursor_is_first_move); - collector.add_system(*SET_PREV_KEY_STATE_PHASE, set_pending_key_states); - - collector.add_sole(Keys::default()).ok(); - collector.add_sole(Cursor::default()).ok(); - collector.add_sole(CursorFlags::default()).ok(); + // TODO: Add input mapping } } - -fn initialize( - keys: Single<Keys>, - cursor: Single<Cursor>, - cursor_flags: Single<CursorFlags>, - window: Single<Window>, -) -{ - let keys_weak_ref = keys.to_weak_ref(); - - window.set_key_callback(move |key, _scancode, key_state, _modifiers| { - let keys_ref = keys_weak_ref.access().expect("No world"); - - let mut keys = keys_ref.to_single(); - - keys.pending.push((key, key_state)); - }); - - let cursor_weak_ref = cursor.to_weak_ref(); - - window.set_cursor_pos_callback(move |cursor_position| { - let cursor_ref = cursor_weak_ref.access().expect("No world"); - - let mut cursor = cursor_ref.to_single(); - - cursor.position = Vec2 { - x: cursor_position.x, - y: cursor_position.y, - }; - - cursor.has_moved = true; - }); - - let cursor_flags_weak_ref = cursor_flags.to_weak_ref(); - - window.set_focus_callback(move |is_focused| { - tracing::trace!("Window is focused: {is_focused}"); - - let cursor_flags_ref = cursor_flags_weak_ref.access().expect("No world"); - - cursor_flags_ref.to_single().is_first_move.flag = is_focused; - }); -} - -fn maybe_clear_cursor_is_first_move( - cursor: Single<Cursor>, - mut cursor_flags: Single<CursorFlags>, -) -{ - if cursor_flags.is_first_move.pending_clear { - tracing::trace!("Clearing is_first_move"); - - // This flag was set for the whole previous tick so it can be cleared now - cursor_flags.is_first_move.clear(); - - return; - } - - if cursor.has_moved && cursor_flags.is_first_move.flag { - tracing::trace!("Setting flag to clear is_first_move next tick"); - - // Make this system clear is_first_move the next time it runs - cursor_flags.is_first_move.pending_clear = true; - } -} - -fn set_pending_key_states(mut keys: Single<Keys>) -{ - let Keys { map, pending } = &mut *keys; - - for key_data in map.values_mut() { - key_data.prev_tick_state = key_data.state; - } - - for (key, key_state) in pending { - let Some(key_data) = map.get_mut(key) else { - unreachable!(); - }; - - key_data.state = *key_state; - } -} - -#[derive(Debug)] -struct KeyData -{ - state: KeyState, - prev_tick_state: KeyState, -} diff --git a/engine/src/input/keyboard.rs b/engine/src/input/keyboard.rs new file mode 100644 index 0000000..d226df0 --- /dev/null +++ b/engine/src/input/keyboard.rs @@ -0,0 +1,6 @@ +mod reexports +{ + pub use crate::windowing::keyboard::{Key, KeyState, Keyboard}; +} + +pub use reexports::*; diff --git a/engine/src/input/mouse.rs b/engine/src/input/mouse.rs new file mode 100644 index 0000000..90091f3 --- /dev/null +++ b/engine/src/input/mouse.rs @@ -0,0 +1,6 @@ +mod reexports +{ + pub use crate::windowing::mouse::{Button, ButtonState, Buttons, Motion}; +} + +pub use reexports::*; diff --git a/engine/src/lib.rs b/engine/src/lib.rs index c537e06..560d288 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -3,40 +3,50 @@ use ecs::component::Sequence as ComponentSequence; use ecs::extension::Extension; -use ecs::pair::Pair; use ecs::phase::PRE_UPDATE as PRE_UPDATE_PHASE; use ecs::sole::Sole; +use ecs::system::initializable::Initializable; +use ecs::system::observer::Observer; use ecs::system::{Into, System}; use ecs::uid::Uid; use ecs::{SoleAlreadyExistsError, World}; -use crate::delta_time::{update as update_delta_time, DeltaTime, LastUpdate}; +use crate::asset::{Assets, Extension as AssetExtension}; +use crate::delta_time::{DeltaTime, LastUpdate, update as update_delta_time}; +use crate::shader::Extension as ShaderExtension; -mod opengl; mod util; +mod work_queue; +pub mod asset; pub mod camera; pub mod collision; pub mod data_types; pub mod delta_time; pub mod draw_flags; pub mod file_format; +pub mod image; pub mod input; pub mod lighting; pub mod material; pub mod math; pub mod mesh; +pub mod model; pub mod projection; +pub mod reflection; pub mod renderer; +pub mod shader; pub mod texture; pub mod transform; -pub mod window; +pub mod windowing; pub extern crate ecs; pub(crate) use crate::data_types::matrix; pub use crate::data_types::{color, vector}; +const INITIAL_ASSET_CAPACITY: usize = 128; + #[derive(Debug)] pub struct Engine { @@ -49,6 +59,9 @@ impl Engine #[must_use] pub fn new() -> Self { + #[cfg(windows)] + nu_ansi_term::enable_ansi_support().unwrap(); + let mut world = World::new(); world.add_sole(DeltaTime::default()).ok(); @@ -60,6 +73,16 @@ impl Engine .initialize((LastUpdate::default(),)), ); + let mut assets = Assets::with_capacity(INITIAL_ASSET_CAPACITY); + + crate::model::asset::add_importers(&mut assets); + crate::material::asset::add_importers(&mut assets); + crate::shader::add_asset_importers(&mut assets); + crate::image::set_asset_importers(&mut assets); + + world.add_extension(AssetExtension { assets }); + world.add_extension(ShaderExtension); + Self { world } } @@ -79,13 +102,12 @@ impl Engine self.world.register_system(phase_euid, system); } - pub fn register_observer_system<'this, SystemImpl>( + pub fn register_observer<'this, SystemImpl>( &'this mut self, - system: impl System<'this, SystemImpl>, - event: Pair<Uid, Uid>, + observer: impl Observer<'this, SystemImpl>, ) { - self.world.register_observer_system(system, event); + self.world.register_observer(observer); } /// Adds a globally shared singleton value. diff --git a/engine/src/lighting.rs b/engine/src/lighting.rs index 09dd980..9ab2ca8 100644 --- a/engine/src/lighting.rs +++ b/engine/src/lighting.rs @@ -1,8 +1,8 @@ use ecs::{Component, Sole}; +use crate::builder; use crate::color::Color; use crate::data_types::vector::Vec3; -use crate::util::builder; builder! { #[builder(name = PointLightBuilder, derives = (Debug, Clone))] @@ -59,7 +59,6 @@ pub struct AttenuationParams impl Default for AttenuationParams { - #[must_use] fn default() -> Self { Self { diff --git a/engine/src/material.rs b/engine/src/material.rs index e368519..94ab24e 100644 --- a/engine/src/material.rs +++ b/engine/src/material.rs @@ -1,29 +1,37 @@ use ecs::Component; +use crate::builder; use crate::color::Color; -use crate::data_types::dimens::Dimens; -use crate::texture::{Id as TextureId, Texture}; -use crate::util::builder; +use crate::texture::Texture; -#[derive(Debug, Clone, Component)] +pub mod asset; + +#[derive(Debug, Clone)] #[non_exhaustive] pub struct Material { pub ambient: Color<f32>, pub diffuse: Color<f32>, pub specular: Color<f32>, - pub ambient_map: TextureId, - pub diffuse_map: TextureId, - pub specular_map: TextureId, - pub textures: Vec<Texture>, + pub ambient_map: Option<Texture>, + pub diffuse_map: Option<Texture>, + pub specular_map: Option<Texture>, pub shininess: f32, } impl Material { - pub fn builder() -> Builder + pub const fn builder() -> Builder + { + Builder::new() + } +} + +impl Default for Material +{ + fn default() -> Self { - Builder::default() + Self::builder().build() } } @@ -31,29 +39,27 @@ impl Material #[derive(Debug, Clone)] pub struct Builder { - ambient: Option<Color<f32>>, - diffuse: Option<Color<f32>>, - specular: Option<Color<f32>>, - ambient_map: Option<TextureId>, - diffuse_map: Option<TextureId>, - specular_map: Option<TextureId>, - textures: Vec<Texture>, + ambient: Color<f32>, + diffuse: Color<f32>, + specular: Color<f32>, + ambient_map: Option<Texture>, + diffuse_map: Option<Texture>, + specular_map: Option<Texture>, shininess: f32, } impl Builder { #[must_use] - pub fn new() -> Self + pub const fn new() -> Self { Self { - ambient: None, - diffuse: None, - specular: None, + ambient: Color::WHITE_F32, + diffuse: Color::WHITE_F32, + specular: Color::WHITE_F32, ambient_map: None, diffuse_map: None, specular_map: None, - textures: Vec::new(), shininess: 32.0, } } @@ -61,7 +67,7 @@ impl Builder #[must_use] pub fn ambient(mut self, ambient: Color<f32>) -> Self { - self.ambient = Some(ambient); + self.ambient = ambient; self } @@ -69,7 +75,7 @@ impl Builder #[must_use] pub fn diffuse(mut self, diffuse: Color<f32>) -> Self { - self.diffuse = Some(diffuse); + self.diffuse = diffuse; self } @@ -77,13 +83,13 @@ impl Builder #[must_use] pub fn specular(mut self, specular: Color<f32>) -> Self { - self.specular = Some(specular); + self.specular = specular; self } #[must_use] - pub fn ambient_map(mut self, ambient_map: TextureId) -> Self + pub fn ambient_map(mut self, ambient_map: Texture) -> Self { self.ambient_map = Some(ambient_map); @@ -91,7 +97,7 @@ impl Builder } #[must_use] - pub fn diffuse_map(mut self, diffuse_map: TextureId) -> Self + pub fn diffuse_map(mut self, diffuse_map: Texture) -> Self { self.diffuse_map = Some(diffuse_map); @@ -99,7 +105,7 @@ impl Builder } #[must_use] - pub fn specular_map(mut self, specular_map: TextureId) -> Self + pub fn specular_map(mut self, specular_map: Texture) -> Self { self.specular_map = Some(specular_map); @@ -107,22 +113,6 @@ impl Builder } #[must_use] - pub fn textures(mut self, textures: impl IntoIterator<Item = Texture>) -> Self - { - self.textures = textures.into_iter().collect(); - - self - } - - #[must_use] - pub fn texture(mut self, texture: Texture) -> Self - { - self.textures.push(texture); - - self - } - - #[must_use] pub fn shininess(mut self, shininess: f32) -> Self { self.shininess = shininess; @@ -135,43 +125,15 @@ impl Builder /// # Panics /// Will panic if no ambient map, diffuse map or specular map is set. #[must_use] - pub fn build(mut self) -> Material + pub const fn build(self) -> Material { - let ambient_map = self.ambient_map.unwrap_or_else(|| { - let texture = create_1x1_white_texture(); - let texture_id = texture.id(); - - self.textures.push(texture); - - texture_id - }); - - let diffuse_map = self.diffuse_map.unwrap_or_else(|| { - let texture = create_1x1_white_texture(); - let texture_id = texture.id(); - - self.textures.push(texture); - - texture_id - }); - - let specular_map = self.specular_map.unwrap_or_else(|| { - let texture = create_1x1_white_texture(); - let texture_id = texture.id(); - - self.textures.push(texture); - - texture_id - }); - Material { - ambient: self.ambient.unwrap_or(Color::WHITE_F32), - diffuse: self.diffuse.unwrap_or(Color::WHITE_F32), - specular: self.specular.unwrap_or(Color::WHITE_F32), - ambient_map, - diffuse_map, - specular_map, - textures: self.textures, + ambient: self.ambient, + diffuse: self.diffuse, + specular: self.specular, + ambient_map: self.ambient_map, + diffuse_map: self.diffuse_map, + specular_map: self.specular_map, shininess: self.shininess, } } @@ -187,8 +149,8 @@ impl Default for Builder builder! { /// Material flags. -#[builder(name = FlagsBuilder, derives = (Debug, Default, Clone))] -#[derive(Debug, Default, Clone, Component)] +#[builder(name = FlagsBuilder, derives = (Debug, Clone))] +#[derive(Debug, Clone, Component)] #[non_exhaustive] pub struct Flags { @@ -201,13 +163,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..1f53dad --- /dev/null +++ b/engine/src/material/asset.rs @@ -0,0 +1,90 @@ +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; +use crate::texture::Texture; + +#[derive(Debug, Clone)] +pub struct Map +{ + pub assets: HashMap<Cow<'static, str>, AssetHandle<Material>>, +} + +/// Material asset import settings. +#[derive(Debug)] +#[non_exhaustive] +pub struct Settings {} + +pub fn add_importers(assets: &mut Assets) +{ + assets.set_importer(["mtl"], import_wavefront_mtl_asset); +} + +fn import_wavefront_mtl_asset( + asset_submitter: &mut AssetSubmitter<'_>, + path: &Path, + _settings: Option<&'_ Settings>, +) -> Result<(), Error> +{ + let named_materials = crate::file_format::wavefront::mtl::parse( + &read_to_string(path) + .map_err(|err| Error::ReadFailed(err, path.to_path_buf()))?, + )?; + + let mut mat_asset_map = Map { + assets: HashMap::with_capacity(named_materials.len()), + }; + + for material in named_materials { + let mut material_builder = Material::builder() + .ambient(material.ambient) + .diffuse(material.diffuse) + .specular(material.specular) + .shininess(material.shininess); + + if let Some(ambient_map) = material.ambient_map { + material_builder = material_builder.ambient_map(Texture::new( + asset_submitter.submit_load_other(ambient_map.path.as_path()), + )); + } + + if let Some(diffuse_map) = material.diffuse_map { + material_builder = material_builder.diffuse_map(Texture::new( + asset_submitter.submit_load_other(diffuse_map.path.as_path()), + )); + } + + if let Some(specular_map) = material.specular_map { + material_builder = material_builder.specular_map(Texture::new( + asset_submitter.submit_load_other(specular_map.path.as_path()), + )); + } + + let material_name = material.name; + let material = material_builder.build(); + + let material_asset = + asset_submitter.submit_store_named(material_name.clone(), material); + + mat_asset_map + .assets + .insert(material_name.into(), material_asset); + } + + asset_submitter.submit_store(mat_asset_map); + + Ok(()) +} + +#[derive(Debug, thiserror::Error)] +enum Error +{ + #[error("Failed to read file {}", .1.display())] + ReadFailed(#[source] std::io::Error, PathBuf), + + #[error(transparent)] + Other(#[from] crate::file_format::wavefront::mtl::Error), +} diff --git a/engine/src/mesh.rs b/engine/src/mesh.rs index 91d199e..fb977af 100644 --- a/engine/src/mesh.rs +++ b/engine/src/mesh.rs @@ -1,11 +1,9 @@ -use ecs::Component; - -use crate::util::builder; +use crate::builder; use crate::vector::{Vec2, Vec3}; pub mod cube; -#[derive(Debug, Clone, Component)] +#[derive(Debug, Clone, Default)] pub struct Mesh { vertices: Vec<Vertex>, diff --git a/engine/src/mesh/cube.rs b/engine/src/mesh/cube.rs index 4d2c470..e91cf0e 100644 --- a/engine/src/mesh/cube.rs +++ b/engine/src/mesh/cube.rs @@ -1,7 +1,7 @@ use crate::data_types::dimens::Dimens3; use crate::math::calc_triangle_surface_normal; use crate::mesh::{Mesh, Vertex}; -use crate::util::builder; +use crate::builder; use crate::vector::{Vec2, Vec3}; builder! { diff --git a/engine/src/model.rs b/engine/src/model.rs new file mode 100644 index 0000000..ebf623f --- /dev/null +++ b/engine/src/model.rs @@ -0,0 +1,193 @@ +use std::borrow::Cow; +use std::collections::HashMap; + +use ecs::Component; + +use crate::asset::{Assets, Handle as AssetHandle}; +use crate::material::Material; +use crate::material::asset::Map as MaterialAssetMap; +use crate::mesh::Mesh; + +pub mod asset; + +#[derive(Debug, Clone, Component)] +#[non_exhaustive] +pub struct Model +{ + pub spec_asset: AssetHandle<Spec>, +} + +impl Model +{ + pub fn new(asset_handle: AssetHandle<Spec>) -> Self + { + Self { spec_asset: asset_handle } + } +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct Spec +{ + pub mesh_asset: Option<AssetHandle<Mesh>>, + pub materials: Materials, + pub material_names: Vec<Cow<'static, str>>, +} + +impl Spec +{ + pub fn builder() -> SpecBuilder + { + SpecBuilder::default() + } + + pub fn find_first_material<'assets>( + &'assets self, + assets: &'assets Assets, + ) -> MaterialSearchResult<'assets> + { + let Some(material_name) = self.material_names.first() else { + return MaterialSearchResult::NoMaterials; + }; + + let material_asset = match &self.materials { + Materials::Maps(material_asset_map_assets) => material_asset_map_assets + .iter() + .find_map(|mat_asset_map_asset| { + let mat_asset_map = assets.get(mat_asset_map_asset)?; + + mat_asset_map.assets.get(material_name) + }), + Materials::Direct(material_assets) => material_assets.get(material_name), + }; + + let Some(material_asset) = material_asset else { + return MaterialSearchResult::NotFound; + }; + + if assets.get(material_asset).is_none() { + tracing::trace!("Missing material asset"); + return MaterialSearchResult::NotFound; + } + + MaterialSearchResult::Found(material_asset) + } +} + +#[derive(Debug, Default, Clone)] +pub struct SpecBuilder +{ + mesh_asset: Option<AssetHandle<Mesh>>, + materials: Materials, + material_names: Vec<Cow<'static, str>>, +} + +impl SpecBuilder +{ + pub fn mesh(mut self, asset: AssetHandle<Mesh>) -> Self + { + self.mesh_asset = Some(asset); + + self + } + + pub fn materials(mut self, materials: Materials) -> Self + { + self.materials = materials; + + self + } + + pub fn material_name(mut self, material_name: impl Into<Cow<'static, str>>) -> Self + { + self.material_names.push(material_name.into()); + + self + } + + pub fn material_names<MaterialName>( + mut self, + material_names: impl IntoIterator<Item = MaterialName>, + ) -> Self + where + MaterialName: Into<Cow<'static, str>>, + { + self.material_names + .extend(material_names.into_iter().map(|mat_name| mat_name.into())); + + self + } + + #[tracing::instrument(skip_all)] + pub fn build(self) -> Spec + { + if !self.materials.is_empty() && self.material_names.is_empty() { + tracing::warn!("Model spec will have materials but no material names"); + } + + if self.materials.is_empty() && !self.material_names.is_empty() { + tracing::warn!("Model spec will have material names but no materials"); + } + + Spec { + mesh_asset: self.mesh_asset, + materials: self.materials, + material_names: self.material_names, + } + } +} + +#[derive(Debug, Clone)] +pub enum Materials +{ + Direct(HashMap<Cow<'static, str>, AssetHandle<Material>>), + Maps(Vec<AssetHandle<MaterialAssetMap>>), +} + +impl Materials +{ + pub fn direct<MaterialName>( + material_assets: impl IntoIterator<Item = (MaterialName, AssetHandle<Material>)>, + ) -> Self + where + MaterialName: Into<Cow<'static, str>>, + { + Self::Direct( + material_assets + .into_iter() + .map(|(material_name, mat_asset)| (material_name.into(), mat_asset)) + .collect(), + ) + } + + pub fn is_empty(&self) -> bool + { + match self { + Self::Direct(material_assets) => material_assets.is_empty(), + Self::Maps(material_asset_map_assets) => material_asset_map_assets.is_empty(), + } + } + + pub fn len(&self) -> usize + { + match self { + Self::Direct(material_assets) => material_assets.len(), + Self::Maps(material_asset_map_assets) => material_asset_map_assets.len(), + } + } +} + +impl Default for Materials +{ + fn default() -> Self + { + Self::Maps(Vec::new()) + } +} + +pub enum MaterialSearchResult<'a> +{ + Found(&'a AssetHandle<Material>), + NotFound, + NoMaterials, +} diff --git a/engine/src/model/asset.rs b/engine/src/model/asset.rs new file mode 100644 index 0000000..070200d --- /dev/null +++ b/engine/src/model/asset.rs @@ -0,0 +1,82 @@ +use std::collections::HashSet; +use std::fs::read_to_string; +use std::path::{Path, PathBuf}; + +use crate::asset::{Assets, Submitter as AssetSubmitter}; +use crate::material::asset::Map as MaterialAssetMap; +use crate::model::{Materials, Spec}; + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct Settings {} + +pub fn add_importers(assets: &mut Assets) +{ + assets.set_importer(["obj"], import_wavefront_obj_asset); +} + +fn import_wavefront_obj_asset( + asset_submitter: &mut AssetSubmitter<'_>, + path: &Path, + _settings: Option<&'_ Settings>, +) -> Result<(), Error> +{ + let obj = crate::file_format::wavefront::obj::parse( + &read_to_string(path) + .map_err(|err| Error::ReadFailed(err, path.to_path_buf()))?, + )?; + + let mesh = obj.to_mesh()?; + + let mesh_asset = asset_submitter.submit_store_named("mesh", mesh); + + let mut material_asset_map_assets = + Vec::with_capacity(obj.mtl_libs.iter().flatten().count()); + + for mtl_lib_path in obj.mtl_libs.iter().flatten() { + let mtl_lib_asset = + asset_submitter.submit_load_other::<MaterialAssetMap>(mtl_lib_path.as_path()); + + material_asset_map_assets.push(mtl_lib_asset); + } + + let material_names = obj + .faces + .into_iter() + .map(|face| face.material_name) + .flatten() + .fold( + (HashSet::<String>::new(), Vec::<String>::new()), + |(mut pushed_mat_names, mut unique_mat_names), material_name| { + if pushed_mat_names.contains(&material_name) { + return (pushed_mat_names, unique_mat_names); + } + + unique_mat_names.push(material_name.clone()); + pushed_mat_names.insert(material_name); + + (pushed_mat_names, unique_mat_names) + }, + ) + .1; + + asset_submitter.submit_store( + Spec::builder() + .mesh(mesh_asset) + .materials(Materials::Maps(material_asset_map_assets)) + .material_names(material_names) + .build(), + ); + + Ok(()) +} + +#[derive(Debug, thiserror::Error)] +enum Error +{ + #[error("Failed to read file {}", .1.display())] + ReadFailed(#[source] std::io::Error, PathBuf), + + #[error(transparent)] + Other(#[from] crate::file_format::wavefront::obj::Error), +} diff --git a/engine/src/opengl/buffer.rs b/engine/src/opengl/buffer.rs deleted file mode 100644 index eded553..0000000 --- a/engine/src/opengl/buffer.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::marker::PhantomData; -use std::mem::size_of_val; -use std::ptr::null; - -#[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 store_mapped<Value>( - &mut self, - values: &[Value], - usage: Usage, - mut map_func: impl FnMut(&Value) -> Item, - ) - { - unsafe { - #[allow(clippy::cast_possible_wrap)] - gl::NamedBufferData( - self.buf, - (size_of::<Item>() * values.len()) as gl::types::GLsizeiptr, - null(), - usage.into_gl(), - ); - } - - for (index, value) in values.iter().enumerate() { - let item = map_func(value); - - unsafe { - gl::NamedBufferSubData( - self.buf, - (index * size_of::<Item>()) as gl::types::GLintptr, - size_of::<Item>() as gl::types::GLsizeiptr, - (&raw const item).cast(), - ); - } - } - } - - pub fn object(&self) -> gl::types::GLuint - { - self.buf - } -} - -/// Buffer usage. -#[derive(Debug)] -#[allow(dead_code)] -pub enum Usage -{ - /// The buffer data is set only once and used by the GPU at most a few times. - Stream, - - /// The buffer data is set only once and used many times. - Static, - - /// The buffer data is changed a lot and used many times. - Dynamic, -} - -impl Usage -{ - fn into_gl(self) -> gl::types::GLenum - { - match self { - Self::Stream => gl::STREAM_DRAW, - Self::Static => gl::STATIC_DRAW, - Self::Dynamic => gl::DYNAMIC_DRAW, - } - } -} diff --git a/engine/src/opengl/debug.rs b/engine/src/opengl/debug.rs deleted file mode 100644 index 203590a..0000000 --- a/engine/src/opengl/debug.rs +++ /dev/null @@ -1,145 +0,0 @@ -use std::ffi::c_void; -use std::io::{stderr, Write}; -use std::panic::catch_unwind; -use std::ptr::null_mut; -use std::sync::Mutex; - -use crate::opengl::util::gl_enum; - -pub type MessageCallback = fn( - source: MessageSource, - ty: MessageType, - id: u32, - severity: MessageSeverity, - message: &str, -); - -pub fn enable_debug_output() -{ - unsafe { - gl::Enable(gl::DEBUG_OUTPUT); - gl::Enable(gl::DEBUG_OUTPUT_SYNCHRONOUS); - } -} - -pub fn set_debug_message_callback(cb: MessageCallback) -{ - *DEBUG_MESSAGE_CB.lock().unwrap() = Some(cb); - - unsafe { - gl::DebugMessageCallback(Some(debug_message_cb), null_mut()); - } -} - -pub fn set_debug_message_control( - source: Option<MessageSource>, - ty: Option<MessageType>, - severity: Option<MessageSeverity>, - ids: &[u32], - ids_action: MessageIdsAction, -) -{ - // Ids shouldn't realistically be large enough to cause a panic here - let ids_len: i32 = ids.len().try_into().unwrap(); - - unsafe { - gl::DebugMessageControl( - source.map_or(gl::DONT_CARE, |source| source as u32), - ty.map_or(gl::DONT_CARE, |ty| ty as u32), - severity.map_or(gl::DONT_CARE, |severity| severity as u32), - ids_len, - ids.as_ptr(), - ids_action as u8, - ); - } -} - -#[derive(Debug, Clone, Copy)] -#[allow(dead_code)] -pub enum MessageIdsAction -{ - Enable = 1, - Disable = 0, -} - -gl_enum! { -pub enum MessageSource -{ - Api = gl::DEBUG_SOURCE_API, - WindowSystem = gl::DEBUG_SOURCE_WINDOW_SYSTEM, - ShaderCompiler = gl::DEBUG_SOURCE_SHADER_COMPILER, - ThirdParty = gl::DEBUG_SOURCE_THIRD_PARTY, - Application = gl::DEBUG_SOURCE_APPLICATION, - Other = gl::DEBUG_SOURCE_OTHER, -} -} - -gl_enum! { -pub enum MessageType -{ - DeprecatedBehavior = gl::DEBUG_TYPE_DEPRECATED_BEHAVIOR, - Error = gl::DEBUG_TYPE_ERROR, - Marker = gl::DEBUG_TYPE_MARKER, - Other = gl::DEBUG_TYPE_OTHER, - Performance = gl::DEBUG_TYPE_PERFORMANCE, - PopGroup = gl::DEBUG_TYPE_POP_GROUP, - PushGroup = gl::DEBUG_TYPE_PUSH_GROUP, - Portability = gl::DEBUG_TYPE_PORTABILITY, - UndefinedBehavior = gl::DEBUG_TYPE_UNDEFINED_BEHAVIOR, -} -} - -gl_enum! { -pub enum MessageSeverity -{ - High = gl::DEBUG_SEVERITY_HIGH, - Medium = gl::DEBUG_SEVERITY_MEDIUM, - Low = gl::DEBUG_SEVERITY_LOW, - Notification = gl::DEBUG_SEVERITY_NOTIFICATION, -} -} - -static DEBUG_MESSAGE_CB: Mutex<Option<MessageCallback>> = Mutex::new(None); - -extern "system" fn debug_message_cb( - source: gl::types::GLenum, - ty: gl::types::GLenum, - id: gl::types::GLuint, - severity: gl::types::GLenum, - message_length: gl::types::GLsizei, - message: *const gl::types::GLchar, - _user_param: *mut c_void, -) -{ - // Unwinds are catched because unwinding from Rust code into foreign code is UB. - let res = catch_unwind(|| { - let cb_lock = DEBUG_MESSAGE_CB.lock().unwrap(); - - if let Some(cb) = *cb_lock { - let msg_source = MessageSource::from_gl(source).unwrap(); - let msg_type = MessageType::from_gl(ty).unwrap(); - let msg_severity = MessageSeverity::from_gl(severity).unwrap(); - - let msg_length = usize::try_from(message_length).unwrap(); - - // SAFETY: The received message should be a valid ASCII string - let message = unsafe { - std::str::from_utf8_unchecked(std::slice::from_raw_parts( - message.cast(), - msg_length, - )) - }; - - cb(msg_source, msg_type, id, msg_severity, message); - } - }); - - if res.is_err() { - // eprintln is not used since it can panic and unwinds are unwanted because - // unwinding from Rust code into foreign code is UB. - stderr() - .write_all(b"ERROR: Panic in debug message callback") - .ok(); - println!(); - } -} diff --git a/engine/src/opengl/glsl.rs b/engine/src/opengl/glsl.rs deleted file mode 100644 index 6fd5638..0000000 --- a/engine/src/opengl/glsl.rs +++ /dev/null @@ -1,616 +0,0 @@ -use std::borrow::Cow; -use std::fmt::{Display, Formatter}; -use std::path::{Path, PathBuf}; -use std::string::FromUtf8Error; - -const PREINCLUDE_DIRECTIVE: &str = "#preinclude"; - -pub fn preprocess<'content>( - shader_content: impl Into<Cow<'content, str>>, - read_file: &impl Fn(&Path) -> Result<Vec<u8>, std::io::Error>, -) -> Result<Cow<'content, str>, PreprocessingError> -{ - do_preprocess(shader_content, SpanPath::Original, read_file) -} - -fn do_preprocess<'content>( - shader_content: impl Into<Cow<'content, str>>, - shader_path: SpanPath<'_>, - read_file: &impl Fn(&Path) -> Result<Vec<u8>, std::io::Error>, -) -> Result<Cow<'content, str>, PreprocessingError> -{ - let shader_content = shader_content.into(); - - let mut preincludes = shader_content - .match_indices(PREINCLUDE_DIRECTIVE) - .peekable(); - - if preincludes.peek().is_none() { - // Shader content contains no preincludes - return Ok(shader_content.into()); - }; - - let mut preprocessed = shader_content.to_string(); - - let mut curr = shader_content.find(PREINCLUDE_DIRECTIVE); - - let mut last_start = 0; - let mut span_line_offset = 0; - - while let Some(preinclude_start) = curr { - let replacement_job = handle_preinclude( - &preprocessed, - &shader_path, - preinclude_start, - span_line_offset, - )?; - - let path = replacement_job.path.clone(); - - let mut included = - String::from_utf8(read_file(&replacement_job.path).map_err(|err| { - PreprocessingError::ReadIncludedShaderFailed { - source: err, - path: replacement_job.path.clone(), - } - })?) - .map_err(|err| { - PreprocessingError::IncludedShaderInvalidUtf8 { - source: err, - path: path.clone(), - } - })?; - - if let Some(first_line) = included.lines().next() { - if first_line.starts_with("#version") { - included = included - .chars() - .skip_while(|character| *character != '\n') - .collect(); - } - } - - let included_preprocessed = do_preprocess( - &included, - SpanPath::Path(replacement_job.path.as_path().into()), - read_file, - )?; - - let start = replacement_job.start_index; - let end = replacement_job.end_index; - - preprocessed.replace_range(start..end, &included_preprocessed); - - curr = preprocessed[last_start + 1..] - .find(PREINCLUDE_DIRECTIVE) - .map(|index| index + 1); - - last_start = preinclude_start + included_preprocessed.len(); - - span_line_offset += included_preprocessed.lines().count(); - } - - Ok(preprocessed.into()) -} - -fn handle_preinclude( - shader_content: &str, - shader_path: &SpanPath<'_>, - preinclude_start_index: usize, - span_line_offset: usize, -) -> Result<ReplacementJob, PreprocessingError> -{ - let expect_token = |token: char, index: usize| { - let token_found = shader_content.chars().nth(index).ok_or_else(|| { - PreprocessingError::ExpectedToken { - expected: token, - span: Span::new( - shader_content, - shader_path.to_owned(), - index, - span_line_offset, - preinclude_start_index, - ), - } - })?; - - if token_found != token { - return Err(PreprocessingError::InvalidToken { - expected: token, - found: token_found, - span: Span::new( - shader_content, - shader_path.to_owned(), - index, - span_line_offset, - preinclude_start_index, - ), - }); - } - - Ok(()) - }; - - let space_index = preinclude_start_index + PREINCLUDE_DIRECTIVE.len(); - let quote_open_index = space_index + 1; - - expect_token(' ', space_index)?; - expect_token('"', quote_open_index)?; - - let buf = shader_content[quote_open_index + 1..] - .chars() - .take_while(|character| *character != '"') - .map(|character| character as u8) - .collect::<Vec<_>>(); - - if buf.is_empty() { - return Err(PreprocessingError::ExpectedToken { - expected: '"', - span: Span::new( - shader_content, - shader_path.to_owned(), - shader_content.len() - 1, - span_line_offset, - preinclude_start_index, - ), - }); - } - - let path_len = buf.len(); - - let path = PathBuf::from(String::from_utf8(buf).map_err(|err| { - PreprocessingError::PreincludePathInvalidUtf8 { - source: err, - span: Span::new( - shader_content, - shader_path.to_owned(), - quote_open_index + 1, - span_line_offset, - preinclude_start_index, - ), - } - })?); - - Ok(ReplacementJob { - start_index: preinclude_start_index, - end_index: quote_open_index + 1 + path_len + 1, - path, - }) -} - -struct ReplacementJob -{ - start_index: usize, - end_index: usize, - path: PathBuf, -} - -#[derive(Debug, thiserror::Error)] -pub enum PreprocessingError -{ - #[error( - "Invalid token at line {}, column {} of {}. Expected '{}', found '{}'", - span.line, - span.column, - span.path, - expected, - found - )] - InvalidToken - { - expected: char, - found: char, - span: Span, - }, - - #[error( - "Expected token '{}' at line {}, column {} of {}. Found eof", - expected, - span.line, - span.column, - span.path - )] - ExpectedToken - { - expected: char, span: Span - }, - - #[error( - "Preinclude path at line {}, column {} of {} is invalid UTF-8", - span.line, - span.column, - span.path - )] - PreincludePathInvalidUtf8 - { - #[source] - source: FromUtf8Error, - span: Span, - }, - - #[error("Failed to read included shader")] - ReadIncludedShaderFailed - { - #[source] - source: std::io::Error, - path: PathBuf, - }, - - #[error("Included shader is not valid UTF-8")] - IncludedShaderInvalidUtf8 - { - #[source] - source: FromUtf8Error, - path: PathBuf, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub struct Span -{ - pub line: usize, - pub column: usize, - pub path: SpanPath<'static>, -} - -impl Span -{ - fn new( - file_content: &str, - path: SpanPath<'static>, - char_index: usize, - line_offset: usize, - line_start_index: usize, - ) -> Self - { - let line = find_line_of_index(file_content, char_index) + 1 - - line_offset.saturating_sub(1); - - Self { - line, - column: char_index - line_start_index + 1, - path, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SpanPath<'a> -{ - Original, - Path(Cow<'a, Path>), -} - -impl<'a> SpanPath<'a> -{ - fn to_owned(&self) -> SpanPath<'static> - { - match self { - Self::Original => SpanPath::Original, - Self::Path(path) => SpanPath::Path(Cow::Owned(path.to_path_buf().into())), - } - } -} - -impl<'a> Display for SpanPath<'a> -{ - fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result - { - match self { - Self::Original => write!(formatter, "original file"), - Self::Path(path) => write!(formatter, "file {}", path.display()), - } - } -} - -impl<'a, PathLike> PartialEq<PathLike> for SpanPath<'a> -where - PathLike: AsRef<Path>, -{ - fn eq(&self, other: &PathLike) -> bool - { - match self { - Self::Original => false, - Self::Path(path) => path == other.as_ref(), - } - } -} - -fn find_line_of_index(text: &str, index: usize) -> usize -{ - text.chars() - .take(index + 1) - .enumerate() - .filter(|(_, character)| *character == '\n') - .count() -} - -#[cfg(test)] -mod tests -{ - use std::ffi::OsStr; - use std::path::Path; - - use super::{preprocess, PreprocessingError}; - use crate::opengl::glsl::SpanPath; - - #[test] - fn preprocess_no_directives_is_same() - { - assert_eq!( - preprocess("#version 330 core\n", &|_| { unreachable!() }).unwrap(), - "#version 330 core\n" - ); - } - - #[test] - fn preprocess_with_directives_works() - { - assert_eq!( - preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"foo.glsl\"\n", - "\n", - "void main() {}", - ), - &|_| { Ok(b"out vec4 FragColor;".to_vec()) } - ) - .unwrap(), - concat!( - "#version 330 core\n", - "\n", - "out vec4 FragColor;\n", - "\n", - "void main() {}", - ) - ); - - assert_eq!( - preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ), - &|_| { Ok(b"out vec4 FragColor;".to_vec()) } - ) - .unwrap(), - concat!( - "#version 330 core\n", - "\n", - "out vec4 FragColor;\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ) - ); - - assert_eq!( - preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "#preinclude \"foo.glsl\"\n", - "\n", - "void main() {}", - ), - &|path| { - if path == OsStr::new("bar.glsl") { - Ok(b"out vec4 FragColor;".to_vec()) - } else { - Ok(concat!( - "uniform sampler2D input_texture;\n", - "in vec2 in_texture_coords;" - ) - .as_bytes() - .to_vec()) - } - }, - ) - .unwrap(), - concat!( - "#version 330 core\n", - "\n", - "out vec4 FragColor;\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "uniform sampler2D input_texture;\n", - "in vec2 in_texture_coords;\n", - "\n", - "void main() {}", - ) - ); - } - - #[test] - fn preprocess_invalid_directive_does_not_work() - { - let res = preprocess( - concat!( - "#version 330 core\n", - "\n", - // Missing " - "#preinclude foo.glsl\"\n", - "\n", - "void main() {}", - ), - &|_| Ok(b"out vec4 FragColor;".to_vec()), - ); - - let Err(PreprocessingError::InvalidToken { expected, found, span }) = res else { - panic!( - "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}" - ); - }; - - assert_eq!(expected, '"'); - assert_eq!(found, 'f'); - assert_eq!(span.line, 3); - assert_eq!(span.column, 13); - assert_eq!(span.path, SpanPath::Original); - } - - #[test] - fn preprocess_error_has_correct_span() - { - let res = preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "#preinclude \"foo.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ), - &|path| { - if path == OsStr::new("bar.glsl") { - Ok(concat!( - "out vec4 FragColor;\n", - "in vec2 in_texture_coords;\n", - "in float foo;" - ) - .as_bytes() - .to_vec()) - } else if path == OsStr::new("foo.glsl") { - Ok(concat!( - "uniform sampler2D input_texture;\n", - "\n", - // Missing space before first " - "#preinclude\"shared_types.glsl\"\n", - ) - .as_bytes() - .to_vec()) - } else { - panic!(concat!( - "Expected read function to be called with ", - "either path bar.glsl or foo.glsl" - )); - } - }, - ); - - let Err(PreprocessingError::InvalidToken { expected, found, span }) = res else { - panic!( - "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}" - ); - }; - - assert_eq!(expected, ' '); - assert_eq!(found, '"'); - assert_eq!(span.line, 3); - assert_eq!(span.column, 12); - assert_eq!(span.path, SpanPath::Path(Path::new("foo.glsl").into())); - } - - #[test] - fn preprocess_included_shader_with_include_works() - { - assert_eq!( - preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ), - &|path| { - if path == OsStr::new("bar.glsl") { - Ok(concat!( - "#preinclude \"foo.glsl\"\n", - "\n", - "out vec4 FragColor;" - ) - .as_bytes() - .to_vec()) - } else { - Ok(concat!( - "uniform sampler2D input_texture;\n", - "in vec2 in_texture_coords;" - ) - .as_bytes() - .to_vec()) - } - } - ) - .unwrap(), - concat!( - "#version 330 core\n", - "\n", - "uniform sampler2D input_texture;\n", - "in vec2 in_texture_coords;\n", - "\n", - "out vec4 FragColor;\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ) - ); - } - - #[test] - fn preprocess_included_shader_with_include_error_span_is_correct() - { - let res = preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ), - &|path| { - if path == OsStr::new("bar.glsl") { - Ok(concat!( - // ' instead of " - "#preinclude 'foo.glsl\"\n", - "\n", - "out vec4 FragColor;" - ) - .as_bytes() - .to_vec()) - } else { - Ok(concat!( - "uniform sampler2D input_texture;\n", - "in vec2 in_texture_coords;" - ) - .as_bytes() - .to_vec()) - } - }, - ); - - let Err(PreprocessingError::InvalidToken { expected, found, span }) = res else { - panic!( - "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}" - ); - }; - - assert_eq!(expected, '"'); - assert_eq!(found, '\''); - assert_eq!(span.line, 1); - assert_eq!(span.column, 13); - assert_eq!(span.path, Path::new("bar.glsl")); - } -} diff --git a/engine/src/opengl/mod.rs b/engine/src/opengl/mod.rs deleted file mode 100644 index 53e0120..0000000 --- a/engine/src/opengl/mod.rs +++ /dev/null @@ -1,128 +0,0 @@ -use bitflags::bitflags; -use gl::types::GLint; - -use crate::data_types::dimens::Dimens; -use crate::vector::Vec2; - -pub mod buffer; -pub mod glsl; -pub mod shader; -pub mod texture; -pub mod vertex_array; - -mod util; - -pub mod debug; - -pub fn set_viewport(position: Vec2<u32>, size: Dimens<u32>) -{ - unsafe { - #[allow(clippy::cast_possible_wrap)] - gl::Viewport( - position.x as i32, - position.y as i32, - size.width as i32, - size.height as i32, - ); - } -} - -pub fn clear_buffers(mask: BufferClearMask) -{ - unsafe { - gl::Clear(mask.bits()); - } -} - -pub fn set_polygon_mode(face: impl Into<PolygonModeFace>, mode: impl Into<PolygonMode>) -{ - unsafe { - gl::PolygonMode(face.into() as u32, mode.into() as u32); - } -} - -pub fn enable(capacity: Capability) -{ - unsafe { - gl::Enable(capacity as u32); - } -} - -pub fn get_context_flags() -> ContextFlags -{ - let mut context_flags: GLint = 0; - - unsafe { - gl::GetIntegerv(gl::CONTEXT_FLAGS as u32, &mut context_flags); - } - - ContextFlags::from_bits_truncate(context_flags as u32) -} - -bitflags! { - #[derive(Debug, Clone, Copy)] - pub struct BufferClearMask: u32 { - const COLOR = gl::COLOR_BUFFER_BIT; - const DEPTH = gl::DEPTH_BUFFER_BIT; - const STENCIL = gl::STENCIL_BUFFER_BIT; - } -} - -#[derive(Debug)] -#[repr(u32)] -pub enum Capability -{ - DepthTest = gl::DEPTH_TEST, - MultiSample = gl::MULTISAMPLE, -} - -#[derive(Debug)] -#[repr(u32)] -pub enum PolygonMode -{ - Point = gl::POINT, - Line = gl::LINE, - Fill = gl::FILL, -} - -impl From<crate::draw_flags::PolygonMode> for PolygonMode -{ - fn from(mode: crate::draw_flags::PolygonMode) -> Self - { - match mode { - crate::draw_flags::PolygonMode::Point => Self::Point, - crate::draw_flags::PolygonMode::Fill => Self::Fill, - crate::draw_flags::PolygonMode::Line => Self::Line, - } - } -} - -#[derive(Debug)] -#[repr(u32)] -pub enum PolygonModeFace -{ - Front = gl::FRONT, - Back = gl::BACK, - FrontAndBack = gl::FRONT_AND_BACK, -} - -impl From<crate::draw_flags::PolygonModeFace> for PolygonModeFace -{ - fn from(face: crate::draw_flags::PolygonModeFace) -> Self - { - match face { - crate::draw_flags::PolygonModeFace::Front => Self::Front, - crate::draw_flags::PolygonModeFace::Back => Self::Back, - crate::draw_flags::PolygonModeFace::FrontAndBack => Self::FrontAndBack, - } - } -} - -bitflags! { -#[derive(Debug, Clone, Copy)] -pub struct ContextFlags: u32 { - const FORWARD_COMPATIBLE = gl::CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT; - const DEBUG = gl::CONTEXT_FLAG_DEBUG_BIT; - const ROBUST_ACCESS = gl::CONTEXT_FLAG_ROBUST_ACCESS_BIT; -} -} diff --git a/engine/src/opengl/shader.rs b/engine/src/opengl/shader.rs deleted file mode 100644 index a626fc7..0000000 --- a/engine/src/opengl/shader.rs +++ /dev/null @@ -1,270 +0,0 @@ -use std::ffi::CStr; -use std::ptr::null_mut; - -use crate::matrix::Matrix; -use crate::vector::Vec3; - -#[derive(Debug)] -pub struct Shader -{ - shader: gl::types::GLuint, -} - -impl Shader -{ - pub fn new(kind: Kind) -> Self - { - let shader = unsafe { gl::CreateShader(kind.into_gl()) }; - - Self { shader } - } - - pub fn set_source(&mut self, source: &str) -> Result<(), Error> - { - if !source.is_ascii() { - return Err(Error::SourceNotAscii); - } - - unsafe { - #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] - gl::ShaderSource( - self.shader, - 1, - &source.as_ptr().cast(), - &(source.len() as gl::types::GLint), - ); - } - - Ok(()) - } - - pub fn compile(&mut self) -> Result<(), Error> - { - unsafe { - gl::CompileShader(self.shader); - } - - let mut compile_success = gl::types::GLint::default(); - - unsafe { - gl::GetShaderiv(self.shader, gl::COMPILE_STATUS, &mut compile_success); - } - - if compile_success == 0 { - let info_log = self.get_info_log(); - - return Err(Error::CompileFailed(info_log)); - } - - Ok(()) - } - - fn get_info_log(&self) -> String - { - let mut buf = vec![gl::types::GLchar::default(); 512]; - - unsafe { - #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] - gl::GetShaderInfoLog( - self.shader, - buf.len() as gl::types::GLsizei, - null_mut(), - buf.as_mut_ptr(), - ); - } - - let info_log = unsafe { CStr::from_ptr(buf.as_ptr()) }; - - unsafe { String::from_utf8_unchecked(info_log.to_bytes().to_vec()) } - } -} - -impl Drop for Shader -{ - fn drop(&mut self) - { - unsafe { - gl::DeleteShader(self.shader); - } - } -} - -/// Shader kind. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Kind -{ - Vertex, - Fragment, -} - -impl Kind -{ - fn into_gl(self) -> gl::types::GLenum - { - match self { - Self::Vertex => gl::VERTEX_SHADER, - Self::Fragment => gl::FRAGMENT_SHADER, - } - } -} - -/// Shader program -#[derive(Debug, PartialEq, Eq, Hash)] -pub struct Program -{ - program: gl::types::GLuint, -} - -impl Program -{ - pub fn new() -> Self - { - let program = unsafe { gl::CreateProgram() }; - - Self { program } - } - - pub fn attach(&mut self, shader: &Shader) - { - unsafe { - gl::AttachShader(self.program, shader.shader); - } - } - - pub fn link(&mut self) -> Result<(), Error> - { - unsafe { - gl::LinkProgram(self.program); - } - - let mut link_success = gl::types::GLint::default(); - - unsafe { - gl::GetProgramiv(self.program, gl::LINK_STATUS, &mut link_success); - } - - if link_success == 0 { - let info_log = self.get_info_log(); - - return Err(Error::CompileFailed(info_log)); - } - - Ok(()) - } - - pub fn activate(&self) - { - unsafe { - gl::UseProgram(self.program); - } - } - - pub fn set_uniform(&mut self, name: &CStr, var: &impl UniformVariable) - { - let location = UniformLocation(unsafe { - gl::GetUniformLocation(self.program, name.as_ptr().cast()) - }); - - var.set(self, location); - } - - 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); - } - } -} - -pub trait UniformVariable -{ - fn set(&self, program: &mut Program, uniform_location: UniformLocation); -} - -impl UniformVariable for f32 -{ - fn set(&self, program: &mut Program, uniform_location: UniformLocation) - { - unsafe { - gl::ProgramUniform1f(program.program, uniform_location.0, *self); - } - } -} - -impl UniformVariable for i32 -{ - fn set(&self, program: &mut Program, uniform_location: UniformLocation) - { - unsafe { - gl::ProgramUniform1i(program.program, uniform_location.0, *self); - } - } -} - -impl UniformVariable for Vec3<f32> -{ - fn set(&self, program: &mut Program, uniform_location: UniformLocation) - { - unsafe { - gl::ProgramUniform3f( - program.program, - uniform_location.0, - self.x, - self.y, - self.z, - ); - } - } -} - -impl UniformVariable for Matrix<f32, 4, 4> -{ - fn set(&self, program: &mut Program, uniform_location: UniformLocation) - { - unsafe { - gl::ProgramUniformMatrix4fv( - program.program, - uniform_location.0, - 1, - gl::FALSE, - self.as_ptr(), - ); - } - } -} - -#[derive(Debug)] -pub struct UniformLocation(gl::types::GLint); - -/// Shader error. -#[derive(Debug, thiserror::Error)] -pub enum Error -{ - #[error("All characters in source are not within the ASCII range")] - SourceNotAscii, - - #[error("Failed to compile: {0}")] - CompileFailed(String), -} diff --git a/engine/src/opengl/texture.rs b/engine/src/opengl/texture.rs deleted file mode 100644 index 52c8554..0000000 --- a/engine/src/opengl/texture.rs +++ /dev/null @@ -1,240 +0,0 @@ -use crate::data_types::dimens::Dimens; -use crate::texture::Properties; - -#[derive(Debug)] -pub struct Texture -{ - texture: gl::types::GLuint, -} - -impl Texture -{ - pub fn new() -> Self - { - let mut texture = gl::types::GLuint::default(); - - unsafe { - gl::CreateTextures(gl::TEXTURE_2D, 1, &mut texture); - }; - - Self { texture } - } - - pub fn bind(&self) - { - unsafe { - gl::BindTexture(gl::TEXTURE_2D, self.texture); - } - } - - pub fn generate( - &mut self, - dimens: Dimens<u32>, - data: &[u8], - pixel_data_format: PixelDataFormat, - ) - { - self.alloc_image(pixel_data_format, dimens, data); - - unsafe { - gl::GenerateTextureMipmap(self.texture); - } - } - - pub fn apply_properties(&mut self, properties: &Properties) - { - self.set_wrap(properties.wrap); - self.set_magnifying_filter(properties.magnifying_filter); - self.set_minifying_filter(properties.minifying_filter); - } - - pub fn set_wrap(&mut self, wrapping: Wrapping) - { - let wrapping_gl = wrapping.to_gl(); - - #[allow(clippy::cast_possible_wrap)] - unsafe { - gl::TextureParameteri(self.texture, gl::TEXTURE_WRAP_S, wrapping_gl as i32); - gl::TextureParameteri(self.texture, gl::TEXTURE_WRAP_T, wrapping_gl as i32); - } - } - - pub fn set_magnifying_filter(&mut self, filtering: Filtering) - { - let filtering_gl = filtering.to_gl(); - - #[allow(clippy::cast_possible_wrap)] - unsafe { - gl::TextureParameteri( - self.texture, - gl::TEXTURE_MAG_FILTER, - filtering_gl as i32, - ); - } - } - - pub fn set_minifying_filter(&mut self, filtering: Filtering) - { - let filtering_gl = filtering.to_gl(); - - #[allow(clippy::cast_possible_wrap)] - unsafe { - gl::TextureParameteri( - self.texture, - gl::TEXTURE_MIN_FILTER, - filtering_gl as i32, - ); - } - } - - fn alloc_image( - &mut self, - pixel_data_format: PixelDataFormat, - dimens: Dimens<u32>, - data: &[u8], - ) - { - unsafe { - #[allow(clippy::cast_possible_wrap)] - gl::TextureStorage2D( - self.texture, - 1, - pixel_data_format.to_sized_internal_format(), - dimens.width as i32, - dimens.height as i32, - ); - - #[allow(clippy::cast_possible_wrap)] - gl::TextureSubImage2D( - self.texture, - 0, - 0, - 0, - dimens.width as i32, - dimens.height as i32, - pixel_data_format.to_format(), - gl::UNSIGNED_BYTE, - data.as_ptr().cast(), - ); - } - } -} - -impl Drop for Texture -{ - fn drop(&mut self) - { - unsafe { - gl::DeleteTextures(1, &self.texture); - } - } -} - -/// Texture wrapping. -#[derive(Debug, Clone, Copy)] -pub enum Wrapping -{ - Repeat, - MirroredRepeat, - ClampToEdge, - ClampToBorder, -} - -impl Wrapping -{ - fn to_gl(self) -> gl::types::GLenum - { - match self { - Self::Repeat => gl::REPEAT, - Self::MirroredRepeat => gl::MIRRORED_REPEAT, - Self::ClampToEdge => gl::CLAMP_TO_EDGE, - Self::ClampToBorder => gl::CLAMP_TO_BORDER, - } - } -} - -#[derive(Debug, Clone, Copy)] -pub enum Filtering -{ - Nearest, - Linear, -} - -impl Filtering -{ - fn to_gl(self) -> gl::types::GLenum - { - match self { - Self::Linear => gl::LINEAR, - Self::Nearest => gl::NEAREST, - } - } -} - -/// Texture pixel data format. -#[derive(Debug, Clone, Copy)] -pub enum PixelDataFormat -{ - Rgb8, - Rgba8, -} - -impl PixelDataFormat -{ - fn to_sized_internal_format(self) -> gl::types::GLenum - { - match self { - Self::Rgb8 => gl::RGB8, - Self::Rgba8 => gl::RGBA8, - } - } - - fn to_format(self) -> gl::types::GLenum - { - match self { - Self::Rgb8 => gl::RGB, - Self::Rgba8 => gl::RGBA, - } - } -} - -pub fn set_active_texture_unit(texture_unit: TextureUnit) -{ - unsafe { - gl::ActiveTexture(texture_unit.into_gl()); - } -} - -macro_rules! texture_unit_enum { - (cnt=$cnt: literal) => { - seq_macro::seq!(N in 0..$cnt { - #[derive(Debug, Clone, Copy)] - pub enum TextureUnit { - #( - No~N, - )* - } - - impl TextureUnit { - fn into_gl(self) -> gl::types::GLenum { - match self { - #( - Self::No~N => gl::TEXTURE~N, - )* - } - } - - pub fn from_num(num: usize) -> Option<Self> { - match num { - #( - N => Some(Self::No~N), - )* - _ => None - } - } - } - }); - }; -} - -texture_unit_enum!(cnt = 31); diff --git a/engine/src/opengl/util.rs b/engine/src/opengl/util.rs deleted file mode 100644 index e60778f..0000000 --- a/engine/src/opengl/util.rs +++ /dev/null @@ -1,30 +0,0 @@ -// May only be used when certain crate features are enabled -#![allow(unused_macros, unused_imports)] - -macro_rules! gl_enum { - ( - $visibility: vis enum $name: ident - {$( - $variant: ident = gl::$gl_enum: ident, - )+} - ) => { - #[derive(Debug, Clone, Copy)] - #[repr(u32)] - $visibility enum $name - {$( - $variant = gl::$gl_enum, - )+} - - impl $name { - fn from_gl(num: gl::types::GLenum) -> Option<Self> - { - match num { - $(gl::$gl_enum => Some(Self::$variant),)+ - _ => None - } - } - } - }; -} - -pub(crate) use gl_enum; diff --git a/engine/src/opengl/vertex_array.rs b/engine/src/opengl/vertex_array.rs deleted file mode 100644 index 1f8a870..0000000 --- a/engine/src/opengl/vertex_array.rs +++ /dev/null @@ -1,158 +0,0 @@ -use std::mem::size_of; - -use crate::opengl::buffer::Buffer; - -#[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<VertexT>( - &mut self, - binding_index: u32, - vertex_buffer: &Buffer<VertexT>, - offset: isize, - ) - { - unsafe { - gl::VertexArrayVertexBuffer( - self.array, - binding_index, - vertex_buffer.object(), - offset, - size_of::<VertexT>() as i32, - ); - } - } - - 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) } - } -} - -#[derive(Debug)] -pub enum PrimitiveKind -{ - Triangles, -} - -impl PrimitiveKind -{ - fn into_gl(self) -> gl::types::GLenum - { - match self { - Self::Triangles => gl::TRIANGLES, - } - } -} - -#[derive(Debug, Clone, Copy)] -#[repr(u32)] -pub enum DataType -{ - Float = gl::FLOAT, -} - -impl DataType -{ - pub fn size(self) -> u32 - { - #[allow(clippy::cast_possible_truncation)] - match self { - Self::Float => size_of::<gl::types::GLfloat>() as u32, - } - } -} diff --git a/engine/src/projection.rs b/engine/src/projection.rs index faa741f..29636e7 100644 --- a/engine/src/projection.rs +++ b/engine/src/projection.rs @@ -1,9 +1,9 @@ +use crate::builder; use crate::data_types::dimens::Dimens3; use crate::matrix::Matrix; -use crate::util::builder; use crate::vector::Vec3; -#[derive(Debug)] +#[derive(Debug, Clone)] #[non_exhaustive] pub enum Projection { @@ -12,7 +12,7 @@ pub enum Projection } /// Perspective projection parameters. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Perspective { pub fov_radians: f32, diff --git a/engine/src/reflection.rs b/engine/src/reflection.rs new file mode 100644 index 0000000..5bd2aef --- /dev/null +++ b/engine/src/reflection.rs @@ -0,0 +1,147 @@ +use std::alloc::Layout; +use std::any::TypeId; + +pub use engine_macros::Reflection; + +pub trait With: 'static +{ + const REFLECTION: &Reflection; + + fn reflection() -> &'static Reflection + where + Self: Sized; + + fn get_reflection(&self) -> &'static Reflection; +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum Reflection +{ + Struct(Struct), + Array(Array), + Slice(Slice), + Literal, +} + +#[derive(Debug, Clone)] +pub struct Struct +{ + pub fields: &'static [StructField], +} + +#[derive(Debug, Clone)] +pub struct StructField +{ + pub name: &'static str, + pub index: usize, + pub layout: Layout, + pub byte_offset: usize, + pub type_id: TypeId, + pub type_name: &'static str, + pub reflection: &'static Reflection, +} + +#[derive(Debug, Clone)] +pub struct Array +{ + pub item_reflection: &'static Reflection, + pub length: usize, +} + +#[derive(Debug, Clone)] +pub struct Slice +{ + pub item_reflection: &'static Reflection, +} + +macro_rules! impl_with_for_literals { + ($($literal: ty),*) => { + $( + impl With for $literal + { + const REFLECTION: &Reflection = &Reflection::Literal; + + fn reflection() -> &'static Reflection + where + Self: Sized + { + Self::REFLECTION + } + + fn get_reflection(&self) -> &'static Reflection + { + Self::reflection() + } + } + )* + }; +} + +impl_with_for_literals!( + u8, + i8, + u16, + i16, + u32, + i32, + u64, + i64, + u128, + i128, + f32, + f64, + usize, + isize, + &'static str +); + +impl<T: With, const LEN: usize> With for [T; LEN] +{ + const REFLECTION: &Reflection = &Reflection::Array(Array { + item_reflection: T::REFLECTION, + length: LEN, + }); + + fn reflection() -> &'static Reflection + where + Self: Sized, + { + Self::REFLECTION + } + + fn get_reflection(&self) -> &'static Reflection + { + Self::reflection() + } +} + +impl<T: With> With for &'static [T] +{ + const REFLECTION: &Reflection = + &Reflection::Slice(Slice { item_reflection: T::REFLECTION }); + + fn reflection() -> &'static Reflection + where + Self: Sized, + { + Self::REFLECTION + } + + fn get_reflection(&self) -> &'static Reflection + { + Self::reflection() + } +} + +// Used by the Reflection derive macro +#[doc(hidden)] +pub mod __private +{ + pub const fn get_type_reflection<T>() -> &'static super::Reflection + where + T: super::With, + { + T::REFLECTION + } +} diff --git a/engine/src/renderer.rs b/engine/src/renderer.rs index 17bc925..6c20102 100644 --- a/engine/src/renderer.rs +++ b/engine/src/renderer.rs @@ -1,7 +1,443 @@ -use ecs::pair::{ChildOf, Pair}; -use ecs::phase::{Phase, UPDATE as UPDATE_PHASE}; -use ecs::static_entity; +use std::any::type_name; +use std::collections::VecDeque; +use std::path::Path; +use std::sync::LazyLock; +use std::sync::atomic::{AtomicU64, Ordering}; +use bitflags::bitflags; +use ecs::actions::Actions; +use ecs::pair::{ChildOf, Pair, Wildcard}; +use ecs::phase::{POST_UPDATE as POST_UPDATE_PHASE, Phase}; +use ecs::query::term::Without; +use ecs::sole::Single; +use ecs::{Component, Query, declare_entity}; + +use crate::asset::{Assets, Handle as AssetHandle, Label as AssetLabel}; +use crate::builder; +use crate::color::Color; +use crate::data_types::dimens::Dimens; +use crate::draw_flags::{DrawFlags, NoDraw, PolygonModeConfig}; +use crate::image::Image; +use crate::mesh::Mesh; +use crate::model::{MaterialSearchResult, Model}; +use crate::renderer::object::{Id as ObjectId, Store as ObjectStore}; +use crate::shader::cursor::{ + BindingLocation as ShaderBindingLocation, + BindingValue as ShaderBindingValue, + Cursor as ShaderCursor, +}; +use crate::shader::default::ASSET_LABEL as DEFAULT_SHADER_ASSET_LABEL; +use crate::shader::{ + Context as ShaderContext, + ModuleSource as ShaderModuleSource, + Program as ShaderProgram, + Shader, +}; +use crate::texture::{Properties as TextureProperties, Texture}; +use crate::windowing::window::Window; + +pub mod object; pub mod opengl; -static_entity!(pub RENDER_PHASE, (Phase, Pair::new::<ChildOf>(*UPDATE_PHASE))); +static NEXT_SURFACE_ID: AtomicU64 = AtomicU64::new(0); + +pub static DEFAULT_TEXTURE_ASSET_LABEL: LazyLock<AssetLabel> = + LazyLock::new(|| AssetLabel { + path: Path::new("").into(), + name: Some("default_texture".into()), + }); + +declare_entity!( + pub PRE_RENDER_PHASE, + ( + Phase, + Pair::builder() + .relation::<ChildOf>() + .target_id(*POST_UPDATE_PHASE) + .build() + ) +); + +declare_entity!( + pub RENDER_PHASE, + ( + Phase, + Pair::builder() + .relation::<ChildOf>() + .target_id(*PRE_RENDER_PHASE) + .build() + ) +); + +builder! { +/// Window graphics properties. +#[builder(name=GraphicsPropertiesBuilder, derives=(Debug, Clone))] +#[derive(Debug, Clone, Component)] +#[non_exhaustive] +pub struct GraphicsProperties +{ + /// Number of samples for multisampling. `None` means no multisampling. + #[builder(skip_generate_fn)] + pub multisampling_sample_cnt: Option<u8>, + + /// Whether graphics API debugging is enabled. + pub debug: bool, + + /// Whether depth testing is enabled + pub depth_test: bool, +} +} + +impl GraphicsProperties +{ + pub fn builder() -> GraphicsPropertiesBuilder + { + GraphicsPropertiesBuilder::default() + } +} + +impl Default for GraphicsProperties +{ + fn default() -> Self + { + Self::builder().build() + } +} + +impl GraphicsPropertiesBuilder +{ + pub fn multisampling_sample_cnt(mut self, multisampling_sample_cnt: u8) -> Self + { + self.multisampling_sample_cnt = Some(multisampling_sample_cnt); + self + } + + pub fn no_multisampling(mut self) -> Self + { + self.multisampling_sample_cnt = None; + self + } +} + +impl Default for GraphicsPropertiesBuilder +{ + fn default() -> Self + { + Self { + multisampling_sample_cnt: Some(8), + debug: false, + depth_test: true, + } + } +} + +#[derive(Debug, Component)] +pub struct SurfaceSpec +{ + pub id: SurfaceId, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SurfaceId +{ + inner: u64, +} + +impl SurfaceId +{ + pub fn new_unique() -> Self + { + Self { + inner: NEXT_SURFACE_ID.fetch_add(1, Ordering::Relaxed), + } + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub enum Command +{ + MakeCurrent(SurfaceId), + ClearBuffers(BufferClearMask), + SwapBuffers(SurfaceId), + CreateShaderProgram(ObjectId, ShaderProgram), + ActivateShader(ObjectId), + SetShaderBinding(ShaderBindingLocation, ShaderBindingValue), + CreateTexture(Texture), + DrawMesh + { + mesh_asset: AssetHandle<Mesh>, + }, + SetPolygonModeConfig(PolygonModeConfig), +} + +bitflags! { + #[derive(Debug)] + pub struct BufferClearMask: u8 { + const COLOR = 1; + const DEPTH = 2; + const STENCIL = 3; + } +} + +/// Renderer command FIFO queue. +/// +/// This component is present in renderer context entities. +#[derive(Debug, Component)] +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) } + } +} + +#[derive(Debug, Component)] +pub struct WindowUsingRendererCtx; + +#[derive(Debug, Component)] +pub struct CtxUsedByWindow; + +type RenderableEntity<'a> = ( + &'a Model, + Option<&'a DrawFlags>, + Option<&'a Shader>, + Option<&'a mut PendingShaderBindings>, +); + +pub fn init(mut assets: Single<Assets>) +{ + assets.store_with_label( + DEFAULT_TEXTURE_ASSET_LABEL.clone(), + Image::from_color(Dimens { width: 1, height: 1 }, Color::WHITE_U8), + ); +} + +#[tracing::instrument(skip_all)] +pub fn enqueue_commands( + renderer_ctx_query: Query<( + &mut CommandQueue, + &ObjectStore, + &[Pair<CtxUsedByWindow, Wildcard>], + )>, + renderable_query: Query<RenderableEntity<'_>, (Without<NoDraw>,)>, + assets: Single<Assets>, + shader_context: Single<ShaderContext>, + mut actions: Actions, +) +{ + let Some(default_shader_asset) = assets + .get_handle_to_loaded::<ShaderModuleSource>(DEFAULT_SHADER_ASSET_LABEL.clone()) + else { + tracing::error!("Default shader asset is not loaded"); + return; + }; + + for (renderer_ctx_ent_id, (mut command_queue, object_store, used_by_windows)) in + renderer_ctx_query.iter_with_euids() + { + for ctx_used_by_window in used_by_windows { + let window_ent_id = ctx_used_by_window.id().target_entity(); + + let Some(window_ent) = ctx_used_by_window.get_target_ent() else { + tracing::error!("Window entity does not exist"); + continue; + }; + + if window_ent.get::<Window>().is_none() { + tracing::debug!( + window_entity_id=%window_ent_id, + "Window entity does not have a {} component", + type_name::<Window>() + ); + + actions.remove_components( + renderer_ctx_ent_id, + [Pair::builder() + .relation::<CtxUsedByWindow>() + .target_id(window_ent_id) + .build() + .id()], + ); + + continue; + }; + + let Some(surface_spec) = window_ent.get::<SurfaceSpec>() else { + tracing::debug!( + window_entity_id=%window_ent_id, + "Window entity does not have a {} component", + type_name::<SurfaceSpec>() + ); + continue; + }; + + command_queue.push(Command::MakeCurrent(surface_spec.id)); + + let default_texture_asset = assets + .get_handle_to_loaded::<Image>(DEFAULT_TEXTURE_ASSET_LABEL.clone()) + .expect("Not possible"); + + if !object_store + .contains_with_id(&ObjectId::Asset(default_texture_asset.id())) + { + command_queue.push(Command::CreateTexture(Texture { + asset_handle: default_texture_asset, + properties: TextureProperties::default(), + })); + } + + command_queue.push(Command::ClearBuffers( + BufferClearMask::COLOR | BufferClearMask::DEPTH, + )); + + for (model, draw_flags, shader, mut pending_shader_bindings) in + &renderable_query + { + let shader_asset = match &shader { + Some(shader) => &shader.asset_handle, + None => &default_shader_asset, + }; + + if pending_shader_bindings.as_ref().map_or_else( + || true, + |pending_shader_bindings| pending_shader_bindings.bindings.is_empty(), + ) { + continue; + } + + let Some(model_spec) = assets.get(&model.spec_asset) else { + continue; + }; + + let Some(mesh_asset) = &model_spec.mesh_asset else { + continue; + }; + + debug_assert!(model_spec.material_names.len() <= 1); + + let model_material_asset = match model_spec.find_first_material(&assets) { + MaterialSearchResult::Found(model_material_asset) => { + model_material_asset.clone() + // Some(model_material_asset.clone()) + } + MaterialSearchResult::NotFound + | MaterialSearchResult::NoMaterials => { + // MaterialSearchResult::NotFound => { + continue; + } // MaterialSearchResult::NoMaterials => None, + }; + + if !object_store.contains_with_id(&ObjectId::Asset(shader_asset.id())) { + let Some(shader_program) = + shader_context.get_program(&shader_asset.id()) + else { + tracing::error!( + "Shader context doesn't have a program for shader asset {:?}", + assets.get_label(&shader_asset) + ); + continue; + }; + + command_queue.push(Command::CreateShaderProgram( + ObjectId::Asset(shader_asset.id()), + shader_program.clone(), + )); + } + + command_queue + .push(Command::ActivateShader(ObjectId::Asset(shader_asset.id()))); + + let Some(model_material) = assets.get(&model_material_asset) else { + // TODO: Handle this case since it may occur + unreachable!(); + }; + + for texture in [ + &model_material.ambient_map, + &model_material.diffuse_map, + &model_material.specular_map, + ] + .into_iter() + .flatten() + { + if !object_store + .contains_with_id(&ObjectId::Asset(texture.asset_handle.id())) + { + command_queue.push(Command::CreateTexture(texture.clone())); + } + } + + if let Some(pending_shader_bindings) = &mut pending_shader_bindings { + for (shader_binding_loc, shader_binding_val) in + pending_shader_bindings.bindings.drain(..) + { + command_queue.push(Command::SetShaderBinding( + shader_binding_loc, + shader_binding_val, + )); + } + } + + if let Some(draw_flags) = draw_flags.as_deref() + && draw_flags.polygon_mode_config != PolygonModeConfig::default() + { + command_queue.push(Command::SetPolygonModeConfig( + draw_flags.polygon_mode_config.clone(), + )); + } + + command_queue.push(Command::DrawMesh { mesh_asset: mesh_asset.clone() }); + + if let Some(draw_flags) = draw_flags.as_deref() + && draw_flags.polygon_mode_config != PolygonModeConfig::default() + { + command_queue.push(Command::SetPolygonModeConfig( + PolygonModeConfig::default(), + )); + } + } + + command_queue.push(Command::SwapBuffers(surface_spec.id)); + } + } +} + +#[derive(Default, Clone, Component)] +pub struct PendingShaderBindings +{ + pub bindings: Vec<(ShaderBindingLocation, ShaderBindingValue)>, +} + +impl<'a> Extend<(ShaderCursor<'a>, ShaderBindingValue)> for PendingShaderBindings +{ + fn extend<Iter: IntoIterator<Item = (ShaderCursor<'a>, ShaderBindingValue)>>( + &mut self, + iter: Iter, + ) + { + self.bindings.extend(iter.into_iter().map( + |(shader_cursor, shader_binding_val)| { + (shader_cursor.into_binding_location(), shader_binding_val) + }, + )) + } +} diff --git a/engine/src/renderer/object.rs b/engine/src/renderer/object.rs new file mode 100644 index 0000000..357bd6a --- /dev/null +++ b/engine/src/renderer/object.rs @@ -0,0 +1,123 @@ +use std::collections::HashMap; +use std::collections::hash_map::Entry as HashMapEntry; + +use ecs::Component; + +use crate::asset::Id as AssetId; + +/// Renderer object ID. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Id +{ + Asset(AssetId), + Other(u64), +} + +/// Renderer object store. +#[derive(Debug, Default, Component)] +pub struct Store +{ + objects: HashMap<Id, Object>, +} + +impl Store +{ + pub fn get_obj(&self, id: &Id) -> Option<&Object> + { + self.objects.get(id) + } + + pub fn get_texture_obj(&self, id: &Id) -> Option<&Object> + { + let obj = self.get_obj(id)?; + + if !matches!(obj.kind(), Kind::Texture) { + return None; + } + + Some(obj) + } + + pub fn get_shader_program_obj(&self, id: &Id) -> Option<&Object> + { + let obj = self.get_obj(id)?; + + if !matches!(obj.kind(), Kind::ShaderProgram) { + return None; + } + + Some(obj) + } + + pub fn contains_with_id(&self, id: &Id) -> bool + { + self.objects.contains_key(id) + } + + pub fn insert(&mut self, id: Id, object: Object) + { + self.objects.insert(id, object); + } + + pub fn entry(&mut self, id: Id) -> StoreEntry<'_> + { + StoreEntry { inner: self.objects.entry(id) } + } +} + +#[derive(Debug)] +pub struct StoreEntry<'store> +{ + inner: HashMapEntry<'store, Id, Object>, +} + +impl<'store> StoreEntry<'store> +{ + pub fn or_insert(self, default_obj: Object) -> &'store mut Object + { + self.inner.or_insert(default_obj) + } + + pub fn or_insert_with( + self, + default_func: impl FnOnce() -> Object, + ) -> &'store mut Object + { + self.inner.or_insert_with(default_func) + } +} + +/// Renderer object. +#[derive(Debug, Clone)] +pub struct Object +{ + raw: u32, + kind: Kind, +} + +impl Object +{ + pub fn from_raw(raw: u32, kind: Kind) -> Self + { + Self { raw, kind } + } + + pub fn as_raw(&self) -> u32 + { + self.raw + } + + pub fn kind(&self) -> Kind + { + self.kind + } +} + +/// Renderer object kind. +#[derive(Debug, Clone, Copy)] +#[non_exhaustive] +pub enum Kind +{ + Texture, + ShaderProgram, +} diff --git a/engine/src/renderer/opengl.rs b/engine/src/renderer/opengl.rs index 858a899..c72a344 100644 --- a/engine/src/renderer/opengl.rs +++ b/engine/src/renderer/opengl.rs @@ -1,87 +1,143 @@ //! OpenGL renderer. +use std::any::type_name; +use std::borrow::Cow; use std::collections::HashMap; -use std::ffi::{c_void, CString}; -use std::io::{Error as IoError, ErrorKind as IoErrorKind}; -use std::path::Path; -use std::process::abort; use ecs::actions::Actions; -use ecs::component::local::Local; -use ecs::component::Handle as ComponentHandle; -use ecs::phase::START as START_PHASE; +use ecs::entity::obtainer::Obtainer as EntityObtainer; +use ecs::event::component::{Changed, Removed}; +use ecs::pair::{ChildOf, Pair, Wildcard}; +use ecs::phase::{Phase, START as START_PHASE}; use ecs::query::term::Without; use ecs::sole::Single; -use ecs::system::{Into as _, System}; -use ecs::{Component, Query}; - -use crate::camera::{Active as ActiveCamera, Camera}; -use crate::color::Color; -use crate::data_types::dimens::Dimens; -use crate::draw_flags::{DrawFlags, NoDraw, PolygonModeConfig}; -use crate::lighting::{DirectionalLight, GlobalLight, PointLight}; -use crate::material::{Flags as MaterialFlags, Material}; -use crate::matrix::Matrix; -use crate::mesh::Mesh; -use crate::opengl::buffer::{Buffer, Usage as BufferUsage}; -use crate::opengl::debug::{ - enable_debug_output, - set_debug_message_callback, - set_debug_message_control, +use ecs::system::observer::Observe; +use ecs::{Component, Query, declare_entity}; +use glutin::display::GetGlDisplay; +use glutin::prelude::GlDisplay; +use glutin::surface::{ + GlSurface, + Surface as GlutinSurface, + WindowSurface as GlutinWindowSurface, +}; +use opengl_bindings::debug::{ MessageIdsAction, MessageSeverity, MessageSource, MessageType, + SetDebugMessageControlError as GlSetDebugMessageControlError, + set_debug_message_callback, + set_debug_message_control, }; -use crate::opengl::glsl::{ - preprocess as glsl_preprocess, - PreprocessingError as GlslPreprocessingError, +use opengl_bindings::misc::{ + BufferClearMask as GlBufferClearMask, + Capability, + SetViewportError as GlSetViewportError, + clear_buffers, + enable, + set_enabled, }; -use crate::opengl::shader::{ +use opengl_bindings::shader::{ Error as GlShaderError, Kind as ShaderKind, Program as GlShaderProgram, Shader as GlShader, + // UniformLocation as GlUniformLocation, }; -use crate::opengl::texture::{ - set_active_texture_unit, +use opengl_bindings::texture::{ + Filtering as GlTextureFiltering, + GenerateError as GlTextureGenerateError, + PixelDataFormat as GlTexturePixelDataFormat, Texture as GlTexture, - TextureUnit, + Wrapping as GlTextureWrapping, }; -use crate::opengl::vertex_array::{ - DataType as VertexArrayDataType, +use opengl_bindings::vertex_array::{ + DrawError as GlDrawError, PrimitiveKind, VertexArray, }; -use crate::opengl::{ - clear_buffers, - enable, - get_context_flags as get_opengl_context_flags, +use opengl_bindings::{ContextWithFns, CurrentContextWithFns}; +use safer_ffi::layout::ReprC; +use zerocopy::{Immutable, IntoBytes}; + +use crate::asset::{Assets, Id as AssetId}; +use crate::data_types::dimens::Dimens; +use crate::image::{ColorType as ImageColorType, Image}; +use crate::matrix::Matrix; +use crate::model::Model; +use crate::renderer::object::{ + Id as RendererObjectId, + Kind as RendererObjectKind, + Object as RendererObject, + Store as RendererObjectStore, +}; +use crate::renderer::opengl::glutin_compat::{ + DisplayBuilder, + Error as GlutinCompatError, +}; +use crate::renderer::opengl::graphics_mesh::GraphicsMesh; +use crate::renderer::{ BufferClearMask, - Capability, - ContextFlags, + Command as RendererCommand, + CommandQueue as RendererCommandQueue, + CtxUsedByWindow as RendererCtxUsedByWindow, + GraphicsProperties, + PRE_RENDER_PHASE, + RENDER_PHASE, + SurfaceId, + SurfaceSpec, + WindowUsingRendererCtx, +}; +use crate::shader::cursor::BindingValue as ShaderBindingValue; +use crate::shader::{ + Error as ShaderError, + Program as ShaderProgram, + Stage as ShaderStage, +}; +use crate::texture::{ + Filtering as TextureFiltering, + Properties as TextureProperties, + Wrapping as TextureWrapping, }; -use crate::projection::{ClipVolume, Projection}; -use crate::renderer::opengl::vertex::{AttributeComponentType, Vertex}; -use crate::renderer::RENDER_PHASE; -use crate::texture::{Id as TextureId, Texture}; -use crate::transform::{Scale, WorldPosition}; -use crate::util::{defer, Defer, RefOrValue}; use crate::vector::{Vec2, Vec3}; -use crate::window::Window; +use crate::windowing::Context as WindowingContext; +use crate::windowing::window::{ + Closed as WindowClosed, + CreationAttributes as WindowCreationAttributes, + CreationReady, + Window, +}; +mod glutin_compat; +mod graphics_mesh; mod vertex; -type RenderableEntity<'a> = ( - &'a Mesh, - &'a Material, - Option<&'a MaterialFlags>, - Option<&'a WorldPosition>, - Option<&'a Scale>, - Option<&'a DrawFlags>, - Option<&'a GlObjects>, +declare_entity!( + pub POST_RENDER_PHASE, + (Phase, Pair::builder().relation::<ChildOf>().target_id(*RENDER_PHASE).build()) ); +#[derive(Debug, Component)] +struct WindowGlConfig +{ + gl_config: glutin::config::Config, +} + +#[derive(Component)] +struct GraphicsContext +{ + gl_context: ContextWithFns, + graphics_mesh_store: GraphicsMeshStore, + surfaces: HashMap<SurfaceId, GlutinSurface<GlutinWindowSurface>>, + uniform_buffer_objs: HashMap<u32, opengl_bindings::buffer::Buffer<u8>>, +} + +#[derive(Debug, Default)] +struct GraphicsMeshStore +{ + graphics_meshes: HashMap<AssetId, GraphicsMesh>, +} + #[derive(Debug, Default)] #[non_exhaustive] pub struct Extension {} @@ -90,607 +146,1008 @@ impl ecs::extension::Extension for Extension { fn collect(self, mut collector: ecs::extension::Collector<'_>) { - collector.add_system(*START_PHASE, initialize); + collector.add_declared_entity(&PRE_RENDER_PHASE); + collector.add_declared_entity(&RENDER_PHASE); + collector.add_declared_entity(&POST_RENDER_PHASE); - collector.add_system( - *RENDER_PHASE, - render - .into_system() - .initialize((GlobalGlObjects::default(),)), - ); - } -} + collector.add_system(*START_PHASE, super::init); -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(); - } - }); - - if get_opengl_context_flags().contains(ContextFlags::DEBUG) { - initialize_debug(); - } + collector.add_system(*RENDER_PHASE, super::enqueue_commands); + collector.add_system(*RENDER_PHASE, handle_commands); - let window_size = window.size().expect("Failed to get window size"); + collector.add_system(*POST_RENDER_PHASE, prepare_windows); + collector.add_system(*POST_RENDER_PHASE, init_window_graphics); - set_viewport(Vec2 { x: 0, y: 0 }, window_size); + collector.add_observer(handle_model_removed); - window.set_framebuffer_size_callback(|new_window_size| { - set_viewport(Vec2::ZERO, new_window_size); - }); - - enable(Capability::DepthTest); - enable(Capability::MultiSample); + collector.add_observer(handle_window_changed); + collector.add_observer(handle_window_removed); + } } -#[allow(clippy::too_many_arguments)] -fn render( - query: Query<RenderableEntity<'_>, (Without<NoDraw>,)>, - point_light_query: Query<(&PointLight, &WorldPosition)>, - directional_lights: Query<(&DirectionalLight,)>, - camera_query: Query<(&Camera, &WorldPosition, &ActiveCamera)>, - window: Single<Window>, - global_light: Single<GlobalLight>, - mut gl_objects: Local<GlobalGlObjects>, - mut actions: Actions, +#[tracing::instrument(skip_all)] +fn handle_model_removed( + observe: Observe<Pair<Removed, Model>>, + renderer_ctx_query: Query<( + &mut GraphicsContext, + Pair<RendererCtxUsedByWindow, Wildcard>, + )>, + assets: Single<Assets>, ) { - let Some((camera, camera_world_pos, _)) = camera_query.iter().next() else { - tracing::warn!("No current camera. Nothing will be rendered"); - return; - }; - - let directional_lights = directional_lights.iter().collect::<Vec<_>>(); + for evt_match in &observe { + let model_ent_id = evt_match.id(); + + for (renderer_ctx_ent_id, (mut graphics_ctx, renderer_ctx_used_by_window)) in + renderer_ctx_query.iter_with_euids() + { + let GraphicsContext { + ref gl_context, + ref mut graphics_mesh_store, + ref surfaces, + .. + } = *graphics_ctx; + + let model = evt_match.get_removed_comp(); + + let Some(model_spec) = assets.get(&model.spec_asset) else { + continue; + }; - let GlobalGlObjects { - shader_program, - textures: gl_textures, - } = &mut *gl_objects; + let Some(mesh_asset) = &model_spec.mesh_asset else { + continue; + }; - let shader_program = - shader_program.get_or_insert_with(|| create_default_shader_program().unwrap()); + if !graphics_mesh_store + .graphics_meshes + .contains_key(&mesh_asset.id()) + { + continue; + } - clear_buffers(BufferClearMask::COLOR | BufferClearMask::DEPTH); + let Some(window_ent) = renderer_ctx_used_by_window.get_target_ent() else { + tracing::error!( + window_entity_id = %renderer_ctx_used_by_window.id().target_entity(), + "Window entity does not exist" + ); + continue; + }; - for ( - euid, - (mesh, material, material_flags, position, scale, draw_flags, gl_objects), - ) in query.iter_with_euids() - { - let material_flags = material_flags - .map(|material_flags| material_flags.clone()) - .unwrap_or_default(); + let Some(surface_spec) = window_ent.get::<SurfaceSpec>() else { + tracing::error!( + window_entity_id = %window_ent.uid(), + "Window entity does not have a {} component", + type_name::<SurfaceSpec>() + ); + continue; + }; - let gl_objs = match gl_objects.as_deref() { - Some(gl_objs) => RefOrValue::Ref(gl_objs), - None => RefOrValue::Value(Some(GlObjects::new(&mesh))), - }; + let Some(surface) = surfaces.get(&surface_spec.id) else { + tracing::error!( + window_entity_id = %window_ent.uid(), + "Surface specified by window entity's {} component does not exist", + type_name::<SurfaceSpec>() + ); + continue; + }; - defer!(|gl_objs| { - if let RefOrValue::Value(opt_gl_objs) = gl_objs { - actions.add_components(euid, (opt_gl_objs.take().unwrap(),)); + let curr_gl_ctx = match gl_context.make_current(surface) { + Ok(curr_gl_ctx) => curr_gl_ctx, + Err(err) => { + tracing::error!("{err}"); + continue; + } }; - }); - apply_transformation_matrices( - Transformation { - position: position.map(|pos| *pos).unwrap_or_default().position, - scale: scale.map(|scale| *scale).unwrap_or_default().scale, - }, - shader_program, - &camera, - &camera_world_pos, - window.size().expect("Failed to get window size"), - ); + tracing::debug!( + model_entity_id=%model_ent_id, + renderer_ctx_entity_id=%renderer_ctx_ent_id, + "Cleaning up after model in renderer context" + ); - apply_light( - &material, - &material_flags, - &global_light, - shader_program, - (point_light_query.iter(), point_light_query.iter().count()), - directional_lights - .iter() - .map(|(dir_light,)| &**dir_light) - .collect::<Vec<_>>() - .as_slice(), - &camera_world_pos, - ); + let Some(mut graphics_mesh) = + graphics_mesh_store.graphics_meshes.remove(&mesh_asset.id()) + else { + tracing::warn!( + model_entity_id=%model_ent_id, + "No mesh exists for model" + ); + continue; + }; - for (index, texture) in material.textures.iter().enumerate() { - let gl_texture = gl_textures - .entry(texture.id()) - .or_insert_with(|| create_gl_texture(texture)); + graphics_mesh.destroy(&curr_gl_ctx); + } + } +} - let texture_unit = TextureUnit::from_num(index).expect("Too many textures"); +#[tracing::instrument(skip_all)] +fn handle_window_changed( + observe: Observe<Pair<Changed, Window>>, + entity_obtainer: EntityObtainer, +) +{ + for evt_match in &observe { + let window_ent = evt_match.get_ent_infallible(); - set_active_texture_unit(texture_unit); + tracing::trace!( + new_state = ?evt_match.get_changed_comp(), + "Handling window change" + ); - gl_texture.bind(); - } + let Some(surface_spec) = window_ent.get::<SurfaceSpec>() else { + continue; + }; - shader_program.activate(); + let Some(renderer_ctx_ent_id) = window_ent + .get_matching_components( + Pair::builder() + .relation::<WindowUsingRendererCtx>() + .target_id(Wildcard::uid()) + .build() + .id(), + ) + .next() + .map(|comp_ref| comp_ref.id().target_entity()) + else { + continue; + }; - if let Some(draw_flags) = &draw_flags { - crate::opengl::set_polygon_mode( - draw_flags.polygon_mode_config.face, - draw_flags.polygon_mode_config.mode, + let Some(renderer_ctx_ent) = entity_obtainer.get_entity(renderer_ctx_ent_id) + else { + tracing::error!("Renderer context entity does not exist"); + continue; + }; + + let Some(graphics_context) = renderer_ctx_ent.get::<GraphicsContext>() else { + tracing::error!( + "Renderer context entity does not have a GraphicsContext component" ); - } + continue; + }; - draw_mesh(gl_objs.get().unwrap()); + let Some(surface) = graphics_context.surfaces.get(&surface_spec.id) else { + tracing::error!( + window_entity_id = %window_ent.uid(), + "Surface specified by window entity's {} component does not exist", + type_name::<SurfaceSpec>() + ); + continue; + }; - if draw_flags.is_some() { - let default_polygon_mode_config = PolygonModeConfig::default(); + let Ok(current_graphics_context) = + graphics_context.gl_context.make_current(surface) + else { + tracing::error!("Failed to make graphics context current"); + continue; + }; - crate::opengl::set_polygon_mode( - default_polygon_mode_config.face, - default_polygon_mode_config.mode, - ); + if let Err(err) = set_viewport( + ¤t_graphics_context, + Vec2::default(), + evt_match.get_changed_comp().inner_size(), + ) { + tracing::error!("Failed to set viewport: {err}"); } } } -#[derive(Debug, Default, Component)] -struct GlobalGlObjects -{ - shader_program: Option<GlShaderProgram>, - textures: HashMap<TextureId, GlTexture>, -} - -fn set_viewport(position: Vec2<u32>, size: Dimens<u32>) +#[tracing::instrument(skip_all)] +fn handle_window_removed(observe: Observe<Pair<Removed, Window>>, mut actions: Actions) { - crate::opengl::set_viewport(position, size); -} + for evt_match in &observe { + let window_ent_id = evt_match.id(); -fn initialize_debug() -{ - enable_debug_output(); + let window_ent = evt_match.get_ent_infallible(); - set_debug_message_callback(opengl_debug_message_cb); - set_debug_message_control(None, None, None, &[], MessageIdsAction::Disable); -} + tracing::debug!( + entity_id = %window_ent_id, + title = %evt_match.get_removed_comp().title, + "Handling removal of window" + ); -fn draw_mesh(gl_objects: &GlObjects) -{ - gl_objects.vertex_arr.bind(); + actions.remove_comps::<(SurfaceSpec, WindowGlConfig)>(window_ent_id); - if gl_objects.index_buffer.is_some() { - VertexArray::draw_elements(PrimitiveKind::Triangles, 0, gl_objects.element_cnt); - } else { - VertexArray::draw_arrays(PrimitiveKind::Triangles, 0, gl_objects.element_cnt); - } -} + let Some(with_renderer_ctx_pair) = window_ent + .get_first_wildcard_pair_match::<WindowUsingRendererCtx, Wildcard>() + else { + tracing::warn!( + "Window entity is missing a ({}, *) pair", + type_name::<WindowUsingRendererCtx>() + ); + continue; + }; -fn create_gl_texture(texture: &Texture) -> GlTexture -{ - let mut gl_texture = GlTexture::new(); + let renderer_context_ent_id = with_renderer_ctx_pair.id().target_entity(); - gl_texture.generate( - *texture.dimensions(), - texture.image().as_bytes(), - texture.pixel_data_format(), - ); + actions.remove_comps::<(GraphicsContext, RendererObjectStore)>( + renderer_context_ent_id, + ); - gl_texture.apply_properties(texture.properties()); + actions.remove_components( + renderer_context_ent_id, + [Pair::builder() + .relation::<RendererCtxUsedByWindow>() + .target_id(window_ent_id) + .build() + .id()], + ); - gl_texture + actions.remove_components(window_ent_id, [with_renderer_ctx_pair.id()]); + } } -const VERTEX_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/vertex.glsl"); -const FRAGMENT_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/fragment.glsl"); - -const VERTEX_DATA_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/vertex_data.glsl"); -const LIGHT_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/light.glsl"); - -fn create_default_shader_program() -> Result<GlShaderProgram, CreateShaderError> +#[derive(Debug, Component)] +struct SetupFailed; + +fn prepare_windows( + window_query: Query< + ( + Option<&Window>, + &mut WindowCreationAttributes, + Option<&GraphicsProperties>, + ), + ( + Without<CreationReady>, + Without<WindowGlConfig>, + Without<WindowClosed>, + Without<SetupFailed>, + ), + >, + windowing_context: Single<WindowingContext>, + mut actions: Actions, +) { - let mut vertex_shader = GlShader::new(ShaderKind::Vertex); - - vertex_shader.set_source(&*glsl_preprocess( - VERTEX_GLSL_SHADER_SRC, - &get_glsl_shader_content, - )?)?; - - vertex_shader.compile()?; - - let mut fragment_shader = GlShader::new(ShaderKind::Fragment); + let Some(display_handle) = windowing_context.display_handle() else { + return; + }; - fragment_shader.set_source(&*glsl_preprocess( - FRAGMENT_GLSL_SHADER_SRC, - &get_glsl_shader_content, - )?)?; + for (window_ent_id, (window, mut window_creation_attrs, graphics_props)) in + window_query.iter_with_euids() + { + tracing::debug!("Preparing window entity {window_ent_id} for use in rendering"); - fragment_shader.compile()?; + let mut glutin_config_template_builder = + glutin::config::ConfigTemplateBuilder::new(); - let mut gl_shader_program = GlShaderProgram::new(); + let graphics_props = match graphics_props.as_ref() { + Some(graphics_props) => &*graphics_props, + None => { + actions.add_components(window_ent_id, (GraphicsProperties::default(),)); - gl_shader_program.attach(&vertex_shader); - gl_shader_program.attach(&fragment_shader); + &GraphicsProperties::default() + } + }; - gl_shader_program.link()?; + if let Some(multisampling_sample_cnt) = graphics_props.multisampling_sample_cnt { + glutin_config_template_builder = glutin_config_template_builder + .with_multisampling(multisampling_sample_cnt); + } - Ok(gl_shader_program) -} + let window_handle = match window + .as_ref() + .map(|window| unsafe { + windowing_context.get_window_as_handle(&window.wid()) + }) + .flatten() + .transpose() + { + Ok(window_handle) => window_handle, + Err(err) => { + tracing::error!("Failed to get window handle: {err}"); + actions.add_components(window_ent_id, (SetupFailed,)); + continue; + } + }; -#[derive(Debug, thiserror::Error)] -enum CreateShaderError -{ - #[error(transparent)] - ShaderError(#[from] GlShaderError), + let (new_window_creation_attrs, gl_config) = match DisplayBuilder::new() + .with_window_attributes(window_creation_attrs.clone()) + .build( + window_handle, + &display_handle, + glutin_config_template_builder, + |mut cfgs| cfgs.next(), + ) { + Ok((new_window_creation_attrs, gl_config)) => { + (new_window_creation_attrs, gl_config) + } + Err(GlutinCompatError::WindowRequired) => { + actions.add_components(window_ent_id, (CreationReady,)); + continue; + } + Err(err) => { + tracing::error!("Failed to create platform graphics display: {err}"); + actions.add_components(window_ent_id, (SetupFailed,)); + continue; + } + }; - #[error(transparent)] - PreprocessingError(#[from] GlslPreprocessingError), -} + *window_creation_attrs = new_window_creation_attrs; -fn get_glsl_shader_content(path: &Path) -> Result<Vec<u8>, std::io::Error> -{ - if path == Path::new("vertex_data.glsl") { - return Ok(VERTEX_DATA_GLSL_SHADER_SRC.as_bytes().to_vec()); - } + actions.add_components(window_ent_id, (WindowGlConfig { gl_config },)); - if path == Path::new("light.glsl") { - return Ok(LIGHT_GLSL_SHADER_SRC.as_bytes().to_vec()); + if window.is_none() { + actions.add_components(window_ent_id, (CreationReady,)); + } } - - Err(IoError::new( - IoErrorKind::NotFound, - format!("Content for shader file {} not found", path.display()), - )) } -#[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>>, - element_cnt: u32, - - vertex_arr: VertexArray, -} - -impl GlObjects +#[tracing::instrument(skip_all)] +fn init_window_graphics( + window_query: Query< + (&Window, &WindowGlConfig, &GraphicsProperties), + (Without<SurfaceSpec>, Without<SetupFailed>), + >, + mut actions: Actions, + windowing_context: Single<WindowingContext>, +) { - #[tracing::instrument(skip_all)] - fn new(mesh: &Mesh) -> Self + for (window_ent_id, (window, window_gl_config, graphics_props)) in + window_query.iter_with_euids() { - tracing::trace!( - "Creating vertex array, vertex buffer{}", - if mesh.indices().is_some() { - " and index buffer" - } else { - "" - } - ); + tracing::info!("Initializing graphics for window {window_ent_id}"); + + let display = window_gl_config.gl_config.display(); + + let window_handle = + match unsafe { windowing_context.get_window_as_handle(&window.wid()) } + .transpose() + { + Ok(Some(window_handle)) => window_handle, + Ok(None) => { + tracing::error!( + wid = ?window.wid(), + entity_id = %window_ent_id, + "Windowing context does not contain window" + ); + actions.add_components(window_ent_id, (SetupFailed,)); + continue; + } + Err(err) => { + tracing::error!("Failed to get window handle: {err}"); + actions.add_components(window_ent_id, (SetupFailed,)); + continue; + } + }; - let mut vertex_arr = VertexArray::new(); - let mut vertex_buffer = Buffer::new(); + let Some(window_inner_size) = window.inner_size().try_into_nonzero() else { + tracing::error!( + "Cannot create a surface for a window with a width/height of 0", + ); + continue; + }; - vertex_buffer.store_mapped(mesh.vertices(), BufferUsage::Static, |vertex| { - Vertex { - pos: vertex.pos, - texture_coords: vertex.texture_coords, - normal: vertex.normal, + let surface = match unsafe { + display.create_window_surface( + &window_gl_config.gl_config, + &glutin::surface::SurfaceAttributesBuilder::< + glutin::surface::WindowSurface, + >::new() + .build( + window_handle.as_raw(), + window_inner_size.width, + window_inner_size.height, + ), + ) + } { + Ok(surface) => surface, + Err(err) => { + tracing::error!("Failed to create window surface: {err}"); + continue; } - }); - - vertex_arr.bind_vertex_buffer(0, &vertex_buffer, 0); - - let mut offset = 0u32; + }; - for attrib in Vertex::attrs() { - vertex_arr.enable_attrib(attrib.index); + let context = match unsafe { + display.create_context( + &window_gl_config.gl_config, + &glutin::context::ContextAttributesBuilder::new() + .with_debug(graphics_props.debug) + .build(Some(window_handle.as_raw())), + ) + } { + Ok(context) => context, + Err(err) => { + tracing::error!("Failed to create graphics context: {err}"); + continue; + } + }; - vertex_arr.set_attrib_format( - attrib.index, - match attrib.component_type { - AttributeComponentType::Float => VertexArrayDataType::Float, - }, - false, - offset, - ); + let gl_context = match ContextWithFns::new(context, &surface) { + Ok(context) => context, + Err(err) => { + tracing::error!("Failed to create graphics context: {err}"); + continue; + } + }; - vertex_arr.set_attrib_vertex_buf_binding(attrib.index, 0); + let Ok(curr_gl_context) = gl_context.make_current(&surface) else { + tracing::error!("Failed to make graphics context current"); + continue; + }; - offset += attrib.component_size * attrib.component_cnt as u32; + if let Err(err) = + set_viewport(&curr_gl_context, Vec2 { x: 0, y: 0 }, window.inner_size()) + { + tracing::error!("Failed to set viewport: {err}"); } - if let Some(indices) = mesh.indices() { - let mut index_buffer = Buffer::new(); - - index_buffer.store(indices, BufferUsage::Static); + set_enabled( + &curr_gl_context, + Capability::DepthTest, + graphics_props.depth_test, + ); - vertex_arr.bind_element_buffer(&index_buffer); + set_enabled( + &curr_gl_context, + Capability::MultiSample, + graphics_props.multisampling_sample_cnt.is_some(), + ); - return Self { - _vertex_buffer: vertex_buffer, - index_buffer: Some(index_buffer), - element_cnt: indices - .len() - .try_into() - .expect("Mesh index count does not fit into a 32-bit unsigned int"), - vertex_arr, - }; + if graphics_props.debug { + enable(&curr_gl_context, Capability::DebugOutput); + enable(&curr_gl_context, Capability::DebugOutputSynchronous); + + set_debug_message_callback(&curr_gl_context, opengl_debug_message_cb); + + match set_debug_message_control( + &curr_gl_context, + None, + None, + None, + &[], + MessageIdsAction::Disable, + ) { + Ok(()) => {} + Err(GlSetDebugMessageControlError::TooManyIds { + id_cnt: _, + max_id_cnt: _, + }) => { + unreachable!() // No ids are given + } + } } - Self { - _vertex_buffer: vertex_buffer, - index_buffer: None, - element_cnt: mesh - .vertices() - .len() - .try_into() - .expect("Mesh vertex count does not fit into a 32-bit unsigned int"), - vertex_arr, - } + let surface_id = SurfaceId::new_unique(); + + let renderer_ctx_ent_id = actions.spawn(( + GraphicsContext { + gl_context, + graphics_mesh_store: GraphicsMeshStore::default(), + surfaces: HashMap::from([(surface_id, surface)]), + uniform_buffer_objs: HashMap::new(), + }, + RendererObjectStore::default(), + RendererCommandQueue::default(), + Pair::builder() + .relation::<RendererCtxUsedByWindow>() + .target_id(window_ent_id) + .build(), + )); + + actions.add_components( + window_ent_id, + ( + SurfaceSpec { id: surface_id }, + Pair::builder() + .relation::<WindowUsingRendererCtx>() + .target_id(renderer_ctx_ent_id) + .build(), + ), + ); } } -fn apply_transformation_matrices( - transformation: Transformation, - gl_shader_program: &mut GlShaderProgram, - camera: &Camera, - camera_world_pos: &WorldPosition, - window_size: Dimens<u32>, +#[tracing::instrument(skip_all)] +fn handle_commands( + renderer_ctx_query: Query<( + &mut GraphicsContext, + &mut RendererObjectStore, + &mut RendererCommandQueue, + )>, + assets: Single<Assets>, ) { - gl_shader_program - .set_uniform(c"model", &create_transformation_matrix(transformation)); + for (mut graphics_ctx, mut renderer_object_store, mut command_queue) in + &renderer_ctx_query + { + let GraphicsContext { + ref gl_context, + ref mut graphics_mesh_store, + ref surfaces, + ref mut uniform_buffer_objs, + } = *graphics_ctx; + + let mut opt_curr_gl_ctx: Option<CurrentContextWithFns> = None; + + let mut activated_gl_shader_program: Option<GlShaderProgram> = None; + + for command in command_queue.drain() { + let tracing_span = tracing::info_span!("handle_cmd"); + let _tracing_span_enter = tracing_span.enter(); + + match command { + RendererCommand::MakeCurrent(surface_id) => { + let Some(surface) = surfaces.get(&surface_id) else { + tracing::error!(surface_id=?surface_id, "Surface does not exist"); + continue; + }; + + let curr_gl_ctx = match gl_context.make_current(surface) { + Ok(current_graphics_context) => current_graphics_context, + Err(err) => { + tracing::error!( + "Failed to make graphics context current: {err}" + ); + continue; + } + }; + + opt_curr_gl_ctx = Some(curr_gl_ctx); + } + RendererCommand::ClearBuffers(buffer_clear_mask) => { + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + tracing::error!("No GL context is current"); + continue; + }; + + let mut clear_mask = GlBufferClearMask::empty(); + + clear_mask.set( + GlBufferClearMask::COLOR, + buffer_clear_mask.contains(BufferClearMask::COLOR), + ); + + clear_mask.set( + GlBufferClearMask::DEPTH, + buffer_clear_mask.contains(BufferClearMask::DEPTH), + ); + + clear_mask.set( + GlBufferClearMask::STENCIL, + buffer_clear_mask.contains(BufferClearMask::STENCIL), + ); + + clear_buffers(&curr_gl_ctx, clear_mask); + } + RendererCommand::SwapBuffers(surface_id) => { + let Some(surface) = surfaces.get(&surface_id) else { + tracing::error!(surface_id=?surface_id, "Surface does not exist"); + continue; + }; + + if let Err(err) = surface.swap_buffers(gl_context.context()) { + tracing::error!("Failed to swap buffers: {err}"); + } + } + RendererCommand::CreateShaderProgram( + shader_program_obj_id, + shader_program, + ) => { + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + tracing::error!("No GL context is current"); + continue; + }; + + if renderer_object_store.contains_with_id(&shader_program_obj_id) { + tracing::error!( + object_id=?shader_program_obj_id, + "Object store already contains a object with this ID" + ); + continue; + } + + let gl_shader_program = + match create_shader_program(&curr_gl_ctx, &shader_program) { + Ok(gl_shader_program) => gl_shader_program, + Err(err) => { + tracing::error!("Failed to create shader program: {err}"); + continue; + } + }; + + renderer_object_store.insert( + shader_program_obj_id, + RendererObject::from_raw( + gl_shader_program.into_raw(), + RendererObjectKind::ShaderProgram, + ), + ); + } + RendererCommand::ActivateShader(shader_program_obj_id) => { + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + tracing::error!("No GL context is current"); + continue; + }; + + let Some(shader_program_obj) = renderer_object_store + .get_shader_program_obj(&shader_program_obj_id) + else { + tracing::error!( + "Shader object does not exist or has a wrong kind" + ); + continue; + }; + + let gl_shader_program = + GlShaderProgram::from_raw(shader_program_obj.as_raw()); + + gl_shader_program.activate(&curr_gl_ctx); + + activated_gl_shader_program = Some(gl_shader_program); + } + RendererCommand::SetShaderBinding(binding_location, binding_value) => { + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + tracing::error!("No GL context is current"); + continue; + }; + + if activated_gl_shader_program.is_none() { + tracing::error!("No shader program is activated"); + continue; + } + + if let ShaderBindingValue::Texture(texture_asset) = &binding_value { + let Some(texture_obj) = renderer_object_store.get_texture_obj( + &RendererObjectId::Asset(texture_asset.id()), + ) else { + tracing::error!( + "Texture {:?} does not exist in renderer object store", + assets.get_label(texture_asset) + ); + continue; + }; + + let gl_texture = GlTexture::from_raw(texture_obj.as_raw()); + + gl_texture.bind_to_texture_unit( + curr_gl_ctx, + binding_location.binding_index, + ); + + // gl_shader_program.set_uniform_at_location( + // curr_gl_ctx, + // GlUniformLocation::from_number( + // binding_location.binding_index as i32, + // ), + // &binding_location.binding_index, + // ); + + continue; + } + + let binding_index = binding_location.binding_index; + + let uniform_buffer = + uniform_buffer_objs.entry(binding_index).or_insert_with(|| { + let uniform_buf = + opengl_bindings::buffer::Buffer::<u8>::new(curr_gl_ctx); + + uniform_buf + .init( + curr_gl_ctx, + binding_location.binding_size, + opengl_bindings::buffer::Usage::Dynamic, + ) + .unwrap(); + + uniform_buf.bind_to_indexed_target( + curr_gl_ctx, + opengl_bindings::buffer::BindingTarget::UniformBuffer, + binding_index as u32, + ); + + uniform_buf + }); + + let fvec3_value; + + uniform_buffer + .store_at_byte_offset( + curr_gl_ctx, + binding_location.byte_offset, + match binding_value { + ShaderBindingValue::Uint(ref value) => value.as_bytes(), + ShaderBindingValue::Int(ref value) => value.as_bytes(), + ShaderBindingValue::Float(ref value) => value.as_bytes(), + ShaderBindingValue::FVec3(value) => { + fvec3_value = CF32Vec3::from(value); + + fvec3_value.as_bytes() + } + ShaderBindingValue::FMat4x4(ref value) => { + value.items().as_bytes() + } + ShaderBindingValue::Texture(_) => unreachable!(), + }, + ) + .unwrap(); + } + RendererCommand::CreateTexture(texture) => { + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + tracing::error!("No GL context is current"); + continue; + }; + + let Some(texture_image) = assets.get(&texture.asset_handle) else { + tracing::error!("Texture asset is not loaded",); + continue; + }; + + if let Err(err) = create_texture_object( + curr_gl_ctx, + &mut renderer_object_store, + texture.asset_handle.id(), + texture_image, + &texture.properties, + ) { + tracing::error!("Failed to create texture object: {err}"); + } + } + RendererCommand::DrawMesh { mesh_asset } => { + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + tracing::error!("No GL context is current"); + continue; + }; + + // tracing::warn!("ARGH! Drawing mesh"); + + let graphics_mesh = + match graphics_mesh_store.graphics_meshes.get(&mesh_asset.id()) { + Some(graphics_mesh) => graphics_mesh, + None => { + let Some(mesh) = assets.get(&mesh_asset) else { + tracing::trace!("Missing model asset"); + continue; + }; + + let graphics_mesh = + match GraphicsMesh::new(&curr_gl_ctx, mesh) { + Ok(graphics_mesh) => graphics_mesh, + Err(err) => { + tracing::error!( + "Failed to create {}: {err}", + type_name::<GraphicsMesh>() + ); + + continue; + } + }; + + graphics_mesh_store + .graphics_meshes + .entry(mesh_asset.id()) + .or_insert(graphics_mesh) + } + }; + + if let Err(err) = draw_mesh(&curr_gl_ctx, graphics_mesh) { + tracing::error!("Failed to draw mesh: {err}"); + }; + } + RendererCommand::SetPolygonModeConfig(polygon_mode_config) => { + let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { + tracing::error!("No GL context is current"); + continue; + }; + + opengl_bindings::misc::set_polygon_mode( + &curr_gl_ctx, + polygon_mode_config.face, + polygon_mode_config.mode, + ); + } + } + } + } +} - let view_matrix = create_view_matrix(camera, &camera_world_pos.position); +#[tracing::instrument(skip_all)] +fn create_texture_object( + curr_gl_ctx: &CurrentContextWithFns<'_>, + renderer_object_store: &mut RendererObjectStore, + texture_image_asset_id: AssetId, + image: &Image, + texture_properties: &TextureProperties, +) -> Result<(), GlTextureGenerateError> +{ + let object_id = RendererObjectId::Asset(texture_image_asset_id); - gl_shader_program.set_uniform(c"view", &view_matrix); + if renderer_object_store.contains_with_id(&object_id) { + tracing::error!( + texture_object_id=?object_id, + "Renderer object store already contains object with this ID" + ); + return Ok(()); + } - #[allow(clippy::cast_precision_loss)] - let proj_matrix = match &camera.projection { - Projection::Perspective(perspective_proj) => perspective_proj.to_matrix_rh( - window_size.width as f32 / window_size.height as f32, - ClipVolume::NegOneToOne, + renderer_object_store.insert( + object_id, + RendererObject::from_raw( + create_gl_texture(curr_gl_ctx, image, texture_properties)?.into_raw(), + RendererObjectKind::Texture, ), - Projection::Orthographic(orthographic_proj) => orthographic_proj - .to_matrix_rh(&camera_world_pos.position, ClipVolume::NegOneToOne), - }; + ); - gl_shader_program.set_uniform(c"projection", &proj_matrix); + Ok(()) } -fn apply_light<'point_light>( - material: &Material, - material_flags: &MaterialFlags, - global_light: &GlobalLight, - gl_shader_program: &mut GlShaderProgram, - (point_light_iter, point_light_cnt): ( - impl Iterator< - Item = ( - ComponentHandle<'point_light, PointLight>, - ComponentHandle<'point_light, WorldPosition>, - ), - >, - usize, - ), - directional_lights: &[&DirectionalLight], - camera_world_pos: &WorldPosition, -) +fn set_viewport( + current_context: &CurrentContextWithFns<'_>, + position: Vec2<u32>, + size: &Dimens<u32>, +) -> Result<(), GlSetViewportError> { - debug_assert!( - point_light_cnt < 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( - &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(c"directional_light_cnt", &(directional_lights.len() as i32)); + let position = + opengl_bindings::data_types::Vec2::<u32> { x: position.x, y: position.y }; - for (point_light_index, (point_light, point_light_world_pos)) in - point_light_iter.enumerate() - { - gl_shader_program.set_uniform( - &create_light_uniform_name("point_lights", point_light_index, "position"), - &(point_light_world_pos.position + point_light.local_position), - ); + let size = opengl_bindings::data_types::Dimens::<u32> { + width: size.width, + height: size.height, + }; - set_light_phong_uniforms( - gl_shader_program, - "point_lights", - point_light_index, - &*point_light, - ); + opengl_bindings::misc::set_viewport(current_context, &position, &size) +} - set_light_attenuation_uniforms( - gl_shader_program, - "point_lights", - point_light_index, - &*point_light, - ); +fn draw_mesh( + current_context: &CurrentContextWithFns<'_>, + graphics_mesh: &GraphicsMesh, +) -> Result<(), GlDrawError> +{ + graphics_mesh.vertex_arr.bind(current_context); + + if graphics_mesh.index_buffer.is_some() { + VertexArray::draw_elements( + current_context, + PrimitiveKind::Triangles, + 0, + graphics_mesh.element_cnt, + )?; + } else { + VertexArray::draw_arrays( + current_context, + PrimitiveKind::Triangles, + 0, + graphics_mesh.element_cnt, + )?; } - // There probably won't be more than 2147483648 point lights - #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] - gl_shader_program.set_uniform(c"point_light_cnt", &(point_light_cnt as i32)); - - gl_shader_program.set_uniform( - c"material.ambient", - &Vec3::from(if material_flags.use_ambient_color { - material.ambient.clone() - } else { - global_light.ambient.clone() - }), - ); - - gl_shader_program - .set_uniform(c"material.diffuse", &Vec3::from(material.diffuse.clone())); + Ok(()) +} - #[allow(clippy::cast_possible_wrap)] - gl_shader_program - .set_uniform(c"material.specular", &Vec3::from(material.specular.clone())); +fn create_gl_texture( + current_context: &CurrentContextWithFns<'_>, + image: &Image, + texture_properties: &TextureProperties, +) -> Result<GlTexture, GlTextureGenerateError> +{ + let gl_texture = GlTexture::new(current_context); - let texture_map = material - .textures - .iter() - .enumerate() - .map(|(index, texture)| (texture.id(), index)) - .collect::<HashMap<_, _>>(); + gl_texture.generate( + current_context, + &image.dimensions().into(), + image.as_bytes(), + match image.color_type() { + ImageColorType::Rgb8 => GlTexturePixelDataFormat::Rgb8, + ImageColorType::Rgba8 => GlTexturePixelDataFormat::Rgba8, + _ => { + unimplemented!(); + } + }, + )?; - #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform( - c"material.ambient_map", - &(*texture_map.get(&material.ambient_map).unwrap() as i32), + gl_texture.set_wrap( + current_context, + texture_wrapping_to_gl(texture_properties.wrap), ); - #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform( - c"material.diffuse_map", - &(*texture_map.get(&material.diffuse_map).unwrap() as i32), + gl_texture.set_magnifying_filter( + current_context, + texture_filtering_to_gl(texture_properties.magnifying_filter), ); - #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform( - c"material.specular_map", - &(*texture_map.get(&material.specular_map).unwrap() as i32), + gl_texture.set_minifying_filter( + current_context, + texture_filtering_to_gl(texture_properties.minifying_filter), ); - gl_shader_program.set_uniform(c"material.shininess", &material.shininess); - - gl_shader_program.set_uniform(c"view_pos", &camera_world_pos.position); + Ok(gl_texture) } -fn set_light_attenuation_uniforms( - gl_shader_program: &mut GlShaderProgram, - light_array: &str, - light_index: usize, - light: &PointLight, -) +fn create_shader_program( + current_context: &CurrentContextWithFns<'_>, + shader_program: &ShaderProgram, +) -> Result<GlShaderProgram, CreateShaderError> { - gl_shader_program.set_uniform( - &create_light_uniform_name( - light_array, - light_index, - "attenuation_props.constant", - ), - &light.attenuation_params.constant, - ); + let shader_program_reflection = shader_program.reflection(0).expect("Not possible"); - gl_shader_program.set_uniform( - &create_light_uniform_name(light_array, light_index, "attenuation_props.linear"), - &light.attenuation_params.linear, - ); + let (vs_entry_point_index, vs_entry_point_reflection) = shader_program_reflection + .entry_points() + .enumerate() + .find(|(_, entry_point)| entry_point.stage() == ShaderStage::Vertex) + .ok_or_else(|| { + CreateShaderError::NoShaderStageEntrypointFound(ShaderStage::Vertex) + })?; + + let vertex_shader_entry_point_code = shader_program + .get_entry_point_code(vs_entry_point_index.try_into().expect( + "Vertex shader entry point index does not fit in 32-bit unsigned int", + )) + .map_err(|err| CreateShaderError::GetShaderEntryPointCodeFailed { + err, + stage: ShaderStage::Vertex, + entrypoint: vs_entry_point_reflection + .name() + .map(|name| name.to_string().into()) + .unwrap_or("(none)".into()), + })?; + + let (fs_entry_point_index, fs_entry_point_reflection) = shader_program_reflection + .entry_points() + .enumerate() + .find(|(_, entry_point)| entry_point.stage() == ShaderStage::Fragment) + .ok_or_else(|| { + CreateShaderError::NoShaderStageEntrypointFound(ShaderStage::Fragment) + })?; - gl_shader_program.set_uniform( - &create_light_uniform_name( - light_array, - light_index, - "attenuation_props.quadratic", - ), - &light.attenuation_params.quadratic, - ); -} + let fragment_shader_entry_point_code = shader_program + .get_entry_point_code(fs_entry_point_index.try_into().expect( + "Fragment shader entry point index does not fit in 32-bit unsigned int", + )) + .map_err(|err| CreateShaderError::GetShaderEntryPointCodeFailed { + err, + stage: ShaderStage::Fragment, + entrypoint: fs_entry_point_reflection + .name() + .map(|name| name.to_string().into()) + .unwrap_or("(none)".into()), + })?; -fn set_light_phong_uniforms( - gl_shader_program: &mut GlShaderProgram, - light_array: &str, - light_index: usize, - light: &impl Light, -) -{ - gl_shader_program.set_uniform( - &create_light_uniform_name(light_array, light_index, "phong.diffuse"), - &Vec3::from(light.diffuse().clone()), - ); + let vertex_shader = GlShader::new(current_context, ShaderKind::Vertex); - gl_shader_program.set_uniform( - &create_light_uniform_name(light_array, light_index, "phong.specular"), - &Vec3::from(light.specular().clone()), - ); -} + vertex_shader.set_source( + current_context, + &vertex_shader_entry_point_code.as_str().unwrap(), + )?; -trait Light -{ - fn diffuse(&self) -> &Color<f32>; - fn specular(&self) -> &Color<f32>; -} + vertex_shader.compile(current_context)?; -impl Light for PointLight -{ - fn diffuse(&self) -> &Color<f32> - { - &self.diffuse - } + let fragment_shader = GlShader::new(current_context, ShaderKind::Fragment); - fn specular(&self) -> &Color<f32> - { - &self.specular - } -} + fragment_shader.set_source( + current_context, + &fragment_shader_entry_point_code.as_str().unwrap(), + )?; -impl Light for DirectionalLight -{ - fn diffuse(&self) -> &Color<f32> - { - &self.diffuse - } + fragment_shader.compile(current_context)?; - fn specular(&self) -> &Color<f32> - { - &self.specular - } -} + let gl_shader_program = GlShaderProgram::new(current_context); -fn create_light_uniform_name( - light_array: &str, - light_index: usize, - light_field: &str, -) -> CString -{ - unsafe { - CString::from_vec_with_nul_unchecked( - format!("{light_array}[{light_index}].{light_field}\0").into(), - ) - } + gl_shader_program.attach(current_context, &vertex_shader); + gl_shader_program.attach(current_context, &fragment_shader); + + gl_shader_program.link(current_context)?; + + Ok(gl_shader_program) } -fn create_view_matrix(camera: &Camera, camera_pos: &Vec3<f32>) -> Matrix<f32, 4, 4> +#[derive(Debug, thiserror::Error)] +enum CreateShaderError { - let mut view = Matrix::new(); + #[error( + "Failed to get code of shader program entry point {entrypoint} of stage {stage:?}" + )] + GetShaderEntryPointCodeFailed + { + #[source] + err: ShaderError, + stage: ShaderStage, + entrypoint: Cow<'static, str>, + }, - view.look_at(&camera_pos, &camera.target, &camera.global_up); + #[error("No entrypoint was found for shader stage {0:?}")] + NoShaderStageEntrypointFound(ShaderStage), - view + #[error(transparent)] + ShaderError(#[from] GlShaderError), } #[tracing::instrument(skip_all)] @@ -704,7 +1161,7 @@ fn opengl_debug_message_cb( { use std::backtrace::{Backtrace, BacktraceStatus}; - use tracing::{event, Level}; + use tracing::{Level, event}; macro_rules! create_event { ($level: expr) => { @@ -723,7 +1180,8 @@ fn opengl_debug_message_cb( let backtrace = Backtrace::capture(); if matches!(backtrace.status(), BacktraceStatus::Captured) { - event!(Level::TRACE, "{backtrace}"); + tracing::error!("{backtrace}"); + // event!(Level::TRACE, "{backtrace}"); } } MessageType::Other => { @@ -735,19 +1193,100 @@ fn opengl_debug_message_cb( }; } -#[derive(Debug)] -struct Transformation +#[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: ReprC + Copy> From<Matrix<Value, 4, 4>> + for opengl_bindings::data_types::Matrix<Value, 4, 4> +{ + fn from(matrix: Matrix<Value, 4, 4>) -> Self + { + Self { items: matrix.items } + } +} + +impl<Value: Copy> From<Dimens<Value>> for opengl_bindings::data_types::Dimens<Value> +{ + fn from(dimens: Dimens<Value>) -> Self + { + Self { + width: dimens.width, + height: dimens.height, + } + } +} + +impl From<crate::draw_flags::PolygonMode> for opengl_bindings::misc::PolygonMode { - position: Vec3<f32>, - scale: Vec3<f32>, + 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, + } + } } -fn create_transformation_matrix(transformation: Transformation) -> Matrix<f32, 4, 4> +impl From<crate::draw_flags::PolygonModeFace> for opengl_bindings::misc::PolygonModeFace { - let mut matrix = Matrix::new_identity(); + 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, + } + } +} - matrix.translate(&transformation.position); - matrix.scale(&transformation.scale); +#[derive(Debug, IntoBytes, Immutable)] +#[repr(C)] +pub struct CF32Vec3 +{ + x: f32, + y: f32, + z: f32, +} - matrix +impl From<Vec3<f32>> for CF32Vec3 +{ + fn from(src: Vec3<f32>) -> Self + { + Self { x: src.x, y: src.y, z: src.z } + } } diff --git a/engine/src/renderer/opengl/glsl/fragment.glsl b/engine/src/renderer/opengl/glsl/fragment.glsl deleted file mode 100644 index 5bf5ff2..0000000 --- a/engine/src/renderer/opengl/glsl/fragment.glsl +++ /dev/null @@ -1,73 +0,0 @@ -#version 330 core - -#preinclude "light.glsl" -#preinclude "vertex_data.glsl" - -#define MAX_LIGHT_CNT 64 - -out vec4 FragColor; - -in VertexData vertex_data; - -uniform vec3 view_pos; -uniform sampler2D input_texture; -uniform Material material; - -uniform PointLight point_lights[MAX_LIGHT_CNT]; -uniform int point_light_cnt; - -uniform DirectionalLight directional_lights[MAX_LIGHT_CNT]; -uniform int directional_light_cnt; - -void main() -{ - vec3 ambient_light = calc_ambient_light(material, vertex_data.texture_coords); - - vec3 directional_light_sum = vec3(0.0, 0.0, 0.0); - - for (int dl_index = 0; dl_index < directional_light_cnt; dl_index++) { - CalculatedLight calculated_dir_light; - - calc_light( - // Negated since we want the light to point from the light direction - normalize(-directional_lights[dl_index].direction), - directional_lights[dl_index].phong, - vertex_data, - view_pos, - material, - calculated_dir_light - ); - - directional_light_sum += - calculated_dir_light.diffuse + calculated_dir_light.specular; - } - - vec3 point_light_sum = vec3(0.0, 0.0, 0.0); - - for (int pl_index = 0; pl_index < point_light_cnt; pl_index++) { - vec3 light_direction = - normalize(point_lights[pl_index].position - vertex_data.world_space_pos); - - CalculatedLight calculated_point_light; - - calc_light( - light_direction, - point_lights[pl_index].phong, - vertex_data, - view_pos, - material, - calculated_point_light - ); - - float attenuation = - calc_attenuation(point_lights[pl_index], vertex_data.world_space_pos); - - calculated_point_light.diffuse *= attenuation; - calculated_point_light.specular *= attenuation; - - point_light_sum += - calculated_point_light.diffuse + calculated_point_light.specular; - } - - FragColor = vec4((ambient_light + directional_light_sum + point_light_sum), 1.0); -} diff --git a/engine/src/renderer/opengl/glsl/light.glsl b/engine/src/renderer/opengl/glsl/light.glsl deleted file mode 100644 index f12b5fe..0000000 --- a/engine/src/renderer/opengl/glsl/light.glsl +++ /dev/null @@ -1,133 +0,0 @@ -#version 330 core - -#ifndef LIGHT_GLSL -#define LIGHT_GLSL - -#preinclude "vertex_data.glsl" - -struct Material -{ - vec3 ambient; - vec3 diffuse; - vec3 specular; - sampler2D ambient_map; - sampler2D diffuse_map; - sampler2D specular_map; - float shininess; -}; - -struct LightPhong -{ - vec3 diffuse; - vec3 specular; -}; - -struct AttenuationProperties -{ - float constant; - float linear; - float quadratic; -}; - -struct PointLight -{ - LightPhong phong; - vec3 position; - AttenuationProperties attenuation_props; -}; - -struct DirectionalLight -{ - LightPhong phong; - vec3 direction; -}; - -struct CalculatedLight -{ - vec3 diffuse; - vec3 specular; -}; - -vec3 calc_ambient_light(in Material material, in vec2 texture_coords) -{ - return vec3(texture(material.ambient_map, texture_coords)) * material.ambient; -} - -vec3 calc_diffuse_light( - in Material material, - in LightPhong light_phong, - in vec3 light_dir, - in vec3 norm, - in vec2 texture_coords -) -{ - float diff = max(dot(norm, light_dir), 0.0); - - return light_phong.diffuse * ( - diff * (vec3(texture(material.diffuse_map, texture_coords)) * material.diffuse) - ); -} - -vec3 calc_specular_light( - in Material material, - in LightPhong light_phong, - in vec3 light_dir, - in vec3 norm, - in vec3 view_pos, - in vec3 frag_pos, - in vec2 texture_coords -) -{ - vec3 view_direction = normalize(view_pos - frag_pos); - - vec3 halfway_direction = normalize(light_dir + view_direction); - - float spec = - pow(max(dot(norm, halfway_direction), 0.0), material.shininess); - - return light_phong.specular * ( - spec * (vec3(texture(material.specular_map, texture_coords)) * material.specular) - ); -} - -float calc_attenuation(in PointLight point_light, in vec3 position) -{ - float light_distance = length(point_light.position - position); - - return 1.0 / (point_light.attenuation_props.constant - + point_light.attenuation_props.linear - * light_distance + point_light.attenuation_props.quadratic - * pow(light_distance, 2)); -} - -void calc_light( - in vec3 light_direction, - in LightPhong light_phong, - in VertexData vertex_data, - in vec3 view_pos, - in Material material, - out CalculatedLight calculated_light -) -{ - vec3 norm = normalize(vertex_data.world_space_normal); - - calculated_light.diffuse = calc_diffuse_light( - material, - light_phong, - light_direction, - norm, - vertex_data.texture_coords - ); - - calculated_light.specular = calc_specular_light( - material, - light_phong, - light_direction, - norm, - view_pos, - vertex_data.world_space_pos, - vertex_data.texture_coords - ); -} - -#endif diff --git a/engine/src/renderer/opengl/glsl/vertex.glsl b/engine/src/renderer/opengl/glsl/vertex.glsl deleted file mode 100644 index b57caa6..0000000 --- a/engine/src/renderer/opengl/glsl/vertex.glsl +++ /dev/null @@ -1,24 +0,0 @@ -#version 330 core - -#preinclude "vertex_data.glsl" - -layout (location = 0) in vec3 pos; -layout (location = 1) in vec2 texture_coords; -layout (location = 2) in vec3 normal; - -out VertexData vertex_data; - -uniform mat4 model; -uniform mat4 view; -uniform mat4 projection; - -void main() -{ - gl_Position = projection * view * model * vec4(pos, 1.0); - - vertex_data.world_space_pos = vec3(model * vec4(pos, 1.0)); - vertex_data.texture_coords = texture_coords; - - // TODO: Do this using CPU for performance increase - vertex_data.world_space_normal = mat3(transpose(inverse(model))) * normal; -} diff --git a/engine/src/renderer/opengl/glsl/vertex_data.glsl b/engine/src/renderer/opengl/glsl/vertex_data.glsl deleted file mode 100644 index 486d445..0000000 --- a/engine/src/renderer/opengl/glsl/vertex_data.glsl +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef VERTEX_DATA_GLSL -#define VERTEX_DATA_GLSL - -struct VertexData -{ - vec2 texture_coords; - vec3 world_space_pos; - vec3 world_space_normal; -}; - -#endif diff --git a/engine/src/renderer/opengl/glutin_compat.rs b/engine/src/renderer/opengl/glutin_compat.rs new file mode 100644 index 0000000..cfd6ea7 --- /dev/null +++ b/engine/src/renderer/opengl/glutin_compat.rs @@ -0,0 +1,268 @@ +// Original file: +// https://github.com/rust-windowing/glutin/blob/ +// 0433af9018febe0696c485ed9d66c40dad41f2d4/glutin-winit/src/lib.rs +// +// Copyright © 2022 Kirill Chibisov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the “Software”), to deal +// in the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +//! This library provides helpers for cross-platform [`glutin`] bootstrapping +//! with [`winit`]. + +#![deny(rust_2018_idioms)] +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(clippy::all)] +#![deny(missing_debug_implementations)] +#![deny(missing_docs)] +#![cfg_attr(clippy, deny(warnings))] + +use glutin::config::{Config, ConfigTemplateBuilder}; +use glutin::display::{Display, DisplayApiPreference}; +use glutin::error::Error as GlutinError; +#[cfg(x11_platform)] +use glutin::platform::x11::X11GlConfigExt; +use glutin::prelude::*; +use raw_window_handle::{DisplayHandle, RawWindowHandle, WindowHandle}; + +use crate::windowing::window::CreationAttributes as WindowCreationAttributes; + +#[cfg(all(not(egl_backend), not(glx_backend), not(wgl_backend), not(cgl_backend)))] +compile_error!("Please select at least one api backend"); + +/// The helper to perform [`Display`] creation and OpenGL platform +/// bootstrapping with the help of [`winit`] with little to no platform specific +/// code. +/// +/// This is only required for the initial setup. If you want to create +/// additional windows just use the [`finalize_window`] function and the +/// configuration you've used either for the original window or picked with the +/// existing [`Display`]. +/// +/// [`winit`]: winit +/// [`Display`]: glutin::display::Display +#[derive(Default, Debug, Clone)] +pub struct DisplayBuilder +{ + preference: ApiPreference, + window_attributes: WindowCreationAttributes, +} + +impl DisplayBuilder +{ + /// Create new display builder. + pub fn new() -> Self + { + Default::default() + } + + /// The preference in picking the configuration. + #[allow(dead_code)] + pub fn with_preference(mut self, preference: ApiPreference) -> Self + { + self.preference = preference; + self + } + + /// The window attributes to use when building a window. + /// + /// By default no window is created. + pub fn with_window_attributes( + mut self, + window_creation_attrs: WindowCreationAttributes, + ) -> Self + { + self.window_attributes = window_creation_attrs; + self + } + + /// Initialize the OpenGL platform and create a compatible window to use + /// with it when the [`WindowAttributes`] was passed with + /// [`Self::with_window_attributes()`]. It's optional, since on some + /// platforms like `Android` it is not available early on, so you want to + /// find configuration and later use it with the [`finalize_window`]. + /// But if you don't care about such platform you can always pass + /// [`WindowAttributes`]. + /// + /// # Api-specific + /// + /// **WGL:** - [`WindowAttributes`] **must** be passed in + /// [`Self::with_window_attributes()`] if modern OpenGL(ES) is desired, + /// otherwise only builtin functions like `glClear` will be available. + pub fn build<ConfigPickerFn>( + self, + window_handle: Option<WindowHandle<'_>>, + display_handle: &DisplayHandle<'_>, + template_builder: ConfigTemplateBuilder, + config_picker_fn: ConfigPickerFn, + ) -> Result<(WindowCreationAttributes, Config), Error> + where + ConfigPickerFn: FnOnce(Box<dyn Iterator<Item = Config> + '_>) -> Option<Config>, + { + // XXX with WGL backend window should be created first. + let raw_window_handle = if cfg!(wgl_backend) { + let Some(window_handle) = window_handle else { + return Err(Error::WindowRequired); + }; + + Some(window_handle.as_raw()) + } else { + None + }; + + let gl_display = + create_display(display_handle, self.preference, raw_window_handle) + .map_err(Error::CreateDisplayFailed)?; + + // XXX the native window must be passed to config picker when WGL is used + // otherwise very limited OpenGL features will be supported. + #[cfg(wgl_backend)] + let template_builder = if let Some(raw_window_handle) = raw_window_handle { + template_builder.compatible_with_native_window(raw_window_handle) + } else { + template_builder + }; + + let template = template_builder.build(); + + // SAFETY: The RawWindowHandle passed on the config template + // (when cfg(wgl_backend)) will always point to a valid object since it is + // derived from the window_handle argument which when Some is a WindowHandle and + // WindowHandles always point to a valid object + let gl_configs = unsafe { gl_display.find_configs(template) } + .map_err(Error::FindConfigsFailed)?; + + let picked_gl_config = + config_picker_fn(gl_configs).ok_or(Error::NoConfigPicked)?; + + #[cfg(not(wgl_backend))] + let window_attrs = + { finalize_window_creation_attrs(self.window_attributes, &picked_gl_config) }; + + #[cfg(wgl_backend)] + let window_attrs = self.window_attributes; + + Ok((window_attrs, picked_gl_config)) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error("Failed to create display")] + CreateDisplayFailed(#[source] GlutinError), + + #[error("Failed to find configs")] + FindConfigsFailed(#[source] GlutinError), + + #[error("No config was picked by config picker function")] + NoConfigPicked, + + #[error("Window required for building display on current platform")] + WindowRequired, +} + +fn create_display( + display_handle: &DisplayHandle<'_>, + _api_preference: ApiPreference, + _raw_window_handle: Option<RawWindowHandle>, +) -> Result<Display, GlutinError> +{ + #[cfg(egl_backend)] + let _preference = DisplayApiPreference::Egl; + + #[cfg(glx_backend)] + let _preference = DisplayApiPreference::Glx(Box::new( + crate::windowing::window::platform::x11::register_xlib_error_hook, + )); + + #[cfg(cgl_backend)] + let _preference = DisplayApiPreference::Cgl; + + #[cfg(wgl_backend)] + let _preference = DisplayApiPreference::Wgl(_raw_window_handle); + + #[cfg(all(egl_backend, glx_backend))] + let _preference = match _api_preference { + ApiPreference::PreferEgl => DisplayApiPreference::EglThenGlx(Box::new( + crate::windowing::window::platform::x11::register_xlib_error_hook, + )), + ApiPreference::FallbackEgl => DisplayApiPreference::GlxThenEgl(Box::new( + crate::windowing::window::platform::x11::register_xlib_error_hook, + )), + }; + + #[cfg(all(wgl_backend, egl_backend))] + let _preference = match _api_preference { + ApiPreference::PreferEgl => DisplayApiPreference::EglThenWgl(_raw_window_handle), + ApiPreference::FallbackEgl => { + DisplayApiPreference::WglThenEgl(_raw_window_handle) + } + }; + + let handle = display_handle.as_raw(); + unsafe { Ok(Display::new(handle, _preference)?) } +} + +/// Finalize [`Window`] creation by applying the options from the [`Config`], be +/// aware that it could remove incompatible options from the window builder like +/// `transparency`, when the provided config doesn't support it. +/// +/// [`Window`]: winit::window::Window +/// [`Config`]: glutin::config::Config +#[cfg(not(wgl_backend))] +fn finalize_window_creation_attrs( + mut attributes: WindowCreationAttributes, + gl_config: &Config, +) -> WindowCreationAttributes +{ + // Disable transparency if the end config doesn't support it. + if gl_config.supports_transparency() == Some(false) { + attributes = attributes.with_transparent(false); + } + + #[cfg(x11_platform)] + let attributes = if let Some(x11_visual) = gl_config.x11_visual() { + attributes.with_x11_visual(x11_visual.visual_id() as _) + } else { + attributes + }; + + attributes +} + +/// Simplified version of the [`DisplayApiPreference`] which is used to simplify +/// cross platform window creation. +/// +/// To learn about platform differences the [`DisplayApiPreference`] variants. +/// +/// [`DisplayApiPreference`]: glutin::display::DisplayApiPreference +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ApiPreference +{ + /// Prefer `EGL` over system provider like `GLX` and `WGL`. + PreferEgl, + + /// Fallback to `EGL` when failed to create the system profile. + /// + /// This behavior is used by default. However consider using + /// [`Self::PreferEgl`] if you don't care about missing EGL features. + #[default] + FallbackEgl, +} diff --git a/engine/src/renderer/opengl/graphics_mesh.rs b/engine/src/renderer/opengl/graphics_mesh.rs new file mode 100644 index 0000000..62dadb8 --- /dev/null +++ b/engine/src/renderer/opengl/graphics_mesh.rs @@ -0,0 +1,136 @@ +use opengl_bindings::CurrentContextWithFns as GlCurrentContextWithFns; +use opengl_bindings::buffer::{Buffer as GlBuffer, Usage as GlBufferUsage}; +use opengl_bindings::vertex_array::{ + AttributeFormat as GlVertexArrayAttributeFormat, + DataType as GlVertexArrayDataType, + VertexArray as GlVertexArray, +}; + +use crate::mesh::Mesh; +use crate::renderer::opengl::vertex::{ + AttributeComponentType as VertexAttributeComponentType, + Vertex as RendererVertex, +}; + +#[derive(Debug)] +pub struct GraphicsMesh +{ + /// Vertex and index buffer has to live as long as the vertex array + vertex_buffer: GlBuffer<RendererVertex>, + pub index_buffer: Option<GlBuffer<u32>>, + pub element_cnt: u32, + pub vertex_arr: GlVertexArray, +} + +impl GraphicsMesh +{ + #[tracing::instrument(skip_all)] + pub fn new( + current_context: &GlCurrentContextWithFns<'_>, + mesh: &Mesh, + ) -> Result<Self, Error> + { + tracing::info!( + "Creating vertex array, vertex buffer{}", + if mesh.indices().is_some() { + " and index buffer" + } else { + "" + } + ); + + let vertex_arr = GlVertexArray::new(current_context); + let vertex_buffer = GlBuffer::new(current_context); + + vertex_buffer + .store_mapped( + current_context, + mesh.vertices(), + GlBufferUsage::Static, + |vertex| RendererVertex { + pos: vertex.pos.into(), + texture_coords: vertex.texture_coords.into(), + normal: vertex.normal.into(), + }, + ) + .map_err(Error::StoreVerticesFailed)?; + + vertex_arr.bind_vertex_buffer(current_context, 0, &vertex_buffer, 0); + + let mut offset = 0u32; + + for attrib in RendererVertex::attrs() { + vertex_arr.enable_attrib(current_context, attrib.index); + + vertex_arr.set_attrib_format( + current_context, + attrib.index, + GlVertexArrayAttributeFormat { + data_type: match attrib.component_type { + VertexAttributeComponentType::Float => { + GlVertexArrayDataType::Float + } + }, + count: attrib.component_cnt as u8, + normalized: false, + offset, + }, + ); + + vertex_arr.set_attrib_vertex_buf_binding(current_context, attrib.index, 0); + + offset += attrib.component_size * attrib.component_cnt as u32; + } + + if let Some(indices) = mesh.indices() { + let index_buffer = GlBuffer::new(current_context); + + index_buffer + .store(current_context, indices, GlBufferUsage::Static) + .map_err(Error::StoreIndicesFailed)?; + + vertex_arr.bind_element_buffer(current_context, &index_buffer); + + return Ok(Self { + vertex_buffer: vertex_buffer, + index_buffer: Some(index_buffer), + element_cnt: indices + .len() + .try_into() + .expect("Mesh index count does not fit into a 32-bit unsigned int"), + vertex_arr, + }); + } + + Ok(Self { + vertex_buffer: vertex_buffer, + index_buffer: None, + element_cnt: mesh + .vertices() + .len() + .try_into() + .expect("Mesh vertex count does not fit into a 32-bit unsigned int"), + vertex_arr, + }) + } + + pub fn destroy(&mut self, curr_gl_ctx: &GlCurrentContextWithFns<'_>) + { + self.vertex_arr.delete(curr_gl_ctx); + self.vertex_buffer.delete(curr_gl_ctx); + + if let Some(index_buffer) = &self.index_buffer { + index_buffer.delete(curr_gl_ctx); + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error("Failed to store vertices in vertex buffer")] + StoreVerticesFailed(#[source] opengl_bindings::buffer::Error), + + #[error("Failed to store indices in index buffer")] + StoreIndicesFailed(#[source] opengl_bindings::buffer::Error), +} diff --git a/engine/src/renderer/opengl/vertex.rs b/engine/src/renderer/opengl/vertex.rs index 499b94b..5a1593e 100644 --- a/engine/src/renderer/opengl/vertex.rs +++ b/engine/src/renderer/opengl/vertex.rs @@ -1,12 +1,13 @@ -use crate::vector::{Vec2, Vec3}; +use safer_ffi::derive_ReprC; #[derive(Debug, Clone)] +#[derive_ReprC] #[repr(C)] pub struct Vertex { - pub pos: Vec3<f32>, - pub texture_coords: Vec2<f32>, - pub normal: Vec3<f32>, + pub pos: opengl_bindings::data_types::Vec3<f32>, + pub texture_coords: opengl_bindings::data_types::Vec2<f32>, + pub normal: opengl_bindings::data_types::Vec3<f32>, } impl Vertex diff --git a/engine/src/shader.rs b/engine/src/shader.rs new file mode 100644 index 0000000..65872f1 --- /dev/null +++ b/engine/src/shader.rs @@ -0,0 +1,818 @@ +use std::any::type_name; +use std::borrow::Cow; +use std::collections::HashMap; +use std::fmt::Debug; +use std::path::Path; +use std::str::Utf8Error; + +use bitflags::bitflags; +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{Phase, START as START_PHASE}; +use ecs::sole::Single; +use ecs::{Component, Sole, declare_entity}; +use shader_slang::{ + Blob as SlangBlob, + ComponentType as SlangComponentType, + DebugInfoLevel as SlangDebugInfoLevel, + EntryPoint as SlangEntryPoint, + GlobalSession as SlangGlobalSession, + Module as SlangModule, + ParameterCategory as SlangParameterCategory, + Session as SlangSession, + TypeKind as SlangTypeKind, +}; + +use crate::asset::{ + Assets, + Event as AssetEvent, + HANDLE_ASSETS_PHASE, + Handle as AssetHandle, + Id as AssetId, + Submitter as AssetSubmitter, +}; +use crate::builder; +use crate::renderer::PRE_RENDER_PHASE; +use crate::shader::default::{ + ASSET_LABEL, + enqueue_set_shader_bindings as default_shader_enqueue_set_shader_bindings, +}; + +pub mod cursor; +pub mod default; + +#[derive(Debug, Clone, Component)] +pub struct Shader +{ + pub asset_handle: AssetHandle<ModuleSource>, +} + +/// Shader module. +#[derive(Debug)] +pub struct ModuleSource +{ + pub name: Cow<'static, str>, + pub file_path: Cow<'static, Path>, + pub source: Cow<'static, str>, + pub link_entrypoints: EntrypointFlags, +} + +bitflags! { + #[derive(Debug, Clone, Copy, Default)] + pub struct EntrypointFlags: usize + { + const FRAGMENT = 1 << 0; + const VERTEX = 1 << 1; + } +} + +pub struct Module +{ + inner: SlangModule, +} + +impl Module +{ + pub fn entry_points(&self) -> impl ExactSizeIterator<Item = EntryPoint> + { + self.inner + .entry_points() + .map(|entry_point| EntryPoint { inner: entry_point }) + } + + pub fn get_entry_point(&self, entry_point: &str) -> Option<EntryPoint> + { + let entry_point = self.inner.find_entry_point_by_name(entry_point)?; + + Some(EntryPoint { inner: entry_point }) + } +} + +pub struct EntryPoint +{ + inner: SlangEntryPoint, +} + +impl EntryPoint +{ + pub fn function_name(&self) -> Option<&str> + { + self.inner.function_reflection().name() + } +} + +pub struct EntryPointReflection<'a> +{ + inner: &'a shader_slang::reflection::EntryPoint, +} + +impl<'a> EntryPointReflection<'a> +{ + pub fn name(&self) -> Option<&str> + { + self.inner.name() + } + + pub fn name_override(&self) -> Option<&str> + { + self.inner.name_override() + } + + pub fn stage(&self) -> Stage + { + Stage::from_slang_stage(self.inner.stage()) + } + + pub fn parameters(&self) -> impl ExactSizeIterator<Item = VariableLayout<'a>> + { + self.inner + .parameters() + .map(|param| VariableLayout { inner: param }) + } + + pub fn var_layout(&self) -> Option<VariableLayout<'a>> + { + Some(VariableLayout { inner: self.inner.var_layout()? }) + } +} + +#[derive(Clone)] +pub struct Program +{ + inner: SlangComponentType, +} + +impl Program +{ + pub fn link(&self) -> Result<Program, Error> + { + let linked_program = self.inner.link()?; + + Ok(Program { inner: linked_program }) + } + + pub fn get_entry_point_code(&self, entry_point_index: u32) -> Result<Blob, Error> + { + let blob = self.inner.entry_point_code(entry_point_index.into(), 0)?; + + Ok(Blob { inner: blob }) + } + + pub fn reflection(&self, target: u32) -> Result<ProgramReflection<'_>, Error> + { + let reflection = self.inner.layout(target as i64)?; + + Ok(ProgramReflection { inner: reflection }) + } +} + +impl Debug for Program +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + formatter + .debug_struct(type_name::<Self>()) + .finish_non_exhaustive() + } +} + +pub struct ProgramReflection<'a> +{ + inner: &'a shader_slang::reflection::Shader, +} + +impl<'a> ProgramReflection<'a> +{ + pub fn get_entry_point_by_index(&self, index: u32) + -> Option<EntryPointReflection<'a>> + { + Some(EntryPointReflection { + inner: self.inner.entry_point_by_index(index)?, + }) + } + + pub fn get_entry_point_by_name(&self, name: &str) + -> Option<EntryPointReflection<'a>> + { + Some(EntryPointReflection { + inner: self.inner.find_entry_point_by_name(name)?, + }) + } + + pub fn entry_points(&self) + -> impl ExactSizeIterator<Item = EntryPointReflection<'a>> + { + self.inner + .entry_points() + .map(|entry_point| EntryPointReflection { inner: entry_point }) + } + + pub fn global_params_type_layout(&self) -> Option<TypeLayout<'a>> + { + Some(TypeLayout { + inner: self.inner.global_params_type_layout()?, + }) + } + + pub fn global_params_var_layout(&self) -> Option<VariableLayout<'a>> + { + Some(VariableLayout { + inner: self.inner.global_params_var_layout()?, + }) + } + + pub fn get_type(&self, name: &str) -> Option<TypeReflection<'a>> + { + Some(TypeReflection { + inner: self.inner.find_type_by_name(name)?, + }) + } + + pub fn get_type_layout(&self, ty: &TypeReflection<'a>) -> Option<TypeLayout<'a>> + { + Some(TypeLayout { + inner: self + .inner + .type_layout(&ty.inner, shader_slang::LayoutRules::Default)?, + }) + } +} + +#[derive(Clone, Copy)] +pub struct VariableLayout<'a> +{ + inner: &'a shader_slang::reflection::VariableLayout, +} + +impl<'a> VariableLayout<'a> +{ + pub fn name(&self) -> Option<&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 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 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 binding_range_descriptor_set_index(&self, index: i64) -> i64 + { + self.inner.binding_range_descriptor_set_index(index) + } + + pub fn get_field_binding_range_offset_by_name(&self, name: &str) -> Option<u64> + { + let field_index = self.inner.find_field_index_by_name(name); + + if field_index < 0 { + return None; + } + + let field_binding_range_offset = + self.inner.field_binding_range_offset(field_index); + + if field_binding_range_offset < 0 { + return None; + } + + Some(field_binding_range_offset.cast_unsigned()) + } + + pub fn ty(&self) -> Option<TypeReflection<'a>> + { + self.inner.ty().map(|ty| TypeReflection { inner: ty }) + } + + pub fn fields(&self) -> impl ExactSizeIterator<Item = VariableLayout<'a>> + { + self.inner + .fields() + .map(|field| VariableLayout { inner: field }) + } + + pub fn field_cnt(&self) -> u32 + { + self.inner.field_count() + } + + pub fn element_type_layout(&self) -> Option<TypeLayout<'a>> + { + self.inner + .element_type_layout() + .map(|type_layout| TypeLayout { inner: type_layout }) + } + + pub fn element_var_layout(&self) -> Option<VariableLayout<'a>> + { + self.inner + .element_var_layout() + .map(|var_layout| VariableLayout { inner: var_layout }) + } + + pub fn container_var_layout(&self) -> Option<VariableLayout<'a>> + { + self.inner + .container_var_layout() + .map(|var_layout| VariableLayout { inner: var_layout }) + } + + pub fn uniform_size(&self) -> Option<usize> + { + // tracing::debug!( + // "uniform_size: {:?} categories: {:?}", + // self.inner.name(), + // self.inner.categories().collect::<Vec<_>>(), + // ); + + if !self + .inner + .categories() + .any(|category| category == SlangParameterCategory::Uniform) + { + return None; + } + + // let category = self.inner.categories().next().unwrap(); + + // println!( + // "AARGH size Category: {category:?} Category count: {}", + // self.inner.category_count() + // ); + + // Some(self.inner.size(category)) + + Some(self.inner.size(SlangParameterCategory::Uniform)) + } + + pub fn stride(&self) -> usize + { + self.inner.stride(self.inner.categories().next().unwrap()) + } +} + +pub struct TypeReflection<'a> +{ + inner: &'a shader_slang::reflection::Type, +} + +impl TypeReflection<'_> +{ + pub fn kind(&self) -> TypeKind + { + TypeKind::from_slang_type_kind(self.inner.kind()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum TypeKind +{ + None, + Struct, + Enum, + Array, + Matrix, + Vector, + Scalar, + ConstantBuffer, + Resource, + SamplerState, + TextureBuffer, + ShaderStorageBuffer, + ParameterBlock, + GenericTypeParameter, + Interface, + OutputStream, + MeshOutput, + Specialized, + Feedback, + Pointer, + DynamicResource, + Count, +} + +impl TypeKind +{ + fn from_slang_type_kind(type_kind: SlangTypeKind) -> Self + { + match type_kind { + SlangTypeKind::None => Self::None, + SlangTypeKind::Struct => Self::Struct, + SlangTypeKind::Enum => Self::Enum, + SlangTypeKind::Array => Self::Array, + SlangTypeKind::Matrix => Self::Matrix, + SlangTypeKind::Vector => Self::Vector, + SlangTypeKind::Scalar => Self::Scalar, + SlangTypeKind::ConstantBuffer => Self::ConstantBuffer, + SlangTypeKind::Resource => Self::Resource, + SlangTypeKind::SamplerState => Self::SamplerState, + SlangTypeKind::TextureBuffer => Self::TextureBuffer, + SlangTypeKind::ShaderStorageBuffer => Self::ShaderStorageBuffer, + SlangTypeKind::ParameterBlock => Self::ParameterBlock, + SlangTypeKind::GenericTypeParameter => Self::GenericTypeParameter, + SlangTypeKind::Interface => Self::Interface, + SlangTypeKind::OutputStream => Self::OutputStream, + SlangTypeKind::MeshOutput => Self::MeshOutput, + SlangTypeKind::Specialized => Self::Specialized, + SlangTypeKind::Feedback => Self::Feedback, + SlangTypeKind::Pointer => Self::Pointer, + SlangTypeKind::DynamicResource => Self::DynamicResource, + SlangTypeKind::Count => Self::Count, + } + } +} + +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) + } + + pub fn compose_into_program( + &self, + modules: &[&Module], + entry_points: &[&EntryPoint], + ) -> Result<Program, Error> + { + let components = + modules + .iter() + .map(|module| SlangComponentType::from(module.inner.clone())) + .chain(entry_points.iter().map(|entry_point| { + SlangComponentType::from(entry_point.inner.clone()) + })) + .collect::<Vec<_>>(); + + let program = self.session.create_composite_component_type(&components)?; + + Ok(Program { inner: program }) + } +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct Error(#[from] shader_slang::Error); + +pub(crate) fn add_asset_importers(assets: &mut Assets) +{ + assets.set_importer::<_, _>(["slang"], import_slang_asset); +} + +fn import_slang_asset( + asset_submitter: &mut AssetSubmitter<'_>, + file_path: &Path, + settings: Option<&'_ Settings>, +) -> Result<(), ImportError> +{ + let file_name = file_path + .file_name() + .ok_or(ImportError::NoPathFileName)? + .to_str() + .ok_or(ImportError::PathFileNameNotUtf8)?; + + let file_path_canonicalized = file_path + .canonicalize() + .map_err(ImportError::CanonicalizePathFailed)?; + + asset_submitter.submit_store(ModuleSource { + name: file_name.to_owned().into(), + file_path: file_path_canonicalized.into(), + source: std::fs::read_to_string(file_path) + .map_err(ImportError::ReadFileFailed)? + .into(), + link_entrypoints: settings + .map(|settings| settings.link_entrypoints) + .unwrap_or_default(), + }); + + Ok(()) +} + +#[derive(Debug, thiserror::Error)] +enum ImportError +{ + #[error("Failed to read file")] + ReadFileFailed(#[source] std::io::Error), + + #[error("Asset path does not have a file name")] + NoPathFileName, + + #[error("Asset path file name is not valid UTF8")] + PathFileNameNotUtf8, + + #[error("Failed to canonicalize asset path")] + CanonicalizePathFailed(#[source] std::io::Error), +} + +declare_entity!( + IMPORT_SHADERS_PHASE, + ( + Phase, + Pair::builder() + .relation::<ChildOf>() + .target_id(*HANDLE_ASSETS_PHASE) + .build() + ) +); + +pub(crate) struct Extension; + +impl ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: ecs::extension::Collector<'_>) + { + let Some(global_session) = SlangGlobalSession::new() else { + tracing::error!("Unable to create global shader-slang session"); + return; + }; + + let session_options = shader_slang::CompilerOptions::default() + .optimization(shader_slang::OptimizationLevel::None) + .matrix_layout_column(true) + .debug_information(SlangDebugInfoLevel::Maximal) + .no_mangle(true); + + let target_desc = shader_slang::TargetDesc::default() + .format(shader_slang::CompileTarget::Glsl) + // .format(shader_slang::CompileTarget::Spirv) + .profile(global_session.find_profile("glsl_330")); + // .profile(global_session.find_profile("spirv_1_5")); + + let targets = [target_desc]; + + let session_desc = shader_slang::SessionDesc::default() + .targets(&targets) + .search_paths(&[""]) + .options(&session_options); + + let Some(session) = global_session.create_session(&session_desc) else { + tracing::error!("Failed to create shader-slang session"); + return; + }; + + collector + .add_sole(Context { + _global_session: global_session, + session, + modules: HashMap::new(), + programs: HashMap::new(), + }) + .ok(); + + collector.add_declared_entity(&IMPORT_SHADERS_PHASE); + + collector.add_system(*START_PHASE, initialize); + collector.add_system(*IMPORT_SHADERS_PHASE, load_modules); + + collector.add_system( + *PRE_RENDER_PHASE, + default_shader_enqueue_set_shader_bindings, + ); + } +} + +fn initialize(mut assets: Single<Assets>) +{ + assets.store_with_label( + ASSET_LABEL.clone(), + ModuleSource { + name: "default_shader.slang".into(), + file_path: Path::new("@engine/default_shader").into(), + source: include_str!("../res/default_shader.slang").into(), + link_entrypoints: EntrypointFlags::VERTEX | EntrypointFlags::FRAGMENT, + }, + ); +} + +#[tracing::instrument(skip_all)] +fn load_modules(mut context: Single<Context>, assets: Single<Assets>) +{ + for AssetEvent::Stored(asset_id, asset_label) in assets.events().last_tick_events() { + let asset_handle = AssetHandle::<ModuleSource>::from_id(*asset_id); + + if !assets.is_loaded_and_has_type(&asset_handle) { + continue; + } + + let Some(module_source) = assets.get(&asset_handle) else { + unreachable!(); + }; + + tracing::debug!(asset_label=?asset_label, "Loading shader module"); + + let module = match load_module(&context.session, module_source) { + Ok(module) => module, + Err(err) => { + tracing::error!("Failed to load shader module: {err}"); + continue; + } + }; + + if !module_source.link_entrypoints.is_empty() { + assert!(context.programs.get(asset_id).is_none()); + + let Some(vertex_shader_entry_point) = module.get_entry_point("vertex_main") + else { + tracing::error!( + "Shader module does not contain a vertex shader entry point" + ); + continue; + }; + + let Some(fragment_shader_entry_point) = + module.get_entry_point("fragment_main") + else { + tracing::error!( + "Shader module don't contain a fragment_main entry point" + ); + continue; + }; + + let shader_program = match context.compose_into_program( + &[&module], + &[&vertex_shader_entry_point, &fragment_shader_entry_point], + ) { + Ok(shader_program) => shader_program, + Err(err) => { + tracing::error!("Failed to compose shader into program: {err}"); + continue; + } + }; + + let linked_shader_program = match shader_program.link() { + Ok(linked_shader_program) => linked_shader_program, + Err(err) => { + tracing::error!("Failed to link shader: {err}"); + continue; + } + }; + + context.programs.insert(*asset_id, linked_shader_program); + } + + context.modules.insert(*asset_id, module); + } +} + +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/shader/cursor.rs b/engine/src/shader/cursor.rs new file mode 100644 index 0000000..b5ba4e0 --- /dev/null +++ b/engine/src/shader/cursor.rs @@ -0,0 +1,160 @@ +use crate::asset::Handle as AssetHandle; +use crate::image::Image; +use crate::matrix::Matrix; +use crate::shader::{TypeKind, TypeLayout, VariableLayout}; +use crate::vector::Vec3; + +/// Shader cursor +#[derive(Clone)] +pub struct Cursor<'a> +{ + type_layout: TypeLayout<'a>, + binding_location: BindingLocation, +} + +impl<'a> Cursor<'a> +{ + pub fn new(var_layout: VariableLayout<'a>) -> Self + { + let binding_location = BindingLocation { + binding_index: var_layout.binding_index(), + binding_size: 0, + byte_offset: var_layout.offset(), + }; + + Self { + type_layout: var_layout.type_layout().unwrap(), + binding_location, + } + } + + pub fn field(&self, name: &str) -> Self + { + let Some(field_var_layout) = self.type_layout.get_field_by_name(name) else { + panic!("Field '{name}' does not exist"); + }; + + let field_type_kind = field_var_layout.ty().unwrap().kind(); + + let field_var_layout = match field_type_kind { + TypeKind::ConstantBuffer => field_var_layout + .type_layout() + .expect("Constant buffer field has no type layout") + .element_var_layout() + .expect("Constant buffer field type layout has no element var layout"), + TypeKind::Array + | TypeKind::Matrix + | TypeKind::Scalar + | TypeKind::Vector + | TypeKind::Struct + | TypeKind::Resource => field_var_layout, + type_kind => unimplemented!("Type kind {type_kind:?} is not yet supported"), + }; + + Self { + type_layout: field_var_layout.type_layout().unwrap(), + binding_location: BindingLocation { + binding_index: self.binding_location.binding_index + + field_var_layout.binding_index(), + binding_size: if field_type_kind == TypeKind::ConstantBuffer { + field_var_layout + .type_layout() + .unwrap() + .uniform_size() + .unwrap() + } else { + self.binding_location.binding_size + }, + byte_offset: self.binding_location.byte_offset + + field_var_layout.offset(), + }, + } + } + + pub fn element(mut self, index: usize) -> Self + { + let element_type_layout = self.type_layout.element_type_layout().unwrap(); + + self.binding_location.byte_offset += index * element_type_layout.stride(); + + self.type_layout = element_type_layout; + + self + } + + pub fn with_field(self, name: &str, func: impl FnOnce(Self) -> Self) -> Self + { + let _ = func(self.field(name)); + + self + } + + pub fn binding_location(&self) -> &BindingLocation + { + &self.binding_location + } + + pub fn into_binding_location(self) -> BindingLocation + { + self.binding_location + } +} + +#[derive(Debug, Clone)] +pub struct BindingLocation +{ + pub binding_index: u32, + pub binding_size: usize, + pub byte_offset: usize, +} + +#[derive(Debug, Clone)] +pub enum BindingValue +{ + Uint(u32), + Int(i32), + Float(f32), + FVec3(Vec3<f32>), + FMat4x4(Matrix<f32, 4, 4>), + Texture(AssetHandle<Image>), +} + +impl From<u32> for BindingValue +{ + fn from(value: u32) -> Self + { + BindingValue::Uint(value) + } +} + +impl From<i32> for BindingValue +{ + fn from(value: i32) -> Self + { + BindingValue::Int(value) + } +} + +impl From<f32> for BindingValue +{ + fn from(value: f32) -> Self + { + BindingValue::Float(value) + } +} + +impl From<Vec3<f32>> for BindingValue +{ + fn from(vec: Vec3<f32>) -> Self + { + BindingValue::FVec3(vec) + } +} + +impl From<Matrix<f32, 4, 4>> for BindingValue +{ + fn from(matrix: Matrix<f32, 4, 4>) -> Self + { + BindingValue::FMat4x4(matrix) + } +} diff --git a/engine/src/shader/default.rs b/engine/src/shader/default.rs new file mode 100644 index 0000000..28bbdc9 --- /dev/null +++ b/engine/src/shader/default.rs @@ -0,0 +1,363 @@ +use std::path::Path; +use std::sync::LazyLock; + +use ecs::Query; +use ecs::actions::Actions; +use ecs::query::term::Without; +use ecs::sole::Single; + +use crate::asset::{Assets, Label as AssetLabel}; +use crate::camera::{Active as ActiveCamera, Camera}; +use crate::data_types::dimens::Dimens; +use crate::draw_flags::NoDraw; +use crate::lighting::{DirectionalLight, GlobalLight, PointLight}; +use crate::material::{Flags as MaterialFlags, Material}; +use crate::matrix::Matrix; +use crate::model::{MaterialSearchResult, Model}; +use crate::projection::{ClipVolume as ProjectionClipVolume, Projection}; +use crate::renderer::{DEFAULT_TEXTURE_ASSET_LABEL, PendingShaderBindings}; +use crate::shader::cursor::{BindingValue as ShaderBindingValue, Cursor as ShaderCursor}; +use crate::shader::{ + Context as ShaderContext, + ModuleSource as ShaderModuleSource, + Shader, +}; +use crate::transform::{Scale, Transform, WorldPosition}; +use crate::vector::Vec3; +use crate::windowing::window::Window; + +pub static ASSET_LABEL: LazyLock<AssetLabel> = LazyLock::new(|| AssetLabel { + path: Path::new("").into(), + name: Some("default_shader".into()), +}); + +pub fn enqueue_set_shader_bindings( + renderable_query: Query<RenderableEntity<'_>, (Without<NoDraw>,)>, + camera_query: Query<(&Camera, &WorldPosition, &ActiveCamera)>, + window_query: Query<(&Window,)>, + point_light_query: Query<(&PointLight, &WorldPosition)>, + directional_light_query: Query<(&DirectionalLight,)>, + assets: Single<Assets>, + shader_context: Single<ShaderContext>, + global_light: Single<GlobalLight>, + mut actions: Actions, +) +{ + let Some((camera, camera_world_pos, _)) = camera_query.iter().next() else { + tracing::warn!("No current camera"); + return; + }; + + let Some((window,)) = window_query.iter().next() else { + // tracing::warn!("No window"); + return; + }; + + let default_shader_asset = assets + .get_handle_to_loaded::<ShaderModuleSource>(ASSET_LABEL.clone()) + .unwrap(); + + for ( + entity_id, + (model, material_flags, world_pos, scale, shader, mut pending_shader_bindings), + ) in renderable_query.iter_with_euids() + { + let shader_asset = match &shader { + Some(shader) => &shader.asset_handle, + None => &default_shader_asset, + }; + + if shader_asset.id() != default_shader_asset.id() { + continue; + } + + let Some(shader_program) = shader_context.get_program(&shader_asset.id()) else { + continue; + }; + + let Some(model_spec) = assets.get(&model.spec_asset) else { + continue; + }; + + let has_pending_shader_bindings_comp = pending_shader_bindings.is_some(); + + let shader_bindings = match pending_shader_bindings.as_deref_mut() { + Some(pending_shader_bindings) => pending_shader_bindings, + None => &mut PendingShaderBindings::default(), + }; + + let shader_cursor = ShaderCursor::new( + shader_program + .reflection(0) + .unwrap() + .global_params_var_layout() + .unwrap(), + ); + + let model_matrix = create_model_matrix(Transform { + position: world_pos.as_deref().cloned().unwrap_or_default().position, + scale: scale.as_deref().cloned().unwrap_or_default().scale, + }); + + let inverted_model_matrix = model_matrix.inverse(); + + let model_material = match model_spec.find_first_material(&assets) { + MaterialSearchResult::Found(model_material_asset) => { + let Some(model_material) = assets.get(&model_material_asset) else { + continue; + }; + + model_material + } + MaterialSearchResult::NotFound => { + continue; + } + MaterialSearchResult::NoMaterials => &const { Material::builder().build() }, + }; + + if [ + &model_material.ambient_map, + &model_material.diffuse_map, + &model_material.specular_map, + ] + .into_iter() + .flatten() + .any(|texture| !assets.is_loaded_and_has_type(&texture.asset_handle)) + { + continue; + } + + let material_flags = material_flags + .as_deref() + .unwrap_or(&const { MaterialFlags::builder().build() }); + + let model_3d_shader_cursor = shader_cursor.field("Uniforms").field("model_3d"); + let lighting_shader_cursor = shader_cursor.field("Uniforms").field("lighting"); + let material_shader_cursor = lighting_shader_cursor.field("material"); + + shader_bindings.extend([ + (model_3d_shader_cursor.field("model"), model_matrix.into()), + ( + model_3d_shader_cursor.field("model_inverted"), + inverted_model_matrix.into(), + ), + ( + model_3d_shader_cursor.field("view"), + create_view_matrix(&camera, &camera_world_pos.position).into(), + ), + ( + model_3d_shader_cursor.field("projection"), + create_projection_matrix( + &camera, + &camera_world_pos.position, + window.inner_size(), + ) + .into(), + ), + ( + lighting_shader_cursor.field("view_pos"), + camera_world_pos.position.into(), + ), + ( + material_shader_cursor.field("ambient"), + Vec3::from( + if material_flags.use_ambient_color { + &model_material.ambient + } else { + &global_light.ambient + } + .clone(), + ) + .into(), + ), + ( + material_shader_cursor.field("diffuse"), + Vec3::from(model_material.diffuse.clone()).into(), + ), + ( + material_shader_cursor.field("specular"), + Vec3::from(model_material.specular.clone()).into(), + ), + ( + material_shader_cursor.field("shininess"), + model_material.shininess.into(), + ), + ( + lighting_shader_cursor.field("directional_light_cnt"), + u32::try_from(directional_light_query.iter().count()) + .expect( + "Directional light count does not fit in 32-bit unsigned integer", + ) + .into(), + ), + ( + lighting_shader_cursor.field("point_light_cnt"), + u32::try_from(point_light_query.iter().count()) + .expect("Point light count does not fit in 32-bit unsigned integer") + .into(), + ), + ]); + + shader_bindings.extend(point_light_query.iter().enumerate().flat_map( + |(point_light_index, (point_light, point_light_world_pos))| { + let point_light_shader_cursor = lighting_shader_cursor + .field("point_lights") + .element(point_light_index); + + let phong_shader_cursor = point_light_shader_cursor.field("phong"); + + let attenuation_props_shader_cursor = + point_light_shader_cursor.field("attenuation_props"); + + [ + ( + phong_shader_cursor.field("diffuse"), + Vec3::from(point_light.diffuse.clone()).into(), + ), + ( + phong_shader_cursor.field("specular"), + Vec3::from(point_light.specular.clone()).into(), + ), + ( + point_light_shader_cursor.field("position"), + (point_light_world_pos.position + point_light.local_position) + .into(), + ), + ( + attenuation_props_shader_cursor.field("constant"), + point_light.attenuation_params.constant.into(), + ), + ( + attenuation_props_shader_cursor.field("linear"), + point_light.attenuation_params.linear.into(), + ), + ( + attenuation_props_shader_cursor.field("quadratic"), + point_light.attenuation_params.quadratic.into(), + ), + ] + }, + )); + + shader_bindings.extend(directional_light_query.iter().enumerate().flat_map( + |(directional_light_index, (directional_light,))| { + let directional_light_shader_cursor = lighting_shader_cursor + .field("directional_lights") + .element(directional_light_index); + + let phong_shader_cursor = directional_light_shader_cursor.field("phong"); + + [ + ( + phong_shader_cursor.field("diffuse"), + Vec3::from(directional_light.diffuse.clone()).into(), + ), + ( + phong_shader_cursor.field("specular"), + Vec3::from(directional_light.specular.clone()).into(), + ), + ( + directional_light_shader_cursor.field("direction"), + directional_light.direction.into(), + ), + ] + }, + )); + + shader_bindings.bindings.push(( + shader_cursor.field("ambient_map").into_binding_location(), + ShaderBindingValue::Texture( + model_material + .ambient_map + .as_ref() + .map(|ambient_map| ambient_map.asset_handle.clone()) + .unwrap_or_else(|| { + assets + .get_handle_to_loaded(DEFAULT_TEXTURE_ASSET_LABEL.clone()) + .expect("Not possible") + }), + ), + )); + + shader_bindings.bindings.push(( + shader_cursor.field("diffuse_map").into_binding_location(), + ShaderBindingValue::Texture( + model_material + .diffuse_map + .as_ref() + .map(|diffuse_map| diffuse_map.asset_handle.clone()) + .unwrap_or_else(|| { + assets + .get_handle_to_loaded(DEFAULT_TEXTURE_ASSET_LABEL.clone()) + .expect("Not possible") + }), + ), + )); + + shader_bindings.bindings.push(( + shader_cursor.field("specular_map").into_binding_location(), + ShaderBindingValue::Texture( + model_material + .specular_map + .as_ref() + .map(|specular_map| specular_map.asset_handle.clone()) + .unwrap_or_else(|| { + assets + .get_handle_to_loaded(DEFAULT_TEXTURE_ASSET_LABEL.clone()) + .expect("Not possible") + }), + ), + )); + + if !has_pending_shader_bindings_comp { + actions.add_components(entity_id, (shader_bindings.clone(),)); + } + } +} + +fn create_model_matrix(transform: Transform) -> Matrix<f32, 4, 4> +{ + let mut matrix = Matrix::new_identity(); + + matrix.translate(&transform.position); + matrix.scale(&transform.scale); + + matrix +} + +fn create_view_matrix(camera: &Camera, camera_world_pos: &Vec3<f32>) +-> Matrix<f32, 4, 4> +{ + let mut view = Matrix::new(); + + // tracing::debug!("Camera target: {:?}", camera.target); + + view.look_at(camera_world_pos, &camera.target, &camera.global_up); + + view +} + +fn create_projection_matrix( + camera: &Camera, + camera_world_pos: &Vec3<f32>, + window_size: &Dimens<u32>, +) -> Matrix<f32, 4, 4> +{ + match &camera.projection { + Projection::Perspective(perspective_proj) => perspective_proj.to_matrix_rh( + window_size.width as f32 / window_size.height as f32, + ProjectionClipVolume::NegOneToOne, + ), + Projection::Orthographic(orthographic_proj) => orthographic_proj + .to_matrix_rh(camera_world_pos, ProjectionClipVolume::NegOneToOne), + } +} + +type RenderableEntity<'a> = ( + &'a Model, + Option<&'a MaterialFlags>, + Option<&'a WorldPosition>, + Option<&'a Scale>, + Option<&'a Shader>, + Option<&'a mut PendingShaderBindings>, +); diff --git a/engine/src/texture.rs b/engine/src/texture.rs index 4a4fe86..d02b9ff 100644 --- a/engine/src/texture.rs +++ b/engine/src/texture.rs @@ -1,194 +1,34 @@ -use std::fmt::Display; -use std::path::Path; -use std::sync::atomic::{AtomicU32, Ordering}; - -use image::io::Reader as ImageReader; -use image::{DynamicImage, ImageError, Rgb, RgbImage}; - -use crate::color::Color; -use crate::data_types::dimens::Dimens; -use crate::opengl::texture::PixelDataFormat; -use crate::util::builder; - -static NEXT_ID: AtomicU32 = AtomicU32::new(0); - -mod reexports -{ - pub use crate::opengl::texture::{Filtering, Wrapping}; -} - -pub use reexports::*; +use crate::asset::Handle as AssetHandle; +use crate::image::Image; +use crate::builder; #[derive(Debug, Clone)] +#[non_exhaustive] pub struct Texture { - id: Id, - image: DynamicImage, - pixel_data_format: PixelDataFormat, - dimensions: Dimens<u32>, - properties: Properties, + pub asset_handle: AssetHandle<Image>, + pub properties: Properties, } impl Texture { - pub fn builder() -> Builder - { - Builder::default() - } - - /// Opens a texture image. - /// - /// # Errors - /// Will return `Err` if: - /// - Opening the image fails - /// - The image data is not 8-bit/color RGB - pub fn open(path: &Path) -> Result<Self, Error> - { - Self::builder().open(path) - } - - #[must_use] - pub fn new_from_color(dimensions: &Dimens<u32>, color: &Color<u8>) -> Self - { - Self::builder().build_with_single_color(dimensions, color) - } - - #[must_use] - pub fn id(&self) -> Id - { - self.id - } - - #[must_use] - pub fn properties(&self) -> &Properties - { - &self.properties - } - - pub fn properties_mut(&mut self) -> &mut Properties - { - &mut self.properties - } - - #[must_use] - pub fn dimensions(&self) -> &Dimens<u32> - { - &self.dimensions - } - - #[must_use] - pub fn pixel_data_format(&self) -> PixelDataFormat - { - self.pixel_data_format - } - - #[must_use] - pub fn image(&self) -> &DynamicImage - { - &self.image - } -} - -impl Drop for Texture -{ - fn drop(&mut self) - { - NEXT_ID.fetch_sub(1, Ordering::Relaxed); - } -} - -/// Texture builder. -#[derive(Debug, Default, Clone)] -pub struct Builder -{ - properties: Properties, -} - -impl Builder -{ - pub fn properties(mut self, properties: Properties) -> Self + pub fn new(asset_handle: AssetHandle<Image>) -> Self { - self.properties = properties; - self - } - - /// Opens a image as a texture. - /// - /// # Errors - /// Will return `Err` if: - /// - Opening the image fails - /// - Decoding the image fails - /// - The image data is in a unsupported format - pub fn open(&self, path: &(impl AsRef<Path> + ?Sized)) -> Result<Texture, Error> - { - let image = ImageReader::open(path) - .map_err(Error::OpenImageFailed)? - .decode() - .map_err(Error::DecodeImageFailed)?; - - let pixel_data_format = match &image { - DynamicImage::ImageRgb8(_) => PixelDataFormat::Rgb8, - DynamicImage::ImageRgba8(_) => PixelDataFormat::Rgba8, - _ => { - return Err(Error::UnsupportedImageDataFormat); - } - }; - - let dimensions = Dimens { - width: image.width(), - height: image.height(), - }; - - let id = NEXT_ID.fetch_add(1, Ordering::Relaxed); - - Ok(Texture { - id: Id::new(id), - image, - pixel_data_format, - dimensions, - properties: self.properties.clone(), - }) + Self { + asset_handle, + properties: Properties::default(), + } } - #[must_use] - pub fn build_with_single_color( - &self, - dimensions: &Dimens<u32>, - color: &Color<u8>, - ) -> Texture + pub fn with_properties( + asset_handle: AssetHandle<Image>, + properties: Properties, + ) -> Self { - let image = RgbImage::from_pixel( - dimensions.width, - dimensions.height, - Rgb([color.red, color.green, color.blue]), - ); - - let id = NEXT_ID.fetch_add(1, Ordering::Relaxed); - - Texture { - id: Id::new(id), - image: image.into(), - pixel_data_format: PixelDataFormat::Rgb8, - dimensions: *dimensions, - properties: self.properties.clone(), - } + Self { asset_handle, properties } } } -/// Texture error. -#[derive(Debug, thiserror::Error)] -pub enum Error -{ - #[error("Failed to open texture image")] - OpenImageFailed(#[source] std::io::Error), - - #[error("Failed to decode texture image")] - DecodeImageFailed(#[source] ImageError), - - #[error("Unsupported image data format")] - UnsupportedImageDataFormat, -} - builder! { /// Texture properties #[builder(name = PropertiesBuilder, derives=(Debug, Clone))] @@ -230,25 +70,21 @@ impl Default for PropertiesBuilder } } -/// Texture ID. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Id -{ - id: u32, -} - -impl Id +#[non_exhaustive] +pub enum Filtering { - fn new(id: u32) -> Self - { - Self { id } - } + Nearest, + Linear, } -impl Display for Id +/// Texture wrapping. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum Wrapping { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result - { - self.id.fmt(formatter) - } + Repeat, + MirroredRepeat, + ClampToEdge, + ClampToBorder, } diff --git a/engine/src/transform.rs b/engine/src/transform.rs index 7c0c941..05819bc 100644 --- a/engine/src/transform.rs +++ b/engine/src/transform.rs @@ -1,7 +1,46 @@ use ecs::Component; +use crate::builder; use crate::vector::Vec3; +builder!( + #[builder(name = Builder, derives=(Debug))] + #[derive(Debug)] + #[non_exhaustive] + pub struct Transform + { + pub position: Vec3<f32>, + pub scale: Vec3<f32>, + } +); + +impl Transform +{ + pub fn builder() -> Builder + { + Builder::default() + } +} + +impl Default for Transform +{ + fn default() -> Self + { + Self::builder().build() + } +} + +impl Default for Builder +{ + fn default() -> Self + { + Self { + position: Vec3::from(0.0), + scale: Vec3::from(1.0), + } + } +} + /// A position in world space. #[derive(Debug, Default, Clone, Copy, Component)] pub struct WorldPosition diff --git a/engine/src/util.rs b/engine/src/util.rs index 0f6c78c..0c81c71 100644 --- a/engine/src/util.rs +++ b/engine/src/util.rs @@ -1,4 +1,72 @@ -use std::marker::PhantomData; +use ecs::util::VecExt; + +#[derive(Debug)] +pub struct MapVec<Key: Ord, Value> +{ + inner: Vec<(Key, Value)>, +} + +impl<Key: Ord, Value> MapVec<Key, Value> +{ + pub fn insert(&mut self, key: Key, value: Value) + { + self.inner + .insert_at_part_pt_by_key((key, value), |(a_key, _)| a_key); + } + + pub fn remove(&mut self, key: Key) -> Option<Value> + { + let index = self + .inner + .binary_search_by_key(&&key, |(a_key, _)| a_key) + .ok()?; + + let (_, value) = self.inner.remove(index); + + Some(value) + } + + pub fn get(&self, key: &Key) -> Option<&Value> + { + let index = self + .inner + .binary_search_by_key(&key, |(a_key, _)| a_key) + .ok()?; + + let Some((_, value)) = self.inner.get(index) else { + unreachable!(); // Reason: Index from binary search cannot be OOB + }; + + Some(value) + } + + pub fn get_mut(&mut self, key: &Key) -> Option<&mut Value> + { + let index = self + .inner + .binary_search_by_key(&key, |(a_key, _)| a_key) + .ok()?; + + let Some((_, value)) = self.inner.get_mut(index) else { + unreachable!(); // Reason: Index from binary search cannot be OOB + }; + + Some(value) + } + + pub fn values(&self) -> impl Iterator<Item = &Value> + { + self.inner.iter().map(|(_, value)| value) + } +} + +impl<Key: Ord, Value> Default for MapVec<Key, Value> +{ + fn default() -> Self + { + Self { inner: Vec::new() } + } +} macro_rules! try_option { ($expr: expr) => { @@ -25,6 +93,18 @@ macro_rules! or { pub(crate) use or; +#[macro_export] +macro_rules! expand_map_opt { + ($in: tt, no_occurance=($($no_occurance: tt)*), occurance=($($occurance: tt)*)) => { + $($occurance)* + }; + + (, no_occurance=($($no_occurance: tt)*), occurance=($($occurance: tt)*)) => { + $($no_occurance)* + }; +} + +#[macro_export] macro_rules! builder { ( $(#[doc = $doc: literal])* @@ -36,7 +116,8 @@ macro_rules! builder { $visibility: vis struct $name: ident { $( - $(#[$field_attr: meta])* + $(#[doc = $field_doc: literal])* + $(#[builder(skip_generate_fn$($field_skip_generate_fn: tt)?)])? $field_visibility: vis $field: ident: $field_type: ty, )* } @@ -46,7 +127,7 @@ macro_rules! builder { $visibility struct $name { $( - $(#[$field_attr])* + $(#[doc = $field_doc])* $field_visibility $field: $field_type, )* } @@ -62,16 +143,23 @@ macro_rules! builder { impl $builder_name { $( - #[must_use] - $visibility fn $field(mut self, $field: $field_type) -> Self - { - self.$field = $field; - self - } + $crate::expand_map_opt!( + $(true $($field_skip_generate_fn)?)?, + no_occurance=( + #[must_use] + $visibility fn $field(mut self, $field: $field_type) -> Self + { + self.$field = $field; + self + } + ), + occurance=() + ); )* #[must_use] - $visibility fn build(self) -> $name { + $visibility const fn build(self) -> $name + { $name { $( $field: self.$field, @@ -82,6 +170,7 @@ macro_rules! builder { impl From<$name> for $builder_name { + #[allow(unused_variables)] fn from(built: $name) -> Self { Self { @@ -93,77 +182,3 @@ macro_rules! builder { } }; } - -pub(crate) use builder; - -pub enum RefOrValue<'a, T> -{ - Ref(&'a T), - Value(Option<T>), -} - -impl<'a, T> RefOrValue<'a, T> -{ - pub fn get(&self) -> Option<&T> - { - match self { - Self::Ref(val_ref) => Some(val_ref), - Self::Value(val_cell) => val_cell.as_ref(), - } - } -} - -#[derive(Debug)] -pub struct Defer<'func, Func, Data> -where - Func: FnMut(&mut Data) + 'func, -{ - func: Func, - pub data: Data, - _pd: PhantomData<&'func ()>, -} - -impl<'func, Func, Data> Defer<'func, Func, Data> -where - Func: FnMut(&mut Data) + 'func, -{ - pub fn new(data: Data, func: Func) -> Self - { - Self { func, data, _pd: PhantomData } - } -} - -impl<'func, Func, Data> Drop for Defer<'func, Func, Data> -where - Func: FnMut(&mut Data) + 'func, -{ - fn drop(&mut self) - { - (self.func)(&mut self.data) - } -} - -/// Defines a function that will be called at the end of the current scope. -/// -/// Only captured variables that are later mutably borrowed needs to specified as -/// captures. -macro_rules! defer { - (|$capture: ident| {$($tt: tt)*}) => { - // This uses the automatic temporary lifetime extension behaviour introduced - // in Rust 1.79.0 (https://blog.rust-lang.org/2024/06/13/Rust-1.79.0.html) to - // create a unnamable variable for the Defer struct. The variable should be - // unnamable so that it cannot be missused and so that this macro can be used - // multiple times without having to give it a identifier for the Defer struct - // variable - let Defer { data: $capture, .. } = if true { - &Defer::new($capture, |$capture| { - $($tt)* - }) - } - else { - unreachable!(); - }; - }; -} - -pub(crate) use defer; diff --git a/engine/src/window.rs b/engine/src/window.rs deleted file mode 100644 index d342341..0000000 --- a/engine/src/window.rs +++ /dev/null @@ -1,753 +0,0 @@ -use std::borrow::Cow; -use std::ffi::{CStr, CString}; - -use bitflags::bitflags; -use ecs::actions::Actions; -use ecs::extension::Collector as ExtensionCollector; -use ecs::pair::{ChildOf, Pair}; -use ecs::phase::{Phase, START as START_PHASE}; -use ecs::sole::Single; -use ecs::{static_entity, Sole}; -use glfw::window::{Hint as WindowCreationHint, HintValue as WindowCreationHintValue}; -use glfw::WindowSize; -use util_macros::VariantArr; - -use crate::data_types::dimens::Dimens; -use crate::renderer::RENDER_PHASE; -use crate::vector::Vec2; - -static_entity!( - pub UPDATE_PHASE, - (Phase, Pair::new::<ChildOf>(*RENDER_PHASE)) -); - -#[derive(Debug, Sole)] -/// Has to be dropped last since it holds the OpenGL context. -#[sole(drop_last)] -pub struct Window -{ - inner: glfw::Window, -} - -impl Window -{ - /// Returns a new Window builder. - #[must_use] - pub fn builder() -> Builder - { - Builder::default() - } - - /// Sets the value of a input mode. - /// - /// # Errors - /// Returns `Err` if the input mode is unsupported on the current system. - pub fn set_input_mode( - &self, - input_mode: InputMode, - enabled: bool, - ) -> Result<(), Error> - { - Ok(self - .inner - .set_input_mode(input_mode.to_glfw_input_mode(), enabled)?) - } - - /// Sets the cursor mode. - /// - /// # Errors - /// If a platform error occurs. - pub fn set_cursor_mode(&self, cursor_mode: CursorMode) -> Result<(), Error> - { - Ok(self - .inner - .set_cursor_mode(cursor_mode.to_glfw_cursor_mode())?) - } - - /// Returns whether or not the window should close. Will return true when the user has - /// attempted to close the window. - #[must_use] - pub fn should_close(&self) -> bool - { - self.inner.should_close() - } - - /// Processes all pending events. - /// - /// # Errors - /// If a platform error occurs. - pub fn poll_events(&self) -> Result<(), Error> - { - Ok(self.inner.poll_events()?) - } - - /// Swaps the front and back buffers of the window. - /// - /// # Errors - /// Will return `Err` if a platform error occurs or if no OpenGL window context - /// is present. - pub fn swap_buffers(&self) -> Result<(), Error> - { - Ok(self.inner.swap_buffers()?) - } - - /// Returns the size of the window. - /// - /// # Errors - /// Will return `Err` if a platform error occurs. - pub fn size(&self) -> Result<Dimens<u32>, Error> - { - let size = self.inner.size()?; - - Ok(Dimens { - width: size.width, - height: size.height, - }) - } - - /// Returns the address of the specified OpenGL function, if it is supported by the - /// current OpenGL context. - /// - /// # Errors - /// Will return `Err` if a platform error occurs or if no current context has - /// been set. - /// - /// # Panics - /// Will panic if the `proc_name` argument contains a nul byte. - pub fn get_proc_address( - &self, - proc_name: &str, - ) -> Result<unsafe extern "C" fn(), Error> - { - let proc_name_c: Cow<CStr> = CStr::from_bytes_with_nul(proc_name.as_bytes()) - .map(Cow::Borrowed) - .or_else(|_| CString::new(proc_name).map(Cow::Owned)) - .expect("OpenGL function name contains a nul byte"); - - Ok(self.inner.get_proc_address(&proc_name_c)?) - } - - /// Makes the OpenGL context of the window current for the calling thread. - /// - /// # Errors - /// Will return `Err` if a platform error occurs or if no OpenGL context is - /// present. - pub fn make_context_current(&self) -> Result<(), Error> - { - Ok(self.inner.make_context_current()?) - } - - /// Sets the window's framebuffer size callback. - pub fn set_framebuffer_size_callback(&self, callback: impl Fn(Dimens<u32>) + 'static) - { - self.inner.set_framebuffer_size_callback(move |size| { - callback(Dimens { - width: size.width, - height: size.height, - }); - }); - } - - /// Sets the window's key size callback. - pub fn set_key_callback( - &self, - callback: impl Fn(Key, i32, KeyState, KeyModifiers) + 'static, - ) - { - self.inner - .set_key_callback(move |key, scancode, key_state, key_modifiers| { - let Some(key_state) = KeyState::from_glfw_key_state(key_state) else { - return; - }; - - callback( - Key::from_glfw_key(key), - scancode, - key_state, - KeyModifiers::from_bits_truncate(key_modifiers.bits()), - ) - }); - } - - /// Sets the window's cursor position callback. - pub fn set_cursor_pos_callback(&self, callback: impl Fn(Vec2<f64>) + 'static) - { - self.inner - .set_cursor_pos_callback(move |pos| callback(Vec2 { x: pos.x, y: pos.y })); - } - - /// Sets the window's mouse button callback. The given function is called when a mouse - /// button enters a new state. - pub fn set_mouse_button_callback( - &self, - callback: impl Fn(MouseButton, MouseButtonState, KeyModifiers) + 'static, - ) - { - self.inner.set_mouse_button_callback( - move |mouse_button, mouse_button_state, key_modifiers| { - callback( - MouseButton::from_glfw_mouse_button(mouse_button), - MouseButtonState::from_glfw_mouse_button_state(mouse_button_state), - KeyModifiers::from_bits_truncate(key_modifiers.bits()), - ) - }, - ); - } - - /// Sets the window's close callback. - pub fn set_close_callback(&self, callback: impl Fn() + 'static) - { - self.inner.set_close_callback(callback); - } - - /// Sets the window's focus callback. The callback is called when the window loses or - /// gains input focus. - pub fn set_focus_callback(&self, callback: impl Fn(bool) + 'static) - { - self.inner.set_focus_callback(callback); - } -} - -/// [`Window`] builder. -#[derive(Debug, Clone, Default)] -pub struct Builder -{ - inner: glfw::WindowBuilder, -} - -impl Builder -{ - /// Sets whether the OpenGL context should be created in debug mode, which may - /// provide additional error and diagnostic reporting functionality. - pub fn opengl_debug_context(mut self, enabled: bool) -> Self - { - self.inner = self.inner.hint( - WindowCreationHint::OpenGLDebugContext, - WindowCreationHintValue::Bool(enabled), - ); - - self - } - - /// Set the desired number of samples to use for multisampling. Zero disables - /// multisampling. - pub fn multisampling_sample_count(mut self, sample_count: u16) -> Self - { - self.inner = self.inner.hint( - WindowCreationHint::Samples, - WindowCreationHintValue::Number(sample_count as i32), - ); - - self - } - - /// Creates a new window. - /// - /// # Errors - /// Will return `Err` if the title contains a internal nul byte or if a platform error - /// occurs. - pub fn create(&self, size: Dimens<u32>, title: &str) -> Result<Window, Error> - { - let builder = self.inner.clone().hint( - WindowCreationHint::OpenGLDebugContext, - WindowCreationHintValue::Bool(true), - ); - - let window = builder.create( - &WindowSize { - width: size.width, - height: size.height, - }, - title, - )?; - - Ok(Window { inner: window }) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, VariantArr)] -#[variant_arr(name = KEYS)] -pub enum Key -{ - Space, - Apostrophe, - Comma, - Minus, - Period, - Slash, - Digit0, - Digit1, - Digit2, - Digit3, - Digit4, - Digit5, - Digit6, - Digit7, - Digit8, - Digit9, - Semicolon, - Equal, - A, - B, - C, - D, - E, - F, - G, - H, - I, - J, - K, - L, - M, - N, - O, - P, - Q, - R, - S, - T, - U, - V, - W, - X, - Y, - Z, - LeftBracket, - Backslash, - RightBracket, - GraveAccent, - World1, - World2, - Escape, - Enter, - Tab, - Backspace, - Insert, - Delete, - Right, - Left, - Down, - Up, - PageUp, - PageDown, - Home, - End, - CapsLock, - ScrollLock, - NumLock, - PrintScreen, - Pause, - F1, - F2, - F3, - F4, - F5, - F6, - F7, - F8, - F9, - F10, - F11, - F12, - F13, - F14, - F15, - F16, - F17, - F18, - F19, - F20, - F21, - F22, - F23, - F24, - F25, - Kp0, - Kp1, - Kp2, - Kp3, - Kp4, - Kp5, - Kp6, - Kp7, - Kp8, - Kp9, - KpDecimal, - KpDivide, - KpMultiply, - KpSubtract, - KpAdd, - KpEnter, - KpEqual, - LeftShift, - LeftControl, - LeftAlt, - LeftSuper, - RightShift, - RightControl, - RightAlt, - RightSuper, - Menu, -} - -impl Key -{ - fn from_glfw_key(glfw_key: glfw::window::Key) -> Self - { - match glfw_key { - glfw::window::Key::Space => Self::Space, - glfw::window::Key::Apostrophe => Self::Apostrophe, - glfw::window::Key::Comma => Self::Comma, - glfw::window::Key::Minus => Self::Minus, - glfw::window::Key::Period => Self::Period, - glfw::window::Key::Slash => Self::Slash, - glfw::window::Key::Digit0 => Self::Digit0, - glfw::window::Key::Digit1 => Self::Digit1, - glfw::window::Key::Digit2 => Self::Digit2, - glfw::window::Key::Digit3 => Self::Digit3, - glfw::window::Key::Digit4 => Self::Digit4, - glfw::window::Key::Digit5 => Self::Digit5, - glfw::window::Key::Digit6 => Self::Digit6, - glfw::window::Key::Digit7 => Self::Digit7, - glfw::window::Key::Digit8 => Self::Digit8, - glfw::window::Key::Digit9 => Self::Digit9, - glfw::window::Key::Semicolon => Self::Semicolon, - glfw::window::Key::Equal => Self::Equal, - glfw::window::Key::A => Self::A, - glfw::window::Key::B => Self::B, - glfw::window::Key::C => Self::C, - glfw::window::Key::D => Self::D, - glfw::window::Key::E => Self::E, - glfw::window::Key::F => Self::F, - glfw::window::Key::G => Self::G, - glfw::window::Key::H => Self::H, - glfw::window::Key::I => Self::I, - glfw::window::Key::J => Self::J, - glfw::window::Key::K => Self::K, - glfw::window::Key::L => Self::L, - glfw::window::Key::M => Self::M, - glfw::window::Key::N => Self::N, - glfw::window::Key::O => Self::O, - glfw::window::Key::P => Self::P, - glfw::window::Key::Q => Self::Q, - glfw::window::Key::R => Self::R, - glfw::window::Key::S => Self::S, - glfw::window::Key::T => Self::T, - glfw::window::Key::U => Self::U, - glfw::window::Key::V => Self::V, - glfw::window::Key::W => Self::W, - glfw::window::Key::X => Self::X, - glfw::window::Key::Y => Self::Y, - glfw::window::Key::Z => Self::Z, - glfw::window::Key::LeftBracket => Self::LeftBracket, - glfw::window::Key::Backslash => Self::Backslash, - glfw::window::Key::RightBracket => Self::RightBracket, - glfw::window::Key::GraveAccent => Self::GraveAccent, - glfw::window::Key::World1 => Self::World1, - glfw::window::Key::World2 => Self::World2, - glfw::window::Key::Escape => Self::Escape, - glfw::window::Key::Enter => Self::Enter, - glfw::window::Key::Tab => Self::Tab, - glfw::window::Key::Backspace => Self::Backspace, - glfw::window::Key::Insert => Self::Insert, - glfw::window::Key::Delete => Self::Delete, - glfw::window::Key::Right => Self::Right, - glfw::window::Key::Left => Self::Left, - glfw::window::Key::Down => Self::Down, - glfw::window::Key::Up => Self::Up, - glfw::window::Key::PageUp => Self::PageUp, - glfw::window::Key::PageDown => Self::PageDown, - glfw::window::Key::Home => Self::Home, - glfw::window::Key::End => Self::End, - glfw::window::Key::CapsLock => Self::CapsLock, - glfw::window::Key::ScrollLock => Self::ScrollLock, - glfw::window::Key::NumLock => Self::NumLock, - glfw::window::Key::PrintScreen => Self::PrintScreen, - glfw::window::Key::Pause => Self::Pause, - glfw::window::Key::F1 => Self::F1, - glfw::window::Key::F2 => Self::F2, - glfw::window::Key::F3 => Self::F3, - glfw::window::Key::F4 => Self::F4, - glfw::window::Key::F5 => Self::F5, - glfw::window::Key::F6 => Self::F6, - glfw::window::Key::F7 => Self::F7, - glfw::window::Key::F8 => Self::F8, - glfw::window::Key::F9 => Self::F9, - glfw::window::Key::F10 => Self::F10, - glfw::window::Key::F11 => Self::F11, - glfw::window::Key::F12 => Self::F12, - glfw::window::Key::F13 => Self::F13, - glfw::window::Key::F14 => Self::F14, - glfw::window::Key::F15 => Self::F15, - glfw::window::Key::F16 => Self::F16, - glfw::window::Key::F17 => Self::F17, - glfw::window::Key::F18 => Self::F18, - glfw::window::Key::F19 => Self::F19, - glfw::window::Key::F20 => Self::F20, - glfw::window::Key::F21 => Self::F21, - glfw::window::Key::F22 => Self::F22, - glfw::window::Key::F23 => Self::F23, - glfw::window::Key::F24 => Self::F24, - glfw::window::Key::F25 => Self::F25, - glfw::window::Key::Kp0 => Self::Kp0, - glfw::window::Key::Kp1 => Self::Kp1, - glfw::window::Key::Kp2 => Self::Kp2, - glfw::window::Key::Kp3 => Self::Kp3, - glfw::window::Key::Kp4 => Self::Kp4, - glfw::window::Key::Kp5 => Self::Kp5, - glfw::window::Key::Kp6 => Self::Kp6, - glfw::window::Key::Kp7 => Self::Kp7, - glfw::window::Key::Kp8 => Self::Kp8, - glfw::window::Key::Kp9 => Self::Kp9, - glfw::window::Key::KpDecimal => Self::KpDecimal, - glfw::window::Key::KpDivide => Self::KpDivide, - glfw::window::Key::KpMultiply => Self::KpMultiply, - glfw::window::Key::KpSubtract => Self::KpSubtract, - glfw::window::Key::KpAdd => Self::KpAdd, - glfw::window::Key::KpEnter => Self::KpEnter, - glfw::window::Key::KpEqual => Self::KpEqual, - glfw::window::Key::LeftShift => Self::LeftShift, - glfw::window::Key::LeftControl => Self::LeftControl, - glfw::window::Key::LeftAlt => Self::LeftAlt, - glfw::window::Key::LeftSuper => Self::LeftSuper, - glfw::window::Key::RightShift => Self::RightShift, - glfw::window::Key::RightControl => Self::RightControl, - glfw::window::Key::RightAlt => Self::RightAlt, - glfw::window::Key::RightSuper => Self::RightSuper, - glfw::window::Key::Menu => Self::Menu, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum KeyState -{ - Pressed, - Released, -} - -impl KeyState -{ - fn from_glfw_key_state(glfw_key_state: glfw::window::KeyState) -> Option<Self> - { - match glfw_key_state { - glfw::window::KeyState::Pressed => Some(Self::Pressed), - glfw::window::KeyState::Released => Some(Self::Released), - glfw::window::KeyState::Repeat => None, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum MouseButton -{ - One, - Two, - Three, - Four, - Five, - Six, - Seven, - Eight, -} - -impl MouseButton -{ - pub const LEFT: Self = Self::One; - pub const MIDDLE: Self = Self::Three; - pub const RIGHT: Self = Self::Two; - - fn from_glfw_mouse_button(mouse_button: glfw::window::MouseButton) -> Self - { - match mouse_button { - glfw::window::MouseButton::One => Self::One, - glfw::window::MouseButton::Two => Self::Two, - glfw::window::MouseButton::Three => Self::Three, - glfw::window::MouseButton::Four => Self::Four, - glfw::window::MouseButton::Five => Self::Five, - glfw::window::MouseButton::Six => Self::Six, - glfw::window::MouseButton::Seven => Self::Seven, - glfw::window::MouseButton::Eight => Self::Eight, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum MouseButtonState -{ - Pressed, - Released, -} - -impl MouseButtonState -{ - fn from_glfw_mouse_button_state( - mouse_button_state: glfw::window::MouseButtonState, - ) -> Self - { - match mouse_button_state { - glfw::window::MouseButtonState::Pressed => Self::Pressed, - glfw::window::MouseButtonState::Released => Self::Released, - } - } -} - -bitflags! { - #[derive(Debug, Clone, Copy)] - pub struct KeyModifiers: i32 { - const SHIFT = glfw::window::KeyModifiers::SHIFT.bits(); - const CONTROL = glfw::window::KeyModifiers::CONTROL.bits(); - const ALT = glfw::window::KeyModifiers::ALT.bits(); - const SUPER = glfw::window::KeyModifiers::SUPER.bits(); - const CAPS_LOCK = glfw::window::KeyModifiers::CAPS_LOCK.bits(); - const NUM_LOCK = glfw::window::KeyModifiers::NUM_LOCK.bits(); - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum CursorMode -{ - /// Hides and grabs the cursor, providing virtual and unlimited cursor movement. - Disabled, - - /// Makes the cursor invisible when it is over the content area of the window but - /// does not restrict the cursor from leaving. - Hidden, - - /// Makes the cursor visible and behaving normally. - Normal, -} - -impl CursorMode -{ - fn to_glfw_cursor_mode(self) -> glfw::window::CursorMode - { - match self { - Self::Disabled => glfw::window::CursorMode::Disabled, - Self::Hidden => glfw::window::CursorMode::Hidden, - Self::Normal => glfw::window::CursorMode::Normal, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum InputMode -{ - /// When the cursor is disabled, raw (unscaled and unaccelerated) mouse motion can be - /// enabled if available. - /// - /// Raw mouse motion is closer to the actual motion of the mouse across a surface. It - /// is not affected by the scaling and acceleration applied to the motion of the - /// desktop cursor. That processing is suitable for a cursor while raw motion is - /// better for controlling for example a 3D camera. Because of this, raw mouse motion - /// is only provided when the cursor is disabled. - RawMouseMotion, -} - -impl InputMode -{ - fn to_glfw_input_mode(self) -> glfw::window::InputMode - { - match self { - Self::RawMouseMotion => glfw::window::InputMode::RawMouseMotion, - } - } -} - -#[derive(Debug)] -pub struct Extension -{ - window_builder: Builder, - window_size: Dimens<u32>, - window_title: String, -} - -impl Extension -{ - #[must_use] - pub fn new(window_builder: Builder) -> Self - { - Self { window_builder, ..Default::default() } - } - - #[must_use] - pub fn window_size(mut self, window_size: Dimens<u32>) -> Self - { - self.window_size = window_size; - - self - } - - #[must_use] - pub fn window_title(mut self, window_title: impl Into<String>) -> Self - { - self.window_title = window_title.into(); - - self - } -} - -impl ecs::extension::Extension for Extension -{ - fn collect(self, mut collector: ExtensionCollector<'_>) - { - collector.add_system(*START_PHASE, initialize); - collector.add_system(*UPDATE_PHASE, update); - - let window = self - .window_builder - .create(self.window_size, &self.window_title) - .unwrap(); - - window.set_cursor_mode(CursorMode::Normal).unwrap(); - - collector.add_sole(window).ok(); - } -} - -impl Default for Extension -{ - fn default() -> Self - { - Self { - window_builder: Builder::default(), - window_size: Dimens { width: 1920, height: 1080 }, - window_title: String::new(), - } - } -} - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct Error(glfw::Error); - -impl From<glfw::Error> for Error -{ - fn from(err: glfw::Error) -> Self - { - Self(err) - } -} - -fn initialize(window: Single<Window>, actions: Actions) -{ - let actions_weak_ref = actions.to_weak_ref(); - - window.set_close_callback(move || { - let actions_weak_ref = actions_weak_ref.clone(); - - let actions_ref = actions_weak_ref.access().expect("No world"); - - actions_ref.to_actions().stop(); - }); -} - -fn update(window: Single<Window>) -{ - window - .swap_buffers() - .expect("Failed to swap window buffers"); - - window.poll_events().expect("Failed to poll window events"); -} diff --git a/engine/src/windowing.rs b/engine/src/windowing.rs new file mode 100644 index 0000000..2bfdb31 --- /dev/null +++ b/engine/src/windowing.rs @@ -0,0 +1,699 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Weak}; +use std::thread::{spawn, JoinHandle as ThreadJoinHandle}; + +use crossbeam_channel::{ + bounded as bounded_channel, + Receiver as ChannelReceiver, + Sender as ChannelSender, + TrySendError, +}; +use ecs::actions::Actions; +use ecs::component::Component; +use ecs::entity::obtainer::Obtainer as EntityObtainer; +use ecs::event::component::{Added, Changed, Removed}; +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{Phase, UPDATE as UPDATE_PHASE}; +use ecs::sole::Single; +use ecs::system::observer::Observe; +use ecs::uid::Uid; +use ecs::{declare_entity, Query, Sole}; +use raw_window_handle::{DisplayHandle, HandleError, HasDisplayHandle, WindowHandle}; +use winit::application::ApplicationHandler; +use winit::dpi::PhysicalPosition; +use winit::error::EventLoopError; +use winit::event::{DeviceEvent, DeviceId, StartCause, WindowEvent}; +use winit::event_loop::{ + ActiveEventLoop, + ControlFlow as EventLoopControlFlow, + EventLoop, + OwnedDisplayHandle, +}; +use winit::keyboard::PhysicalKey; +use winit::window::{Window as WinitWindow, WindowId as WinitWindowId}; + +use crate::data_types::dimens::Dimens; +use crate::util::MapVec; +use crate::vector::Vec2; +use crate::windowing::keyboard::{Key, KeyState, Keyboard, UnknownKeyCodeError}; +use crate::windowing::mouse::{ + Button as MouseButton, + ButtonState as MouseButtonState, + Buttons as MouseButtons, + Motion as MouseMotion, +}; +use crate::windowing::window::{ + Closed as WindowClosed, + CreationAttributes as WindowCreationAttributes, + CreationReady as WindowCreationReady, + CursorGrabMode, + Id as WindowId, + Window, +}; + +pub mod keyboard; +pub mod mouse; +pub mod window; + +const MESSAGE_FROM_APP_CHANNEL_CAP: usize = 128; + +const MESSAGE_TO_APP_CHANNEL_CAP: usize = 16; // Increase if more messages are added + +declare_entity!( + pub PHASE, + ( + Phase, + Pair::builder() + .relation::<ChildOf>() + .target_id(*UPDATE_PHASE) + .build() + ) +); + +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct Extension {} + +impl ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: ecs::extension::Collector<'_>) + { + collector.add_sole(Context::default()).ok(); + collector.add_sole(Keyboard::default()).ok(); + collector.add_sole(MouseMotion::default()).ok(); + collector.add_sole(MouseButtons::default()).ok(); + + collector.add_declared_entity(&PHASE); + + collector.add_system(*PHASE, update_stuff); + + collector.add_observer(handle_window_changed); + collector.add_observer(handle_window_removed); + collector.add_observer(handle_window_creation_ready); + } +} + +fn handle_window_creation_ready( + observe: Observe<Pair<Added, WindowCreationReady>>, + context: Single<Context>, +) +{ + for evt_match in &observe { + let Some(ent) = evt_match.get_entity() else { + unreachable!(); + }; + + if ent.has_component(Window::id()) || ent.has_component(WindowClosed::id()) { + continue; + } + + let Some(window_creation_attrs) = ent.get::<WindowCreationAttributes>() else { + unreachable!(); + }; + + context.try_send_message_to_app(MessageToApp::CreateWindow( + ent.uid(), + window_creation_attrs.clone(), + )); + } +} + +#[tracing::instrument(skip_all)] +fn update_stuff( + mut context: Single<Context>, + mut keyboard: Single<Keyboard>, + mut mouse_motion: Single<MouseMotion>, + mut mouse_buttons: Single<MouseButtons>, + mut actions: Actions, + entity_obtainer: EntityObtainer, +) +{ + keyboard.make_key_states_previous(); + mouse_buttons.make_states_previous(); + mouse_motion.position_delta = Vec2::default(); + + let Context { + ref message_from_app_receiver, + ref mut display_handle, + ref mut windows, + .. + } = *context; + + for message in message_from_app_receiver.try_iter() { + match message { + MessageFromApp::Init(new_display_handle) => { + *display_handle = Some(new_display_handle); + } + MessageFromApp::WindowCreated( + window_ent_id, + winit_window, + window_creation_attrs, + ) => { + actions.add_components( + window_ent_id, + (Window::new(&winit_window, &window_creation_attrs),), + ); + + actions.remove_comps::<(WindowCreationReady,)>(window_ent_id); + + tracing::debug!("Added window component to window entity"); + + windows.insert( + WindowId::from_inner(winit_window.id()), + (winit_window, window_ent_id), + ); + } + MessageFromApp::WindowResized(window_id, new_window_size) => { + let Some(window_ent_id) = + windows.get(&window_id).map(|(_, ent_id)| ent_id) + else { + continue; + }; + + let Some(window_ent) = entity_obtainer.get_entity(*window_ent_id) else { + continue; + }; + + let Some(mut window) = window_ent.get_mut::<Window>() else { + continue; + }; + + window.set_inner_size(new_window_size); + + window.set_changed(); + } + MessageFromApp::WindowCloseRequested(window_id) => { + let Some(window_ent_id) = + windows.get(&window_id).map(|(_, ent_id)| ent_id) + else { + tracing::error!( + wid = ?window_id, + "Window does not exist in windowing context" + ); + continue; + }; + + actions.remove_comps::<(Window,)>(*window_ent_id); + } + MessageFromApp::WindowScaleFactorChanged(window_id, scale_factor) => { + let Some(window_ent_id) = + windows.get(&window_id).map(|(_, ent_id)| ent_id) + else { + tracing::error!( + wid = ?window_id, + "Window does not exist in windowing context" + ); + continue; + }; + + let Some(window_ent) = entity_obtainer.get_entity(*window_ent_id) else { + continue; + }; + + let Some(mut window) = window_ent.get_mut::<Window>() else { + continue; + }; + + window.set_scale_factor(scale_factor); + + window.set_changed(); + } + MessageFromApp::KeyboardKeyStateChanged(key, key_state) => { + keyboard.set_key_state(key, key_state); + } + MessageFromApp::MouseMoved { position_delta } => { + mouse_motion.position_delta += position_delta; + } + MessageFromApp::MouseButtonStateChanged(mouse_button, mouse_button_state) => { + mouse_buttons.set(mouse_button, mouse_button_state); + } + } + } +} + +fn handle_window_changed( + observe: Observe<'_, Pair<Changed, Window>>, + context: Single<Context>, +) +{ + for evt_match in &observe { + let window_ent_id = evt_match.id(); + + let window = evt_match.get_changed_comp(); + + let Some((winit_window, _)) = context.windows.get(&window.wid()) else { + tracing::error!( + wid = ?window.wid(), + entity_id = %window_ent_id, + "Window does not exist in windowing context", + ); + continue; + }; + + window.apply(winit_window); + + context.try_send_message_to_app(MessageToApp::SetWindowCursorGrabMode( + window.wid(), + window.cursor_grab_mode, + )); + } +} + +fn handle_window_removed( + observe: Observe<Pair<Removed, Window>>, + window_query: Query<(&Window,)>, + mut context: Single<Context>, + mut actions: Actions, +) +{ + for evt_match in &observe { + let window = evt_match.get_removed_comp(); + + context.windows.remove(window.wid()); + + actions.add_components(evt_match.id(), (WindowClosed,)); + } + + if window_query.iter().count() == 1 { + actions.stop(); + } +} + +#[derive(Debug, Sole)] +pub struct Context +{ + _thread: ThreadJoinHandle<()>, + is_dropped: Arc<AtomicBool>, + message_from_app_receiver: ChannelReceiver<MessageFromApp>, + message_to_app_sender: ChannelSender<MessageToApp>, + display_handle: Option<OwnedDisplayHandle>, + windows: MapVec<WindowId, (Arc<winit::window::Window>, Uid)>, +} + +impl Context +{ + pub fn display_handle(&self) -> Option<DisplayHandle<'_>> + { + let display_handle = self.display_handle.as_ref()?; + + display_handle.display_handle().ok() + } + + /// Returns the specified window as a window handle, if it exists. + /// + /// # Safety + /// The Window handle must only be used with thread safe APIs. + pub unsafe fn get_window_as_handle( + &self, + window_id: &WindowId, + ) -> Option<Result<WindowHandle<'_>, HandleError>> + { + self.windows.get(window_id).map(|(winit_window, _)| { + #[cfg(windows)] + { + use winit::platform::windows::WindowExtWindows; + + // SAFETY: I don't care + unsafe { winit_window.window_handle_any_thread() } + } + + #[cfg(not(windows))] + { + use raw_window_handle::HasWindowHandle; + + winit_window.window_handle() + } + }) + } + + fn try_send_message_to_app(&self, message: MessageToApp) + { + if let Err(err) = self.message_to_app_sender.try_send(message) { + let error = match &err { + TrySendError::Full(_) => TrySendError::Full(()), + TrySendError::Disconnected(_) => TrySendError::Disconnected(()), + }; + + let message = err.into_inner(); + + tracing::error!("Failed to send message {error}: {message:?}"); + } + } +} + +impl Default for Context +{ + fn default() -> Self + { + let is_dropped = Arc::new(AtomicBool::new(false)); + + let is_dropped_b = is_dropped.clone(); + + let (message_from_app_sender, message_from_app_receiver) = + bounded_channel::<MessageFromApp>(MESSAGE_FROM_APP_CHANNEL_CAP); + + let message_from_app_receiver_b = message_from_app_receiver.clone(); + + let (message_to_app_sender, message_to_app_receiver) = + bounded_channel::<MessageToApp>(MESSAGE_TO_APP_CHANNEL_CAP); + + Self { + _thread: spawn(move || { + let mut app = App { + message_from_app_sender, + message_from_app_receiver: message_from_app_receiver_b, + message_to_app_receiver, + is_dropped: is_dropped_b, + windows: MapVec::default(), + focused_window_id: None, + }; + + let event_loop = match create_event_loop() { + Ok(event_loop) => event_loop, + Err(err) => { + tracing::error!("Failed to create event loop: {err}"); + return; + } + }; + + event_loop.set_control_flow(EventLoopControlFlow::Poll); + + if let Err(err) = event_loop.run_app(&mut app) { + tracing::error!("Event loop error occurred: {err}"); + } + }), + is_dropped, + message_from_app_receiver, + message_to_app_sender, + display_handle: None, + windows: MapVec::default(), + } + } +} + +impl Drop for Context +{ + fn drop(&mut self) + { + self.is_dropped.store(true, Ordering::Relaxed); + } +} + +fn create_event_loop() -> Result<EventLoop<()>, EventLoopError> +{ + let mut event_loop_builder = EventLoop::builder(); + + #[cfg(any(x11_platform, wayland_platform))] + winit::platform::x11::EventLoopBuilderExtX11::with_any_thread( + &mut event_loop_builder, + true, + ); + + #[cfg(windows)] + winit::platform::windows::EventLoopBuilderExtWindows::with_any_thread( + &mut event_loop_builder, + true, + ); + + #[cfg(not(any(x11_platform, wayland_platform, windows)))] + compile_error!("Unsupported platform"); + + event_loop_builder.build() +} + +#[derive(Debug)] +enum MessageFromApp +{ + Init(OwnedDisplayHandle), + WindowCreated(Uid, Arc<WinitWindow>, WindowCreationAttributes), + WindowResized(WindowId, Dimens<u32>), + WindowCloseRequested(WindowId), + WindowScaleFactorChanged(WindowId, f64), + KeyboardKeyStateChanged(Key, KeyState), + MouseMoved + { + position_delta: Vec2<f64>, + }, + MouseButtonStateChanged(MouseButton, MouseButtonState), +} + +#[derive(Debug)] +enum MessageToApp +{ + CreateWindow(Uid, WindowCreationAttributes), + SetWindowCursorGrabMode(WindowId, CursorGrabMode), +} + +#[derive(Debug)] +struct App +{ + message_from_app_sender: ChannelSender<MessageFromApp>, + message_from_app_receiver: ChannelReceiver<MessageFromApp>, + message_to_app_receiver: ChannelReceiver<MessageToApp>, + is_dropped: Arc<AtomicBool>, + windows: MapVec<WindowId, (Weak<WinitWindow>, WindowSettings)>, + focused_window_id: Option<WindowId>, +} + +impl App +{ + fn handle_received_messages(&mut self, event_loop: &ActiveEventLoop) + { + for message in self.message_to_app_receiver.try_iter() { + match message { + MessageToApp::CreateWindow(window_ent_id, window_creation_attrs) => { + tracing::info!( + "Creating window with title {}", + window_creation_attrs.title() + ); + + let winit_window = Arc::new( + match event_loop + .create_window(window_creation_attrs.clone().into_inner()) + { + Ok(window) => window, + Err(err) => { + tracing::error!("Failed to create window: {err}"); + continue; + } + }, + ); + + tracing::info!("Created window has title {}", winit_window.title()); + + self.windows.insert( + WindowId::from_inner(winit_window.id()), + (Arc::downgrade(&winit_window), WindowSettings::default()), + ); + + self.send_message(MessageFromApp::WindowCreated( + window_ent_id, + winit_window, + window_creation_attrs, + )); + } + MessageToApp::SetWindowCursorGrabMode(window_id, cursor_grab_mode) => { + let Some((_, window_settings)) = self.windows.get_mut(&window_id) + else { + tracing::warn!( + window_id=?window_id, + "Cannot set window cursor grab mode. Window not found" + ); + + continue; + }; + + window_settings.cursor_grab_mode = cursor_grab_mode; + } + } + } + } + + fn send_message(&self, message: MessageFromApp) + { + if self.message_from_app_sender.is_full() { + tracing::warn!( + "Message channel is full! Dropping oldest message from channel" + ); + + self.message_from_app_receiver.try_recv().ok(); + } + + if let Err(err) = self.message_from_app_sender.try_send(message) { + let error = match &err { + TrySendError::Full(_) => TrySendError::Full(()), + TrySendError::Disconnected(_) => TrySendError::Disconnected(()), + }; + + let message = err.into_inner(); + + tracing::error!("Failed to send message {error}: {message:?}"); + } + } +} + +impl ApplicationHandler for App +{ + fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) + { + match cause { + StartCause::Init => { + self.send_message(MessageFromApp::Init( + event_loop.owned_display_handle(), + )); + } + StartCause::Poll => { + if self.is_dropped.load(Ordering::Relaxed) { + event_loop.exit(); + return; + } + + self.handle_received_messages(event_loop); + } + _ => {} + } + } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) + { + for (window, _) in self.windows.values() { + let Some(window) = window.upgrade() else { + continue; + }; + + window.request_redraw(); + } + } + + fn resumed(&mut self, _event_loop: &ActiveEventLoop) {} + + fn window_event( + &mut self, + _event_loop: &ActiveEventLoop, + window_id: WinitWindowId, + event: WindowEvent, + ) + { + match event { + WindowEvent::Resized(new_window_size) => { + self.send_message(MessageFromApp::WindowResized( + WindowId::from_inner(window_id), + new_window_size.into(), + )); + } + WindowEvent::CloseRequested => { + self.send_message(MessageFromApp::WindowCloseRequested( + WindowId::from_inner(window_id), + )); + } + WindowEvent::KeyboardInput { + device_id: _, + event: keyboard_event, + is_synthetic: _, + } => { + if keyboard_event.repeat { + return; + } + + let key_code = match keyboard_event.physical_key { + PhysicalKey::Code(key_code) => key_code, + PhysicalKey::Unidentified(native_key) => { + tracing::warn!("Ignoring unidentified key: {native_key:?}"); + return; + } + }; + + let key: Key = match key_code.try_into() { + Ok(key) => key, + Err(UnknownKeyCodeError) => { + tracing::warn!("Ignoring key with unknown key code {key_code:?}"); + return; + } + }; + + self.send_message(MessageFromApp::KeyboardKeyStateChanged( + key, + keyboard_event.state.into(), + )); + } + WindowEvent::MouseInput { device_id: _, state, button } => { + self.send_message(MessageFromApp::MouseButtonStateChanged( + button.into(), + state.into(), + )); + } + WindowEvent::Focused(is_focused) => { + if is_focused { + self.focused_window_id = Some(WindowId::from_inner(window_id)); + } else { + self.focused_window_id = None; + } + } + WindowEvent::ScaleFactorChanged { scale_factor, inner_size_writer: _ } => { + self.send_message(MessageFromApp::WindowScaleFactorChanged( + WindowId::from_inner(window_id), + scale_factor, + )); + } + _ => {} + } + } + + fn device_event( + &mut self, + _event_loop: &ActiveEventLoop, + _device_id: DeviceId, + device_event: DeviceEvent, + ) + { + match device_event { + DeviceEvent::MouseMotion { delta } => { + self.send_message(MessageFromApp::MouseMoved { + position_delta: Vec2 { x: delta.0, y: delta.1 }, + }); + + let Some(focused_window_id) = self.focused_window_id else { + return; + }; + + let Some((focused_window, focused_window_settings)) = + self.windows.get(&focused_window_id) + else { + tracing::error!( + window_id=?focused_window_id, + "Focused window not found" + ); + return; + }; + + if focused_window_settings.cursor_grab_mode != CursorGrabMode::Locked { + return; + } + + // TODO: This might need to be optimized + let Some(focused_window) = focused_window.upgrade() else { + return; + }; + + let focused_window_size = focused_window.inner_size(); + + if let Err(err) = focused_window.set_cursor_position(PhysicalPosition { + x: focused_window_size.width / 2, + y: focused_window_size.height / 2, + }) { + tracing::error!( + window_id=?focused_window_id, + "Failed to set cursor position in focused window: {err}" + ); + }; + } + _ => {} + } + } +} + +#[derive(Debug, Default)] +struct WindowSettings +{ + cursor_grab_mode: CursorGrabMode, +} diff --git a/engine/src/windowing/keyboard.rs b/engine/src/windowing/keyboard.rs new file mode 100644 index 0000000..a1c3e22 --- /dev/null +++ b/engine/src/windowing/keyboard.rs @@ -0,0 +1,791 @@ +use std::collections::HashMap; + +use ecs::Sole; + +#[derive(Debug, Default, Sole)] +pub struct Keyboard +{ + map: HashMap<Key, KeyData>, +} + +impl Keyboard +{ + /// Returns whether the given key was just pressed this frame. This function will + /// return `false` if the key was also pressed the previous frame. + pub fn just_pressed(&self, key: Key) -> bool + { + self.get_key_state(key) == KeyState::Pressed + && self.get_prev_key_state(key) == KeyState::Released + } + + /// Returns whether the given key was just released this frame. This function will + /// return `false` if the key was also released the previous frame. + pub fn just_released(&self, key: Key) -> bool + { + self.get_key_state(key) == KeyState::Released + && self.get_prev_key_state(key) == KeyState::Pressed + } + + /// Returns whether the given key is currently pressed. + pub fn pressed(&self, key: Key) -> bool + { + self.get_key_state(key) == KeyState::Pressed + } + + /// Returns whether the given key is currently released. + pub fn released(&self, key: Key) -> bool + { + self.get_key_state(key) == KeyState::Released + } + + #[must_use] + pub fn get_key_state(&self, key: Key) -> KeyState + { + let Some(key_data) = self.map.get(&key) else { + return KeyState::Released; + }; + + key_data.curr_state + } + + #[must_use] + pub fn get_prev_key_state(&self, key: Key) -> KeyState + { + let Some(key_data) = self.map.get(&key) else { + return KeyState::Released; + }; + + key_data.previous_state + } + + pub fn set_key_state(&mut self, key: Key, key_state: KeyState) + { + let key_data = self.map.entry(key).or_default(); + + key_data.curr_state = key_state; + } + + pub fn make_key_states_previous(&mut self) + { + for key_data in self.map.values_mut() { + key_data.previous_state = key_data.curr_state; + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum Key +{ + /// <kbd>`</kbd> on a US keyboard. This is also called a backtick or grave. + /// This is the <kbd>半角</kbd>/<kbd>全角</kbd>/<kbd>漢字</kbd> + /// (hankaku/zenkaku/kanji) key on Japanese keyboards + Backquote, + /// Used for both the US <kbd>\\</kbd> (on the 101-key layout) and also for the key + /// located between the <kbd>"</kbd> and <kbd>Enter</kbd> keys on row C of the 102-, + /// 104- and 106-key layouts. + /// Labeled <kbd>#</kbd> on a UK (102) keyboard. + Backslash, + /// <kbd>[</kbd> on a US keyboard. + BracketLeft, + /// <kbd>]</kbd> on a US keyboard. + BracketRight, + /// <kbd>,</kbd> on a US keyboard. + Comma, + /// <kbd>0</kbd> on a US keyboard. + Digit0, + /// <kbd>1</kbd> on a US keyboard. + Digit1, + /// <kbd>2</kbd> on a US keyboard. + Digit2, + /// <kbd>3</kbd> on a US keyboard. + Digit3, + /// <kbd>4</kbd> on a US keyboard. + Digit4, + /// <kbd>5</kbd> on a US keyboard. + Digit5, + /// <kbd>6</kbd> on a US keyboard. + Digit6, + /// <kbd>7</kbd> on a US keyboard. + Digit7, + /// <kbd>8</kbd> on a US keyboard. + Digit8, + /// <kbd>9</kbd> on a US keyboard. + Digit9, + /// <kbd>=</kbd> on a US keyboard. + Equal, + /// Located between the left <kbd>Shift</kbd> and <kbd>Z</kbd> keys. + /// Labeled <kbd>\\</kbd> on a UK keyboard. + IntlBackslash, + /// Located between the <kbd>/</kbd> and right <kbd>Shift</kbd> keys. + /// Labeled <kbd>\\</kbd> (ro) on a Japanese keyboard. + IntlRo, + /// Located between the <kbd>=</kbd> and <kbd>Backspace</kbd> keys. + /// Labeled <kbd>¥</kbd> (yen) on a Japanese keyboard. <kbd>\\</kbd> on a + /// Russian keyboard. + IntlYen, + /// <kbd>a</kbd> on a US keyboard. + /// Labeled <kbd>q</kbd> on an AZERTY (e.g., French) keyboard. + A, + /// <kbd>b</kbd> on a US keyboard. + B, + /// <kbd>c</kbd> on a US keyboard. + C, + /// <kbd>d</kbd> on a US keyboard. + D, + /// <kbd>e</kbd> on a US keyboard. + E, + /// <kbd>f</kbd> on a US keyboard. + F, + /// <kbd>g</kbd> on a US keyboard. + G, + /// <kbd>h</kbd> on a US keyboard. + H, + /// <kbd>i</kbd> on a US keyboard. + I, + /// <kbd>j</kbd> on a US keyboard. + J, + /// <kbd>k</kbd> on a US keyboard. + K, + /// <kbd>l</kbd> on a US keyboard. + L, + /// <kbd>m</kbd> on a US keyboard. + M, + /// <kbd>n</kbd> on a US keyboard. + N, + /// <kbd>o</kbd> on a US keyboard. + O, + /// <kbd>p</kbd> on a US keyboard. + P, + /// <kbd>q</kbd> on a US keyboard. + /// Labeled <kbd>a</kbd> on an AZERTY (e.g., French) keyboard. + Q, + /// <kbd>r</kbd> on a US keyboard. + R, + /// <kbd>s</kbd> on a US keyboard. + S, + /// <kbd>t</kbd> on a US keyboard. + T, + /// <kbd>u</kbd> on a US keyboard. + U, + /// <kbd>v</kbd> on a US keyboard. + V, + /// <kbd>w</kbd> on a US keyboard. + /// Labeled <kbd>z</kbd> on an AZERTY (e.g., French) keyboard. + W, + /// <kbd>x</kbd> on a US keyboard. + X, + /// <kbd>y</kbd> on a US keyboard. + /// Labeled <kbd>z</kbd> on a QWERTZ (e.g., German) keyboard. + Y, + /// <kbd>z</kbd> on a US keyboard. + /// Labeled <kbd>w</kbd> on an AZERTY (e.g., French) keyboard, and <kbd>y</kbd> on a + /// QWERTZ (e.g., German) keyboard. + Z, + /// <kbd>-</kbd> on a US keyboard. + Minus, + /// <kbd>.</kbd> on a US keyboard. + Period, + /// <kbd>'</kbd> on a US keyboard. + Quote, + /// <kbd>;</kbd> on a US keyboard. + Semicolon, + /// <kbd>/</kbd> on a US keyboard. + Slash, + /// <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>. + AltLeft, + /// <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>. + /// This is labeled <kbd>AltGr</kbd> on many keyboard layouts. + AltRight, + /// <kbd>Backspace</kbd> or <kbd>⌫</kbd>. + /// Labeled <kbd>Delete</kbd> on Apple keyboards. + Backspace, + /// <kbd>CapsLock</kbd> or <kbd>⇪</kbd> + CapsLock, + /// The application context menu key, which is typically found between the right + /// <kbd>Super</kbd> key and the right <kbd>Control</kbd> key. + ContextMenu, + /// <kbd>Control</kbd> or <kbd>⌃</kbd> + ControlLeft, + /// <kbd>Control</kbd> or <kbd>⌃</kbd> + ControlRight, + /// <kbd>Enter</kbd> or <kbd>↵</kbd>. Labeled <kbd>Return</kbd> on Apple keyboards. + Enter, + /// The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key. + SuperLeft, + /// The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key. + SuperRight, + /// <kbd>Shift</kbd> or <kbd>⇧</kbd> + ShiftLeft, + /// <kbd>Shift</kbd> or <kbd>⇧</kbd> + ShiftRight, + /// <kbd> </kbd> (space) + Space, + /// <kbd>Tab</kbd> or <kbd>⇥</kbd> + Tab, + /// Japanese: <kbd>変</kbd> (henkan) + Convert, + /// Japanese: <kbd>カタカナ</kbd>/<kbd>ひらがな</kbd>/<kbd>ローマ字</kbd> + /// (katakana/hiragana/romaji) + KanaMode, + /// Korean: HangulMode <kbd>한/영</kbd> (han/yeong) + /// + /// Japanese (Mac keyboard): <kbd>か</kbd> (kana) + Lang1, + /// Korean: Hanja <kbd>한</kbd> (hanja) + /// + /// Japanese (Mac keyboard): <kbd>英</kbd> (eisu) + Lang2, + /// Japanese (word-processing keyboard): Katakana + Lang3, + /// Japanese (word-processing keyboard): Hiragana + Lang4, + /// Japanese (word-processing keyboard): Zenkaku/Hankaku + Lang5, + /// Japanese: <kbd>無変換</kbd> (muhenkan) + NonConvert, + /// <kbd>⌦</kbd>. The forward delete key. + /// Note that on Apple keyboards, the key labelled <kbd>Delete</kbd> on the main part + /// of the keyboard is encoded as [`Backspace`]. + /// + /// [`Backspace`]: Self::Backspace + Delete, + /// <kbd>Page Down</kbd>, <kbd>End</kbd>, or <kbd>↘</kbd> + End, + /// <kbd>Help</kbd>. Not present on standard PC keyboards. + Help, + /// <kbd>Home</kbd> or <kbd>↖</kbd> + Home, + /// <kbd>Insert</kbd> or <kbd>Ins</kbd>. Not present on Apple keyboards. + Insert, + /// <kbd>Page Down</kbd>, <kbd>PgDn</kbd>, or <kbd>⇟</kbd> + PageDown, + /// <kbd>Page Up</kbd>, <kbd>PgUp</kbd>, or <kbd>⇞</kbd> + PageUp, + /// <kbd>↓</kbd> + ArrowDown, + /// <kbd>←</kbd> + ArrowLeft, + /// <kbd>→</kbd> + ArrowRight, + /// <kbd>↑</kbd> + ArrowUp, + /// On the Mac, this is used for the numpad <kbd>Clear</kbd> key. + NumLock, + /// <kbd>0 Ins</kbd> on a keyboard. <kbd>0</kbd> on a phone or remote control + Numpad0, + /// <kbd>1 End</kbd> on a keyboard. <kbd>1</kbd> or <kbd>1 QZ</kbd> on a phone or + /// remote control + Numpad1, + /// <kbd>2 ↓</kbd> on a keyboard. <kbd>2 ABC</kbd> on a phone or remote control + Numpad2, + /// <kbd>3 PgDn</kbd> on a keyboard. <kbd>3 DEF</kbd> on a phone or remote control + Numpad3, + /// <kbd>4 ←</kbd> on a keyboard. <kbd>4 GHI</kbd> on a phone or remote control + Numpad4, + /// <kbd>5</kbd> on a keyboard. <kbd>5 JKL</kbd> on a phone or remote control + Numpad5, + /// <kbd>6 →</kbd> on a keyboard. <kbd>6 MNO</kbd> on a phone or remote control + Numpad6, + /// <kbd>7 Home</kbd> on a keyboard. <kbd>7 PQRS</kbd> or <kbd>7 PRS</kbd> on a phone + /// or remote control + Numpad7, + /// <kbd>8 ↑</kbd> on a keyboard. <kbd>8 TUV</kbd> on a phone or remote control + Numpad8, + /// <kbd>9 PgUp</kbd> on a keyboard. <kbd>9 WXYZ</kbd> or <kbd>9 WXY</kbd> on a phone + /// or remote control + Numpad9, + /// <kbd>+</kbd> + NumpadAdd, + /// Found on the Microsoft Natural Keyboard. + NumpadBackspace, + /// <kbd>C</kbd> or <kbd>A</kbd> (All Clear). Also for use with numpads that have a + /// <kbd>Clear</kbd> key that is separate from the <kbd>NumLock</kbd> key. On the + /// Mac, the numpad <kbd>Clear</kbd> key is encoded as [`NumLock`]. + /// + /// [`NumLock`]: Self::NumLock + NumpadClear, + /// <kbd>C</kbd> (Clear Entry) + NumpadClearEntry, + /// <kbd>,</kbd> (thousands separator). For locales where the thousands separator + /// is a "." (e.g., Brazil), this key may generate a <kbd>.</kbd>. + NumpadComma, + /// <kbd>. Del</kbd>. For locales where the decimal separator is "," (e.g., + /// Brazil), this key may generate a <kbd>,</kbd>. + NumpadDecimal, + /// <kbd>/</kbd> + NumpadDivide, + NumpadEnter, + /// <kbd>=</kbd> + NumpadEqual, + /// <kbd>#</kbd> on a phone or remote control device. This key is typically found + /// below the <kbd>9</kbd> key and to the right of the <kbd>0</kbd> key. + NumpadHash, + /// <kbd>M</kbd> Add current entry to the value stored in memory. + NumpadMemoryAdd, + /// <kbd>M</kbd> Clear the value stored in memory. + NumpadMemoryClear, + /// <kbd>M</kbd> Replace the current entry with the value stored in memory. + NumpadMemoryRecall, + /// <kbd>M</kbd> Replace the value stored in memory with the current entry. + NumpadMemoryStore, + /// <kbd>M</kbd> Subtract current entry from the value stored in memory. + NumpadMemorySubtract, + /// <kbd>*</kbd> on a keyboard. For use with numpads that provide mathematical + /// operations (<kbd>+</kbd>, <kbd>-</kbd> <kbd>*</kbd> and <kbd>/</kbd>). + /// + /// Use `NumpadStar` for the <kbd>*</kbd> key on phones and remote controls. + NumpadMultiply, + /// <kbd>(</kbd> Found on the Microsoft Natural Keyboard. + NumpadParenLeft, + /// <kbd>)</kbd> Found on the Microsoft Natural Keyboard. + NumpadParenRight, + /// <kbd>*</kbd> on a phone or remote control device. + /// + /// This key is typically found below the <kbd>7</kbd> key and to the left of + /// the <kbd>0</kbd> key. + /// + /// Use <kbd>"NumpadMultiply"</kbd> for the <kbd>*</kbd> key on + /// numeric keypads. + NumpadStar, + /// <kbd>-</kbd> + NumpadSubtract, + /// <kbd>Esc</kbd> or <kbd>⎋</kbd> + Escape, + /// <kbd>Fn</kbd> This is typically a hardware key that does not generate a separate + /// code. + Fn, + /// <kbd>FLock</kbd> or <kbd>FnLock</kbd>. Function Lock key. Found on the Microsoft + /// Natural Keyboard. + FnLock, + /// <kbd>PrtScr SysRq</kbd> or <kbd>Print Screen</kbd> + PrintScreen, + /// <kbd>Scroll Lock</kbd> + ScrollLock, + /// <kbd>Pause Break</kbd> + Pause, + /// Some laptops place this key to the left of the <kbd>↑</kbd> key. + /// + /// This also the "back" button (triangle) on Android. + BrowserBack, + BrowserFavorites, + /// Some laptops place this key to the right of the <kbd>↑</kbd> key. + BrowserForward, + /// The "home" button on Android. + BrowserHome, + BrowserRefresh, + BrowserSearch, + BrowserStop, + /// <kbd>Eject</kbd> or <kbd>⏏</kbd>. This key is placed in the function section on + /// some Apple keyboards. + Eject, + /// Sometimes labelled <kbd>My Computer</kbd> on the keyboard + LaunchApp1, + /// Sometimes labelled <kbd>Calculator</kbd> on the keyboard + LaunchApp2, + LaunchMail, + MediaPlayPause, + MediaSelect, + MediaStop, + MediaTrackNext, + MediaTrackPrevious, + /// This key is placed in the function section on some Apple keyboards, replacing the + /// <kbd>Eject</kbd> key. + Power, + Sleep, + AudioVolumeDown, + AudioVolumeMute, + AudioVolumeUp, + WakeUp, + // Legacy modifier key. Also called "Super" in certain places. + Meta, + // Legacy modifier key. + Hyper, + Turbo, + Abort, + Resume, + Suspend, + /// Found on Sun’s USB keyboard. + Again, + /// Found on Sun’s USB keyboard. + Copy, + /// Found on Sun’s USB keyboard. + Cut, + /// Found on Sun’s USB keyboard. + Find, + /// Found on Sun’s USB keyboard. + Open, + /// Found on Sun’s USB keyboard. + Paste, + /// Found on Sun’s USB keyboard. + Props, + /// Found on Sun’s USB keyboard. + Select, + /// Found on Sun’s USB keyboard. + Undo, + /// Use for dedicated <kbd>ひらがな</kbd> key found on some Japanese word processing + /// keyboards. + Hiragana, + /// Use for dedicated <kbd>カタカナ</kbd> key found on some Japanese word processing + /// keyboards. + Katakana, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F1, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F2, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F3, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F4, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F5, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F6, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F7, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F8, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F9, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F10, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F11, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F12, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F13, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F14, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F15, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F16, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F17, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F18, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F19, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F20, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F21, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F22, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F23, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F24, + /// General-purpose function key. + F25, + /// General-purpose function key. + F26, + /// General-purpose function key. + F27, + /// General-purpose function key. + F28, + /// General-purpose function key. + F29, + /// General-purpose function key. + F30, + /// General-purpose function key. + F31, + /// General-purpose function key. + F32, + /// General-purpose function key. + F33, + /// General-purpose function key. + F34, + /// General-purpose function key. + F35, +} + +impl TryFrom<winit::keyboard::KeyCode> for Key +{ + type Error = UnknownKeyCodeError; + + fn try_from(key_code: winit::keyboard::KeyCode) -> Result<Self, Self::Error> + { + match key_code { + winit::keyboard::KeyCode::Backquote => Ok(Self::Backquote), + winit::keyboard::KeyCode::Backslash => Ok(Self::Backslash), + winit::keyboard::KeyCode::BracketLeft => Ok(Self::BracketLeft), + winit::keyboard::KeyCode::BracketRight => Ok(Self::BracketRight), + winit::keyboard::KeyCode::Comma => Ok(Self::Comma), + winit::keyboard::KeyCode::Digit0 => Ok(Self::Digit0), + winit::keyboard::KeyCode::Digit1 => Ok(Self::Digit1), + winit::keyboard::KeyCode::Digit2 => Ok(Self::Digit2), + winit::keyboard::KeyCode::Digit3 => Ok(Self::Digit3), + winit::keyboard::KeyCode::Digit4 => Ok(Self::Digit4), + winit::keyboard::KeyCode::Digit5 => Ok(Self::Digit5), + winit::keyboard::KeyCode::Digit6 => Ok(Self::Digit6), + winit::keyboard::KeyCode::Digit7 => Ok(Self::Digit7), + winit::keyboard::KeyCode::Digit8 => Ok(Self::Digit8), + winit::keyboard::KeyCode::Digit9 => Ok(Self::Digit9), + winit::keyboard::KeyCode::Equal => Ok(Self::Equal), + winit::keyboard::KeyCode::IntlBackslash => Ok(Self::IntlBackslash), + winit::keyboard::KeyCode::IntlRo => Ok(Self::IntlRo), + winit::keyboard::KeyCode::IntlYen => Ok(Self::IntlYen), + winit::keyboard::KeyCode::KeyA => Ok(Self::A), + winit::keyboard::KeyCode::KeyB => Ok(Self::B), + winit::keyboard::KeyCode::KeyC => Ok(Self::C), + winit::keyboard::KeyCode::KeyD => Ok(Self::D), + winit::keyboard::KeyCode::KeyE => Ok(Self::E), + winit::keyboard::KeyCode::KeyF => Ok(Self::F), + winit::keyboard::KeyCode::KeyG => Ok(Self::G), + winit::keyboard::KeyCode::KeyH => Ok(Self::H), + winit::keyboard::KeyCode::KeyI => Ok(Self::I), + winit::keyboard::KeyCode::KeyJ => Ok(Self::J), + winit::keyboard::KeyCode::KeyK => Ok(Self::K), + winit::keyboard::KeyCode::KeyL => Ok(Self::L), + winit::keyboard::KeyCode::KeyM => Ok(Self::M), + winit::keyboard::KeyCode::KeyN => Ok(Self::N), + winit::keyboard::KeyCode::KeyO => Ok(Self::O), + winit::keyboard::KeyCode::KeyP => Ok(Self::P), + winit::keyboard::KeyCode::KeyQ => Ok(Self::Q), + winit::keyboard::KeyCode::KeyR => Ok(Self::R), + winit::keyboard::KeyCode::KeyS => Ok(Self::S), + winit::keyboard::KeyCode::KeyT => Ok(Self::T), + winit::keyboard::KeyCode::KeyU => Ok(Self::U), + winit::keyboard::KeyCode::KeyV => Ok(Self::V), + winit::keyboard::KeyCode::KeyW => Ok(Self::W), + winit::keyboard::KeyCode::KeyX => Ok(Self::X), + winit::keyboard::KeyCode::KeyY => Ok(Self::Y), + winit::keyboard::KeyCode::KeyZ => Ok(Self::Z), + winit::keyboard::KeyCode::Minus => Ok(Self::Minus), + winit::keyboard::KeyCode::Period => Ok(Self::Period), + winit::keyboard::KeyCode::Quote => Ok(Self::Quote), + winit::keyboard::KeyCode::Semicolon => Ok(Self::Semicolon), + winit::keyboard::KeyCode::Slash => Ok(Self::Slash), + winit::keyboard::KeyCode::AltLeft => Ok(Self::AltLeft), + winit::keyboard::KeyCode::AltRight => Ok(Self::AltRight), + winit::keyboard::KeyCode::Backspace => Ok(Self::Backspace), + winit::keyboard::KeyCode::CapsLock => Ok(Self::CapsLock), + winit::keyboard::KeyCode::ContextMenu => Ok(Self::ContextMenu), + winit::keyboard::KeyCode::ControlLeft => Ok(Self::ControlLeft), + winit::keyboard::KeyCode::ControlRight => Ok(Self::ControlRight), + winit::keyboard::KeyCode::Enter => Ok(Self::Enter), + winit::keyboard::KeyCode::SuperLeft => Ok(Self::SuperLeft), + winit::keyboard::KeyCode::SuperRight => Ok(Self::SuperRight), + winit::keyboard::KeyCode::ShiftLeft => Ok(Self::ShiftLeft), + winit::keyboard::KeyCode::ShiftRight => Ok(Self::ShiftRight), + winit::keyboard::KeyCode::Space => Ok(Self::Space), + winit::keyboard::KeyCode::Tab => Ok(Self::Tab), + winit::keyboard::KeyCode::Convert => Ok(Self::Convert), + winit::keyboard::KeyCode::KanaMode => Ok(Self::KanaMode), + winit::keyboard::KeyCode::Lang1 => Ok(Self::Lang1), + winit::keyboard::KeyCode::Lang2 => Ok(Self::Lang2), + winit::keyboard::KeyCode::Lang3 => Ok(Self::Lang3), + winit::keyboard::KeyCode::Lang4 => Ok(Self::Lang4), + winit::keyboard::KeyCode::Lang5 => Ok(Self::Lang5), + winit::keyboard::KeyCode::NonConvert => Ok(Self::NonConvert), + winit::keyboard::KeyCode::Delete => Ok(Self::Delete), + winit::keyboard::KeyCode::End => Ok(Self::End), + winit::keyboard::KeyCode::Help => Ok(Self::Help), + winit::keyboard::KeyCode::Home => Ok(Self::Home), + winit::keyboard::KeyCode::Insert => Ok(Self::Insert), + winit::keyboard::KeyCode::PageDown => Ok(Self::PageDown), + winit::keyboard::KeyCode::PageUp => Ok(Self::PageUp), + winit::keyboard::KeyCode::ArrowDown => Ok(Self::ArrowDown), + winit::keyboard::KeyCode::ArrowLeft => Ok(Self::ArrowLeft), + winit::keyboard::KeyCode::ArrowRight => Ok(Self::ArrowRight), + winit::keyboard::KeyCode::ArrowUp => Ok(Self::ArrowUp), + winit::keyboard::KeyCode::NumLock => Ok(Self::NumLock), + winit::keyboard::KeyCode::Numpad0 => Ok(Self::Numpad0), + winit::keyboard::KeyCode::Numpad1 => Ok(Self::Numpad1), + winit::keyboard::KeyCode::Numpad2 => Ok(Self::Numpad2), + winit::keyboard::KeyCode::Numpad3 => Ok(Self::Numpad3), + winit::keyboard::KeyCode::Numpad4 => Ok(Self::Numpad4), + winit::keyboard::KeyCode::Numpad5 => Ok(Self::Numpad5), + winit::keyboard::KeyCode::Numpad6 => Ok(Self::Numpad6), + winit::keyboard::KeyCode::Numpad7 => Ok(Self::Numpad7), + winit::keyboard::KeyCode::Numpad8 => Ok(Self::Numpad8), + winit::keyboard::KeyCode::Numpad9 => Ok(Self::Numpad9), + winit::keyboard::KeyCode::NumpadAdd => Ok(Self::NumpadAdd), + winit::keyboard::KeyCode::NumpadBackspace => Ok(Self::NumpadBackspace), + winit::keyboard::KeyCode::NumpadClear => Ok(Self::NumpadClear), + winit::keyboard::KeyCode::NumpadClearEntry => Ok(Self::NumpadClearEntry), + winit::keyboard::KeyCode::NumpadComma => Ok(Self::NumpadComma), + winit::keyboard::KeyCode::NumpadDecimal => Ok(Self::NumpadDecimal), + winit::keyboard::KeyCode::NumpadDivide => Ok(Self::NumpadDivide), + winit::keyboard::KeyCode::NumpadEnter => Ok(Self::NumpadEnter), + winit::keyboard::KeyCode::NumpadEqual => Ok(Self::NumpadEqual), + winit::keyboard::KeyCode::NumpadHash => Ok(Self::NumpadHash), + winit::keyboard::KeyCode::NumpadMemoryAdd => Ok(Self::NumpadMemoryAdd), + winit::keyboard::KeyCode::NumpadMemoryClear => Ok(Self::NumpadMemoryClear), + winit::keyboard::KeyCode::NumpadMemoryRecall => Ok(Self::NumpadMemoryRecall), + winit::keyboard::KeyCode::NumpadMemoryStore => Ok(Self::NumpadMemoryStore), + winit::keyboard::KeyCode::NumpadMemorySubtract => { + Ok(Self::NumpadMemorySubtract) + } + winit::keyboard::KeyCode::NumpadMultiply => Ok(Self::NumpadMultiply), + winit::keyboard::KeyCode::NumpadParenLeft => Ok(Self::NumpadParenLeft), + winit::keyboard::KeyCode::NumpadParenRight => Ok(Self::NumpadParenRight), + winit::keyboard::KeyCode::NumpadStar => Ok(Self::NumpadStar), + winit::keyboard::KeyCode::NumpadSubtract => Ok(Self::NumpadSubtract), + winit::keyboard::KeyCode::Escape => Ok(Self::Escape), + winit::keyboard::KeyCode::Fn => Ok(Self::Fn), + winit::keyboard::KeyCode::FnLock => Ok(Self::FnLock), + winit::keyboard::KeyCode::PrintScreen => Ok(Self::PrintScreen), + winit::keyboard::KeyCode::ScrollLock => Ok(Self::ScrollLock), + winit::keyboard::KeyCode::Pause => Ok(Self::Pause), + winit::keyboard::KeyCode::BrowserBack => Ok(Self::BrowserBack), + winit::keyboard::KeyCode::BrowserFavorites => Ok(Self::BrowserFavorites), + winit::keyboard::KeyCode::BrowserForward => Ok(Self::BrowserForward), + winit::keyboard::KeyCode::BrowserHome => Ok(Self::BrowserHome), + winit::keyboard::KeyCode::BrowserRefresh => Ok(Self::BrowserRefresh), + winit::keyboard::KeyCode::BrowserSearch => Ok(Self::BrowserSearch), + winit::keyboard::KeyCode::BrowserStop => Ok(Self::BrowserStop), + winit::keyboard::KeyCode::Eject => Ok(Self::Eject), + winit::keyboard::KeyCode::LaunchApp1 => Ok(Self::LaunchApp1), + winit::keyboard::KeyCode::LaunchApp2 => Ok(Self::LaunchApp2), + winit::keyboard::KeyCode::LaunchMail => Ok(Self::LaunchMail), + winit::keyboard::KeyCode::MediaPlayPause => Ok(Self::MediaPlayPause), + winit::keyboard::KeyCode::MediaSelect => Ok(Self::MediaSelect), + winit::keyboard::KeyCode::MediaStop => Ok(Self::MediaStop), + winit::keyboard::KeyCode::MediaTrackNext => Ok(Self::MediaTrackNext), + winit::keyboard::KeyCode::MediaTrackPrevious => Ok(Self::MediaTrackPrevious), + winit::keyboard::KeyCode::Power => Ok(Self::Power), + winit::keyboard::KeyCode::Sleep => Ok(Self::Sleep), + winit::keyboard::KeyCode::AudioVolumeDown => Ok(Self::AudioVolumeDown), + winit::keyboard::KeyCode::AudioVolumeMute => Ok(Self::AudioVolumeMute), + winit::keyboard::KeyCode::AudioVolumeUp => Ok(Self::AudioVolumeUp), + winit::keyboard::KeyCode::WakeUp => Ok(Self::WakeUp), + winit::keyboard::KeyCode::Meta => Ok(Self::Meta), + winit::keyboard::KeyCode::Hyper => Ok(Self::Hyper), + winit::keyboard::KeyCode::Turbo => Ok(Self::Turbo), + winit::keyboard::KeyCode::Abort => Ok(Self::Abort), + winit::keyboard::KeyCode::Resume => Ok(Self::Resume), + winit::keyboard::KeyCode::Suspend => Ok(Self::Suspend), + winit::keyboard::KeyCode::Again => Ok(Self::Again), + winit::keyboard::KeyCode::Copy => Ok(Self::Copy), + winit::keyboard::KeyCode::Cut => Ok(Self::Cut), + winit::keyboard::KeyCode::Find => Ok(Self::Find), + winit::keyboard::KeyCode::Open => Ok(Self::Open), + winit::keyboard::KeyCode::Paste => Ok(Self::Paste), + winit::keyboard::KeyCode::Props => Ok(Self::Props), + winit::keyboard::KeyCode::Select => Ok(Self::Select), + winit::keyboard::KeyCode::Undo => Ok(Self::Undo), + winit::keyboard::KeyCode::Hiragana => Ok(Self::Hiragana), + winit::keyboard::KeyCode::Katakana => Ok(Self::Katakana), + winit::keyboard::KeyCode::F1 => Ok(Self::F1), + winit::keyboard::KeyCode::F2 => Ok(Self::F2), + winit::keyboard::KeyCode::F3 => Ok(Self::F3), + winit::keyboard::KeyCode::F4 => Ok(Self::F4), + winit::keyboard::KeyCode::F5 => Ok(Self::F5), + winit::keyboard::KeyCode::F6 => Ok(Self::F6), + winit::keyboard::KeyCode::F7 => Ok(Self::F7), + winit::keyboard::KeyCode::F8 => Ok(Self::F8), + winit::keyboard::KeyCode::F9 => Ok(Self::F9), + winit::keyboard::KeyCode::F10 => Ok(Self::F10), + winit::keyboard::KeyCode::F11 => Ok(Self::F11), + winit::keyboard::KeyCode::F12 => Ok(Self::F12), + winit::keyboard::KeyCode::F13 => Ok(Self::F13), + winit::keyboard::KeyCode::F14 => Ok(Self::F14), + winit::keyboard::KeyCode::F15 => Ok(Self::F15), + winit::keyboard::KeyCode::F16 => Ok(Self::F16), + winit::keyboard::KeyCode::F17 => Ok(Self::F17), + winit::keyboard::KeyCode::F18 => Ok(Self::F18), + winit::keyboard::KeyCode::F19 => Ok(Self::F19), + winit::keyboard::KeyCode::F20 => Ok(Self::F20), + winit::keyboard::KeyCode::F21 => Ok(Self::F21), + winit::keyboard::KeyCode::F22 => Ok(Self::F22), + winit::keyboard::KeyCode::F23 => Ok(Self::F23), + winit::keyboard::KeyCode::F24 => Ok(Self::F24), + winit::keyboard::KeyCode::F25 => Ok(Self::F25), + winit::keyboard::KeyCode::F26 => Ok(Self::F26), + winit::keyboard::KeyCode::F27 => Ok(Self::F27), + winit::keyboard::KeyCode::F28 => Ok(Self::F28), + winit::keyboard::KeyCode::F29 => Ok(Self::F29), + winit::keyboard::KeyCode::F30 => Ok(Self::F30), + winit::keyboard::KeyCode::F31 => Ok(Self::F31), + winit::keyboard::KeyCode::F32 => Ok(Self::F32), + winit::keyboard::KeyCode::F33 => Ok(Self::F33), + winit::keyboard::KeyCode::F34 => Ok(Self::F34), + winit::keyboard::KeyCode::F35 => Ok(Self::F35), + _ => Err(UnknownKeyCodeError), + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("Unknown key code")] +pub struct UnknownKeyCodeError; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum KeyState +{ + Pressed, + Released, +} + +impl KeyState +{ + #[must_use] + #[inline] + pub fn is_pressed(&self) -> bool + { + matches!(self, Self::Pressed) + } + + #[must_use] + #[inline] + pub fn is_released(&self) -> bool + { + matches!(self, Self::Released) + } +} + +impl From<winit::event::ElementState> for KeyState +{ + fn from(element_state: winit::event::ElementState) -> Self + { + match element_state { + winit::event::ElementState::Pressed => Self::Pressed, + winit::event::ElementState::Released => Self::Released, + } + } +} + +#[derive(Debug)] +struct KeyData +{ + curr_state: KeyState, + previous_state: KeyState, +} + +impl Default for KeyData +{ + fn default() -> Self + { + KeyData { + curr_state: KeyState::Released, + previous_state: KeyState::Released, + } + } +} diff --git a/engine/src/windowing/mouse.rs b/engine/src/windowing/mouse.rs new file mode 100644 index 0000000..1afe594 --- /dev/null +++ b/engine/src/windowing/mouse.rs @@ -0,0 +1,136 @@ +use std::collections::HashMap; + +use ecs::Sole; + +use crate::vector::Vec2; + +#[derive(Debug, Default, Clone, Sole)] +#[non_exhaustive] +pub struct Motion +{ + pub position_delta: Vec2<f64>, +} + +/// Mouse buttons. +#[derive(Debug, Default, Sole)] +pub struct Buttons +{ + map: HashMap<Button, ButtonData>, +} + +impl Buttons +{ + pub fn get(&self, button: Button) -> ButtonState + { + let Some(button_data) = self.map.get(&button) else { + return ButtonState::Released; + }; + + button_data.current_state + } + + pub fn get_previous(&self, button: Button) -> ButtonState + { + let Some(button_data) = self.map.get(&button) else { + return ButtonState::Released; + }; + + button_data.previous_state + } + + pub fn set(&mut self, button: Button, button_state: ButtonState) + { + let button_data = self.map.entry(button).or_default(); + + button_data.current_state = button_state; + } + + pub(crate) fn make_states_previous(&mut self) + { + for button_data in self.map.values_mut() { + button_data.previous_state = button_data.current_state; + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Button +{ + Left, + Right, + Middle, + Back, + Forward, + Other(u16), +} + +impl From<winit::event::MouseButton> for Button +{ + fn from(mouse_button: winit::event::MouseButton) -> Self + { + match mouse_button { + winit::event::MouseButton::Left => Self::Left, + winit::event::MouseButton::Right => Self::Right, + winit::event::MouseButton::Middle => Self::Middle, + winit::event::MouseButton::Back => Self::Back, + winit::event::MouseButton::Forward => Self::Forward, + winit::event::MouseButton::Other(other_mouse_button) => { + Self::Other(other_mouse_button) + } + } + } +} + +/// Mouse button state. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ButtonState +{ + Pressed, + Released, +} + +impl ButtonState +{ + #[must_use] + #[inline] + pub fn is_pressed(&self) -> bool + { + matches!(self, Self::Pressed) + } + + #[must_use] + #[inline] + pub fn is_released(&self) -> bool + { + matches!(self, Self::Released) + } +} + +impl From<winit::event::ElementState> for ButtonState +{ + fn from(element_state: winit::event::ElementState) -> Self + { + match element_state { + winit::event::ElementState::Pressed => Self::Pressed, + winit::event::ElementState::Released => Self::Released, + } + } +} + +#[derive(Debug)] +struct ButtonData +{ + current_state: ButtonState, + previous_state: ButtonState, +} + +impl Default for ButtonData +{ + fn default() -> Self + { + Self { + current_state: ButtonState::Released, + previous_state: ButtonState::Released, + } + } +} diff --git a/engine/src/windowing/window.rs b/engine/src/windowing/window.rs new file mode 100644 index 0000000..627bdec --- /dev/null +++ b/engine/src/windowing/window.rs @@ -0,0 +1,183 @@ +use std::borrow::Cow; + +use ecs::Component; + +use crate::data_types::dimens::Dimens; + +pub mod platform; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id +{ + inner: winit::window::WindowId, +} + +impl Id +{ + pub(crate) fn from_inner(inner: winit::window::WindowId) -> Self + { + Self { inner } + } +} + +macro_rules! impl_creation_attributes_field_fns { + ($field: ident, ($($getter_ret_pre: tt)?), $getter_ret_type: ty, $field_type: ty) => { + impl CreationAttributes + { + pub fn $field(&self) -> $getter_ret_type + { + $($getter_ret_pre)? self.attrs.$field + } + + paste::paste! { + pub fn [<with_ $field>](mut self, $field: impl Into<$field_type>) -> Self { + self.attrs.$field = $field.into(); + self + } + } + } + }; +} + +#[derive(Debug, Component, Clone)] +#[non_exhaustive] +pub struct CreationAttributes +{ + attrs: winit::window::WindowAttributes, +} + +impl_creation_attributes_field_fns!(title, (&), &str, String); +impl_creation_attributes_field_fns!(transparent, (), bool, bool); + +impl CreationAttributes +{ + #[cfg(target_os = "linux")] + pub fn with_x11_visual(mut self, visual_id: XVisualID) -> Self + { + use winit::platform::x11::WindowAttributesExtX11; + + self.attrs = self.attrs.with_x11_visual(visual_id); + + self + } +} + +impl CreationAttributes +{ + pub(crate) fn into_inner(self) -> winit::window::WindowAttributes + { + self.attrs + } +} + +impl Default for CreationAttributes +{ + fn default() -> Self + { + CreationAttributes { + attrs: winit::window::WindowAttributes::default().with_title("Application"), + } + } +} + +#[derive(Debug, Component, Clone, Copy)] +pub struct CreationReady; + +#[derive(Debug, Component)] +#[non_exhaustive] +pub struct Window +{ + pub title: Cow<'static, str>, + pub cursor_visible: bool, + pub cursor_grab_mode: CursorGrabMode, + wid: Id, + inner_size: Dimens<u32>, + scale_factor: f64, +} + +impl Window +{ + pub fn wid(&self) -> Id + { + self.wid + } + + pub fn inner_size(&self) -> &Dimens<u32> + { + &self.inner_size + } + + pub fn scale_factor(&self) -> f64 + { + self.scale_factor + } + + pub(crate) fn new( + winit_window: &winit::window::Window, + creation_attrs: &CreationAttributes, + ) -> Self + { + Self { + title: creation_attrs.title().to_string().into(), + cursor_visible: true, + cursor_grab_mode: CursorGrabMode::None, + wid: Id::from_inner(winit_window.id()), + inner_size: winit_window.inner_size().into(), + scale_factor: winit_window.scale_factor(), + } + } + + pub(crate) fn apply(&self, winit_window: &winit::window::Window) + { + winit_window.set_title(&self.title); + winit_window.set_cursor_visible(self.cursor_visible); + } + + pub(crate) fn set_inner_size(&mut self, inner_size: Dimens<u32>) + { + self.inner_size = inner_size; + } + + pub(crate) fn set_scale_factor(&mut self, scale_factor: f64) + { + self.scale_factor = scale_factor; + } +} + +#[derive(Debug, Component)] +pub struct Closed; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CursorGrabMode +{ + #[default] + None, + + /// The cursor is locked to a specific position in the window. + Locked, +} + +/// A unique identifier for an X11 visual. +pub type XVisualID = u32; + +impl<P> From<winit::dpi::PhysicalSize<P>> for Dimens<P> +{ + fn from(size: winit::dpi::PhysicalSize<P>) -> Self + { + Self { + width: size.width, + height: size.height, + } + } +} + +impl<P> From<Dimens<P>> for winit::dpi::PhysicalSize<P> +{ + fn from(dimens: Dimens<P>) -> Self + { + Self { + width: dimens.width, + height: dimens.height, + } + } +} diff --git a/engine/src/windowing/window/platform.rs b/engine/src/windowing/window/platform.rs new file mode 100644 index 0000000..f3908a2 --- /dev/null +++ b/engine/src/windowing/window/platform.rs @@ -0,0 +1,12 @@ +#[cfg(x11_platform)] +pub mod x11 +{ + use std::ffi::c_void; + + pub type XlibErrorHook = Box<dyn Fn(*mut c_void, *mut c_void) -> bool + Send + Sync>; + + pub fn register_xlib_error_hook(hook: XlibErrorHook) + { + winit::platform::x11::register_xlib_error_hook(hook); + } +} diff --git a/engine/src/work_queue.rs b/engine/src/work_queue.rs new file mode 100644 index 0000000..7226c7d --- /dev/null +++ b/engine/src/work_queue.rs @@ -0,0 +1,44 @@ +use std::marker::PhantomData; +use std::sync::mpsc::{channel as mpsc_channel, Sender as MpscSender}; +use std::thread::JoinHandle as ThreadHandle; + +pub struct Work<UserData: Send + Sync + 'static> +{ + pub func: fn(UserData), + pub user_data: UserData, +} + +#[derive(Debug)] +pub struct WorkQueue<UserData: Send + Sync + 'static> +{ + work_sender: MpscSender<Work<UserData>>, + _thread: ThreadHandle<()>, + _pd: PhantomData<UserData>, +} + +impl<UserData: Send + Sync + 'static> WorkQueue<UserData> +{ + pub fn new() -> Self + { + let (work_sender, work_receiver) = mpsc_channel::<Work<UserData>>(); + + Self { + work_sender, + _thread: std::thread::spawn(move || { + let work_receiver = work_receiver; + + while let Ok(work) = work_receiver.recv() { + (work.func)(work.user_data); + } + }), + _pd: PhantomData, + } + } + + pub fn add_work(&self, work: Work<UserData>) + { + if self.work_sender.send(work).is_err() { + tracing::error!("Cannot add work to work queue. Work queue thread is dead"); + } + } +} |
