diff --git a/default.nix b/default.nix index 4b49658..2a287bb 100644 --- a/default.nix +++ b/default.nix @@ -1,5 +1,4 @@ -{ - lib, +{ lib, rustPlatform, nix-gitignore, bash, @@ -11,43 +10,63 @@ pkg-config, libxkbcommon, pkgs, + stdenv, + targetPackages ? pkgs, }: let version = (builtins.fromTOML (builtins.readFile ./engine/Cargo.toml)).package.version; src = nix-gitignore.gitignoreSource [] ./.; in - rustPlatform.buildRustPackage rec { - pname = "zenyx"; - inherit src version; - cargoLock.lockFile = ./Cargo.lock; - nativeBuildInputs = [ - makeWrapper - pkg-config - ]; - buildInputs = with pkgs; [ - wayland - vulkan-loader - libxkbcommon - libGL - libxkbcommon - xorg.libXcursor - xorg.libXrandr - xorg.libXi - xorg.libX11 - xorg.libxcb - bash - dav1d - ]; - doCheck = false; - LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath buildInputs}"; +rustPlatform.buildRustPackage rec { + pname = "zenyx"; + inherit src version; + cargoLock.lockFile = ./Cargo.lock; - fixupPhase = '' - wrapProgram $out/bin/${pname} --set PATH ${bash}/bin:\$PATH --set LD_LIBRARY_PATH ${pkgs.lib.makeLibraryPath buildInputs} - ''; + nativeBuildInputs = [ + pkg-config + ] ++ lib.optionals stdenv.targetPlatform.isDarwin [ + targetPackages.darwin.apple_sdk.frameworks.CoreServices + ]; - meta = { - description = "Test"; - license = lib.licenses.mit; - platforms = lib.platforms.linux; - mainProgram = "zenyx"; - }; - } + buildInputs = with targetPackages; [ + dav1d + ] ++ lib.optionals (stdenv.targetPlatform.isLinux || stdenv.targetPlatform.isWindows) [ + vulkan-loader + ] ++ lib.optionals stdenv.targetPlatform.isLinux [ + makeWrapper + wayland + libxkbcommon + libGL + xorg.libXcursor + xorg.libXrandr + xorg.libXi + xorg.libX11 + xorg.libxcb + bash + ] ++ lib.optionals stdenv.targetPlatform.isDarwin [ + makeWrapper + darwin.apple_sdk.frameworks.Cocoa + darwin.apple_sdk.frameworks.Metal + darwin.apple_sdk.frameworks.CoreVideo + darwin.apple_sdk.frameworks.QuartzCore + ]; + + CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER = + lib.optionalString stdenv.targetPlatform.isWindows "${stdenv.cc.targetPrefix}gcc"; + + NIX_LDFLAGS = lib.optionalString stdenv.targetPlatform.isDarwin "-framework CoreFoundation"; + + postInstall = lib.optionalString stdenv.targetPlatform.isLinux '' + wrapProgram $out/bin/${pname} \ + --prefix PATH : ${lib.makeBinPath [ bash ]} \ + --set LD_LIBRARY_PATH ${lib.makeLibraryPath buildInputs} + ''; + + doCheck = false; + + meta = { + description = "Cross-platform WSYWIG Game Engine"; + license = lib.licenses.mit; + platforms = lib.platforms.all; + mainProgram = "zenyx"; + }; +} \ No newline at end of file diff --git a/engine/src/core/ecs/mod.rs b/engine/src/core/ecs/mod.rs index e69de29..37b0ab7 100644 --- a/engine/src/core/ecs/mod.rs +++ b/engine/src/core/ecs/mod.rs @@ -0,0 +1,170 @@ +use std::any::{Any, TypeId}; +use std::collections::HashMap; +use std::sync::Mutex; + +pub trait Component: Sized + 'static { + fn update(&mut self, delta_time: f32); + fn serialize(&self) -> Vec; + fn deserialize(data: &[u8; 6]) -> Self; +} + +pub trait Entity: Sized { + fn add_component(&mut self, component: C); + fn remove_component(&mut self); + fn get_component(&self) -> Option<&C>; + fn serialize(&self) -> Vec; + fn deserialize(data: &[u8; 6]) -> Self; +} +lazy_static::lazy_static! { + // Global registry mapping component TypeId to a unique bit flag. + static ref COMPONENT_REGISTRY: Mutex> = Mutex::new(HashMap::new()); + static ref NEXT_COMPONENT_BIT: Mutex = Mutex::new(1); +} + +// To allow dynamic dispatch on components (even though Component itself is not object‐safe) +// we wrap them in an object–safe trait. +pub trait ComponentObject: Any { + fn update_obj(&mut self, delta_time: f32); + fn serialize_obj(&self) -> Vec; + fn as_any(&self) -> &dyn Any; +} +impl ComponentObject for T { + fn update_obj(&mut self, delta_time: f32) { + T::update(self, delta_time) + } + fn serialize_obj(&self) -> Vec { + T::serialize(self) + } + fn as_any(&self) -> &dyn Any { + self + } +} + +pub struct EntityImpl { + id: usize, + bitmask: u64, + // The key is the unique bit flag for the component type. + components: HashMap>, +} + +impl EntityImpl { + pub fn new(id: usize) -> Self { + EntityImpl { + id, + bitmask: 0, + components: HashMap::new(), + } + } +} + +impl Entity for EntityImpl { + fn add_component(&mut self, component: C) { + let type_id = TypeId::of::(); + let mut registry = COMPONENT_REGISTRY.lock().unwrap(); + let bit = registry.entry(type_id).or_insert_with(|| { + let mut next_bit = NEXT_COMPONENT_BIT.lock().unwrap(); + let current = *next_bit; + *next_bit *= 2; + current + }); + self.bitmask |= *bit; + self.components.insert(*bit, Box::new(component)); + } + + fn remove_component(&mut self) { + let type_id = TypeId::of::(); + if let Some(&bit) = COMPONENT_REGISTRY.lock().unwrap().get(&type_id) { + self.bitmask &= !bit; + } + } + + fn get_component(&self) -> Option<&C> { + let type_id = TypeId::of::(); + if let Some(&bit) = COMPONENT_REGISTRY.lock().unwrap().get(&type_id) { + self.components + .get(&bit) + .and_then(|boxed| boxed.as_any().downcast_ref::()) + } else { + None + } + } + + fn serialize(&self) -> Vec { + // Serialize the entity's bitmask into 6 bytes (lowest 48 bits). + let mut bytes = self.bitmask.to_le_bytes().to_vec(); + bytes.truncate(6); + bytes + } + + fn deserialize(data: &[u8; 6]) -> Self { + let mut full = [0u8; 8]; + full[..6].copy_from_slice(data); + let bitmask = u64::from_le_bytes(full); + // When deserializing, we recreate an entity with the restored bitmask. + // Note: The individual component data are not restored here. + Self { + id: 0, + bitmask, + components: HashMap::new(), + } + } +} + +pub struct ECS { + next_entity_id: usize, + pub entities: HashMap, +} + +impl ECS { + pub fn new() -> Self { + ECS { + next_entity_id: 0, + entities: HashMap::new(), + } + } + + pub fn create_entity(&mut self) -> &mut EntityImpl { + let entity = EntityImpl::new(self.next_entity_id); + self.entities.insert(self.next_entity_id, entity); + self.next_entity_id += 1; + self.entities.get_mut(&(self.next_entity_id - 1)).unwrap() + } + + pub fn update(&mut self, delta_time: f32) { + for entity in self.entities.values_mut() { + // Update each component attached to the entity. + for comp in entity.components.values_mut() { + comp.update_obj(delta_time); + } + } + } + + pub fn serialize(&self) -> Vec { + let mut data = Vec::new(); + // For each entity, store its id (8 bytes) and its 6-byte bitmask. + for (id, entity) in &self.entities { + data.extend_from_slice(&id.to_le_bytes()); + data.extend_from_slice(&entity.serialize()); + } + data + } + + pub fn deserialize(&mut self, data: &[u8]) { + self.entities.clear(); + // Each serialized entity uses 8 (id) + 6 (bitmask) = 14 bytes. + let entity_size = 14; + let count = data.len() / entity_size; + for i in 0..count { + let offset = i * entity_size; + let mut id_bytes = [0u8; 8]; + id_bytes.copy_from_slice(&data[offset..offset + 8]); + let id = usize::from_le_bytes(id_bytes); + + let mut mask_bytes = [0u8; 6]; + mask_bytes.copy_from_slice(&data[offset + 8..offset + 14]); + let entity = EntityImpl::deserialize(&mask_bytes); + self.entities.insert(id, entity); + } + self.next_entity_id = count; + } +} diff --git a/engine/src/core/panic.rs b/engine/src/core/panic.rs index 08e5124..b76d9a9 100644 --- a/engine/src/core/panic.rs +++ b/engine/src/core/panic.rs @@ -49,33 +49,39 @@ fn process_panic(info: &std::panic::PanicHookInfo<'_>) -> Result<(), Box String { const HEX_WIDTH: usize = mem::size_of::() * 2 + 2; const NEXT_SYMBOL_PADDING: usize = HEX_WIDTH + 6; diff --git a/engine/src/core/render/ctx.rs b/engine/src/core/render/ctx.rs index 3df1bfc..2190e21 100644 --- a/engine/src/core/render/ctx.rs +++ b/engine/src/core/render/ctx.rs @@ -5,6 +5,7 @@ use std::time::Instant; use cgmath::{Deg, Matrix4, Point3, Rad, SquareMatrix, Vector3, perspective}; use futures::executor::block_on; +use thiserror::Error; use tracing::{debug, error, info, trace}; use wgpu::TextureUsages; use wgpu::{Backends, InstanceDescriptor, util::DeviceExt}; @@ -223,7 +224,7 @@ struct FontState { impl<'window> Renderer<'window> { pub async fn new(window: Arc) -> Result { let instance = wgpu::Instance::new(&InstanceDescriptor { - backends: Backends::from_comma_list("dx12,metal,opengl,webgpu"), + backends: Backends::from_comma_list("dx12,metal,opengl,webgpu,vulkan"), ..Default::default() }); @@ -344,22 +345,31 @@ impl<'window> Renderer<'window> { let camera = Camera::new(&device, &camera_bind_group_layout, width, height); let surface_caps = surface.get_capabilities(&adapter); + let present_mode = [ + wgpu::PresentMode::Immediate, + wgpu::PresentMode::Mailbox, + wgpu::PresentMode::AutoNoVsync, + ] + .iter() + .copied() + .find(|mode| surface_caps.present_modes.contains(mode)) + .unwrap_or(wgpu::PresentMode::Fifo); + + debug!("Using {:#?} present mode.", present_mode); + let surface_config = wgpu::SurfaceConfiguration { width, height, format: surface_caps.formats[0], - present_mode: wgpu::PresentMode::AutoNoVsync, + present_mode, alpha_mode: wgpu::CompositeAlphaMode::Auto, view_formats: vec![], usage: TextureUsages::RENDER_ATTACHMENT, desired_maximum_frame_latency: 3, }; surface.configure(&device, &surface_config); - let (depth_texture, depth_texture_view) = create_depth_texture( - &device, - surface_config.width, - surface_config.height, - ); + let (depth_texture, depth_texture_view) = + create_depth_texture(&device, surface_config.width, surface_config.height); let font_bytes = include_bytes!("DejaVuSans.ttf"); let font = FontRef::try_from_slice(font_bytes).map_err(|e| { @@ -605,7 +615,6 @@ fn create_depth_texture( device: &wgpu::Device, width: u32, height: u32, - // format: wgpu::TextureFormat, ) -> (wgpu::Texture, wgpu::TextureView) { let size = wgpu::Extent3d { width, diff --git a/engine/src/core/render/mod.rs b/engine/src/core/render/mod.rs index 900486c..d841521 100644 --- a/engine/src/core/render/mod.rs +++ b/engine/src/core/render/mod.rs @@ -7,12 +7,15 @@ use winit::dpi::Size; use std::env; use std::fs; use std::path::PathBuf; +use tobj::Mesh; use tobj::{LoadOptions, Model}; use tracing::{debug, error, info, trace, warn}; use wgpu::rwh::HasWindowHandle; use winit::application::ApplicationHandler; use winit::event::{KeyEvent, WindowEvent}; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; +use winit::monitor::MonitorHandle; +use winit::window::Fullscreen; use winit::window::Window; use winit::window::WindowId; @@ -44,16 +47,16 @@ pub struct App<'window> { static CUBE_OBJ: &str = " # Blender 4.2.3 LTS # www.blender.org -mtllib cube.mtl +mtllib untitled.mtl o Cube -v 1.000000 1.000000 -1.000000 -v 1.000000 -1.000000 -1.000000 -v 1.000000 1.000000 1.000000 -v 1.000000 -1.000000 1.000000 -v -1.000000 1.000000 -1.000000 -v -1.000000 -1.000000 -1.000000 -v -1.000000 1.000000 1.000000 -v -1.000000 -1.000000 1.000000 +v 0.645975 0.645975 -0.645975 +v 0.645975 -0.645975 -0.645975 +v 0.645975 0.645975 0.645975 +v 0.645975 -0.645975 0.645975 +v -0.645975 0.645975 -0.645975 +v -0.645975 -0.645975 -0.645975 +v -0.645975 0.645975 0.645975 +v -0.645975 -0.645975 0.645975 vn -0.0000 1.0000 -0.0000 vn -0.0000 -0.0000 1.0000 vn -1.0000 -0.0000 -0.0000 @@ -82,6 +85,7 @@ f 8/8/3 7/9/3 5/10/3 6/11/3 f 6/12/4 2/13/4 4/5/4 8/14/4 f 2/13/5 1/1/5 3/4/5 4/5/5 f 6/11/6 5/10/6 1/1/6 2/13/6 + "; impl App<'_> { @@ -103,10 +107,22 @@ impl App<'_> { ) { Ok(obj) => obj, Err(e) => { - error!("{e}"); - panic!() + error!("Failed to load Pumpkin.obj: {e}"); + // Fallback to CUBE_OBJ + let fallback_obj = CUBE_OBJ.to_string(); + tobj::load_obj_buf( + &mut fallback_obj.as_bytes(), + &LoadOptions { + triangulate: true, + single_index: true, + ..Default::default() + }, + |_| Ok(Default::default()), + ) + .expect("Failed to load fallback CUBE_OBJ") } }; + let (combined_vertices, combined_indices) = parse_obj(&obj.0); wgpu_ctx.add_model(&combined_vertices, &combined_indices); @@ -121,10 +137,10 @@ impl App<'_> { ); info!("Main window created: {:?}", window_id); } - Err(e) => error!("Failed to create WGPU context: {:#}", e), + Err(e) => error!("Failed to create WGPU context: {:}", e), } } - Err(e) => error!("Failed to create main window: {:#}", e), + Err(e) => error!("Failed to create main window: {}", e), } } @@ -153,6 +169,7 @@ impl App<'_> { winit::keyboard::KeyCode::Escape => { self.spawn_child_window(event_loop); } + winit::keyboard::KeyCode::F11 => self.toggle_fullscreen(window_id), other => error!("Unimplemented keycode: {:?}", other), }, _ => debug!("Received a keyboard event with no physical key"), @@ -182,6 +199,23 @@ impl App<'_> { warn!("No window context for toggling background: {:?}", window_id); } } + fn toggle_fullscreen(&mut self, window_id: WindowId) { + if let Some(ctx) = self.windows.get_mut(&window_id) { + let is_fullscreen = ctx.window.fullscreen().is_some(); + let fullscreen_mode = if is_fullscreen { + None + } else { + ctx.window + .current_monitor() + .map(|monitor| Fullscreen::Borderless(Some(monitor))) + }; + + ctx.window.set_fullscreen(fullscreen_mode); + debug!("Fullscreen toggled for window: {:?}", window_id); + } else { + warn!("No window found for fullscreen toggle: {:?}", window_id); + } + } fn spawn_child_window(&mut self, event_loop: &ActiveEventLoop) { if let Some(main_ctx) = self.windows.values().find(|ctx| ctx.is_main_window()) { @@ -213,7 +247,7 @@ impl App<'_> { let mut tmp_path: PathBuf = env::temp_dir(); tmp_path.push("cube.obj"); if let Err(e) = fs::write(&tmp_path, CUBE_OBJ) { - error!("Failed to write cube OBJ to temp: {:?}", e); + error!("Failed to write cube OBJ to temp: {}", e); } let load_options = tobj::LoadOptions { @@ -228,7 +262,7 @@ impl App<'_> { wgpu_ctx.add_model(&cube_vertices, &cube_indices); } Err(e) => { - error!("Failed to load cube OBJ from temp file: {:?}", e) + error!("Failed to load cube OBJ from temp file: {:#}", e) } } } @@ -242,10 +276,10 @@ impl App<'_> { ); debug!("Spawned new child window: {:?}", window_id); } - Err(e) => error!("Failed to create WGPU context for child window: {:?}", e), + Err(e) => error!("Failed to create WGPU context for child window: {}", e), } } - Err(e) => error!("Failed to create child window: {:?}", e), + Err(e) => error!("Failed to create child window: {}", e), } } else { error!("No main window found. Cannot spawn a child window."); diff --git a/engine/src/main.rs b/engine/src/main.rs index 741b460..00ad1d7 100644 --- a/engine/src/main.rs +++ b/engine/src/main.rs @@ -46,13 +46,29 @@ async fn main() { info!("Type 'help' for a list of commands."); let repl_thread = std::thread::spawn(|| { - let rt = runtime::Builder::new_current_thread() + let rt = match runtime::Builder::new_current_thread() .enable_all() - .build() - .unwrap(); + .build() { + Ok(rt) => rt, + Err(e) => { + error!("A fatal error has occured: {e}"); + std::process::exit(1) + }, + }; rt.block_on(core::repl::input::handle_repl()) }); - core::render::init_renderer(event_loop); + splash::print_splash(); + info!("Type 'help' for a list of commands."); + + match EventLoop::new() { + Ok(event_loop) => { + core::render::init_renderer(event_loop); + } + Err(e) => { + error!("{e}") + } + }; + if let Err(_) = repl_thread.join() { error!("REPL thread panicked"); } diff --git a/flake.nix b/flake.nix index b51f286..99f0a07 100644 --- a/flake.nix +++ b/flake.nix @@ -48,6 +48,12 @@ packages = { inherit (pkgs) zenyx; default = pkgs.zenyx; + windows = let + pkgsCross = import nixpkgs { + system = "x86_64-linux"; + crossSystem = nixpkgs.lib.systems.examples.mingwW64; + }; + in pkgsCross.callPackage ./default.nix {}; }; devShells.default = pkgs.mkShell {