Merge branch 'main' of codeberg.org:Caznix/Zenyx

This commit is contained in:
Chance 2025-04-03 01:02:53 -04:00 committed by BitSyndicate
commit 39a3f88d3c
Signed by untrusted user: bitsyndicate
GPG key ID: 443E4198D6BBA6DE
7 changed files with 347 additions and 80 deletions

View file

@ -1,5 +1,4 @@
{ { lib,
lib,
rustPlatform, rustPlatform,
nix-gitignore, nix-gitignore,
bash, bash,
@ -11,43 +10,63 @@
pkg-config, pkg-config,
libxkbcommon, libxkbcommon,
pkgs, pkgs,
stdenv,
targetPackages ? pkgs,
}: let }: let
version = (builtins.fromTOML (builtins.readFile ./engine/Cargo.toml)).package.version; version = (builtins.fromTOML (builtins.readFile ./engine/Cargo.toml)).package.version;
src = nix-gitignore.gitignoreSource [] ./.; src = nix-gitignore.gitignoreSource [] ./.;
in in
rustPlatform.buildRustPackage rec { rustPlatform.buildRustPackage rec {
pname = "zenyx"; pname = "zenyx";
inherit src version; inherit src version;
cargoLock.lockFile = ./Cargo.lock; 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}";
fixupPhase = '' nativeBuildInputs = [
wrapProgram $out/bin/${pname} --set PATH ${bash}/bin:\$PATH --set LD_LIBRARY_PATH ${pkgs.lib.makeLibraryPath buildInputs} pkg-config
''; ] ++ lib.optionals stdenv.targetPlatform.isDarwin [
targetPackages.darwin.apple_sdk.frameworks.CoreServices
];
meta = { buildInputs = with targetPackages; [
description = "Test"; dav1d
license = lib.licenses.mit; ] ++ lib.optionals (stdenv.targetPlatform.isLinux || stdenv.targetPlatform.isWindows) [
platforms = lib.platforms.linux; vulkan-loader
mainProgram = "zenyx"; ] ++ 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";
};
}

View file

@ -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<u8>;
fn deserialize(data: &[u8; 6]) -> Self;
}
pub trait Entity: Sized {
fn add_component<C: Component>(&mut self, component: C);
fn remove_component<C: Component>(&mut self);
fn get_component<C: Component>(&self) -> Option<&C>;
fn serialize(&self) -> Vec<u8>;
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<HashMap<TypeId, u64>> = Mutex::new(HashMap::new());
static ref NEXT_COMPONENT_BIT: Mutex<u64> = Mutex::new(1);
}
// To allow dynamic dispatch on components (even though Component itself is not objectsafe)
// we wrap them in an objectsafe trait.
pub trait ComponentObject: Any {
fn update_obj(&mut self, delta_time: f32);
fn serialize_obj(&self) -> Vec<u8>;
fn as_any(&self) -> &dyn Any;
}
impl<T: Component + 'static> ComponentObject for T {
fn update_obj(&mut self, delta_time: f32) {
T::update(self, delta_time)
}
fn serialize_obj(&self) -> Vec<u8> {
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<u64, Box<dyn ComponentObject>>,
}
impl EntityImpl {
pub fn new(id: usize) -> Self {
EntityImpl {
id,
bitmask: 0,
components: HashMap::new(),
}
}
}
impl Entity for EntityImpl {
fn add_component<C: Component>(&mut self, component: C) {
let type_id = TypeId::of::<C>();
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<C: Component>(&mut self) {
let type_id = TypeId::of::<C>();
if let Some(&bit) = COMPONENT_REGISTRY.lock().unwrap().get(&type_id) {
self.bitmask &= !bit;
}
}
fn get_component<C: Component>(&self) -> Option<&C> {
let type_id = TypeId::of::<C>();
if let Some(&bit) = COMPONENT_REGISTRY.lock().unwrap().get(&type_id) {
self.components
.get(&bit)
.and_then(|boxed| boxed.as_any().downcast_ref::<C>())
} else {
None
}
}
fn serialize(&self) -> Vec<u8> {
// 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<usize, EntityImpl>,
}
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<u8> {
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;
}
}

View file

@ -49,33 +49,39 @@ fn process_panic(info: &std::panic::PanicHookInfo<'_>) -> Result<(), Box<dyn Err
writeln!(file, "{}", render_backtrace().sanitize_path())?; writeln!(file, "{}", render_backtrace().sanitize_path())?;
let panic_msg = format!( let panic_msg = format!(
"Zenyx had a problem and crashed. To help us diagnose the problem you can send us a crash report. r#"Zenyx had a problem and crashed. To help us diagnose the problem you can send us a crash report.
We have generated a report file at '{}'. Submit an issue or email with the subject of 'Zenyx Crash Report' and include the report as an attachment. We have generated a report file at '{}'. Submit an issue or email with the subject of 'Zenyx Crash Report' and include the report as an attachment.
To submit the crash report: To submit the crash report:
https://github.com/Zenyx-Engine/Zenyx/issues https://github.com/Zenyx-Engine/Zenyx/issues
We take privacy seriously, and do not perform any automated error collection. In order to improve the software, we rely on people to submit reports. We take privacy seriously, and do not perform any automated error collection. In order to improve the software, we rely on people to submit reports.
Thank you kindly!", log_path.display()); Thank you kindly!"#,
println!("{}", panic_msg.red().bold()); log_path.display()
println!(
"\nFor future reference, the error summary is as follows:\n{}",
payload_str.red().bold()
); );
if let Err(e ) = MessageDialog::new()
.set_type(MessageType::Error)
.set_title(&format!("{}",payload_str))
// wont display properly if not debug formatted??
.set_text(&format!("{:#?}", panic_msg))
.show_alert() {
error!("{e}");
}
Ok(())
}
let final_msg = format!(
r#"{}
For future reference, the error summary is as follows:
{}"#,
panic_msg, payload_str
);
println!("{}", final_msg.red().bold());
MessageDialog::new()
.set_type(MessageType::Error)
.set_title("A fatal error in Zenyx has occurred")
.set_text(&final_msg)
.show_alert()
.map_err(|e| {
error!("Failed to show error dialog: {}", e);
e.into()
})
}
fn render_backtrace() -> String { fn render_backtrace() -> String {
const HEX_WIDTH: usize = mem::size_of::<usize>() * 2 + 2; const HEX_WIDTH: usize = mem::size_of::<usize>() * 2 + 2;
const NEXT_SYMBOL_PADDING: usize = HEX_WIDTH + 6; const NEXT_SYMBOL_PADDING: usize = HEX_WIDTH + 6;

View file

@ -5,6 +5,7 @@ use std::time::Instant;
use cgmath::{Deg, Matrix4, Point3, Rad, SquareMatrix, Vector3, perspective}; use cgmath::{Deg, Matrix4, Point3, Rad, SquareMatrix, Vector3, perspective};
use futures::executor::block_on; use futures::executor::block_on;
use thiserror::Error;
use tracing::{debug, error, info, trace}; use tracing::{debug, error, info, trace};
use wgpu::TextureUsages; use wgpu::TextureUsages;
use wgpu::{Backends, InstanceDescriptor, util::DeviceExt}; use wgpu::{Backends, InstanceDescriptor, util::DeviceExt};
@ -223,7 +224,7 @@ struct FontState {
impl<'window> Renderer<'window> { impl<'window> Renderer<'window> {
pub async fn new(window: Arc<Window>) -> Result<Self> { pub async fn new(window: Arc<Window>) -> Result<Self> {
let instance = wgpu::Instance::new(&InstanceDescriptor { 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() ..Default::default()
}); });
@ -344,22 +345,31 @@ impl<'window> Renderer<'window> {
let camera = Camera::new(&device, &camera_bind_group_layout, width, height); let camera = Camera::new(&device, &camera_bind_group_layout, width, height);
let surface_caps = surface.get_capabilities(&adapter); 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 { let surface_config = wgpu::SurfaceConfiguration {
width, width,
height, height,
format: surface_caps.formats[0], format: surface_caps.formats[0],
present_mode: wgpu::PresentMode::AutoNoVsync, present_mode,
alpha_mode: wgpu::CompositeAlphaMode::Auto, alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![], view_formats: vec![],
usage: TextureUsages::RENDER_ATTACHMENT, usage: TextureUsages::RENDER_ATTACHMENT,
desired_maximum_frame_latency: 3, desired_maximum_frame_latency: 3,
}; };
surface.configure(&device, &surface_config); surface.configure(&device, &surface_config);
let (depth_texture, depth_texture_view) = create_depth_texture( let (depth_texture, depth_texture_view) =
&device, create_depth_texture(&device, surface_config.width, surface_config.height);
surface_config.width,
surface_config.height,
);
let font_bytes = include_bytes!("DejaVuSans.ttf"); let font_bytes = include_bytes!("DejaVuSans.ttf");
let font = FontRef::try_from_slice(font_bytes).map_err(|e| { let font = FontRef::try_from_slice(font_bytes).map_err(|e| {
@ -605,7 +615,6 @@ fn create_depth_texture(
device: &wgpu::Device, device: &wgpu::Device,
width: u32, width: u32,
height: u32, height: u32,
// format: wgpu::TextureFormat,
) -> (wgpu::Texture, wgpu::TextureView) { ) -> (wgpu::Texture, wgpu::TextureView) {
let size = wgpu::Extent3d { let size = wgpu::Extent3d {
width, width,

View file

@ -7,12 +7,15 @@ use winit::dpi::Size;
use std::env; use std::env;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use tobj::Mesh;
use tobj::{LoadOptions, Model}; use tobj::{LoadOptions, Model};
use tracing::{debug, error, info, trace, warn}; use tracing::{debug, error, info, trace, warn};
use wgpu::rwh::HasWindowHandle; use wgpu::rwh::HasWindowHandle;
use winit::application::ApplicationHandler; use winit::application::ApplicationHandler;
use winit::event::{KeyEvent, WindowEvent}; use winit::event::{KeyEvent, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::monitor::MonitorHandle;
use winit::window::Fullscreen;
use winit::window::Window; use winit::window::Window;
use winit::window::WindowId; use winit::window::WindowId;
@ -44,16 +47,16 @@ pub struct App<'window> {
static CUBE_OBJ: &str = " static CUBE_OBJ: &str = "
# Blender 4.2.3 LTS # Blender 4.2.3 LTS
# www.blender.org # www.blender.org
mtllib cube.mtl mtllib untitled.mtl
o Cube o Cube
v 1.000000 1.000000 -1.000000 v 0.645975 0.645975 -0.645975
v 1.000000 -1.000000 -1.000000 v 0.645975 -0.645975 -0.645975
v 1.000000 1.000000 1.000000 v 0.645975 0.645975 0.645975
v 1.000000 -1.000000 1.000000 v 0.645975 -0.645975 0.645975
v -1.000000 1.000000 -1.000000 v -0.645975 0.645975 -0.645975
v -1.000000 -1.000000 -1.000000 v -0.645975 -0.645975 -0.645975
v -1.000000 1.000000 1.000000 v -0.645975 0.645975 0.645975
v -1.000000 -1.000000 1.000000 v -0.645975 -0.645975 0.645975
vn -0.0000 1.0000 -0.0000 vn -0.0000 1.0000 -0.0000
vn -0.0000 -0.0000 1.0000 vn -0.0000 -0.0000 1.0000
vn -1.0000 -0.0000 -0.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 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 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 f 6/11/6 5/10/6 1/1/6 2/13/6
"; ";
impl App<'_> { impl App<'_> {
@ -103,10 +107,22 @@ impl App<'_> {
) { ) {
Ok(obj) => obj, Ok(obj) => obj,
Err(e) => { Err(e) => {
error!("{e}"); error!("Failed to load Pumpkin.obj: {e}");
panic!() // 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); let (combined_vertices, combined_indices) = parse_obj(&obj.0);
wgpu_ctx.add_model(&combined_vertices, &combined_indices); wgpu_ctx.add_model(&combined_vertices, &combined_indices);
@ -121,10 +137,17 @@ impl App<'_> {
); );
info!("Main window created: {:?}", window_id); info!("Main window created: {:?}", window_id);
} }
<<<<<<< HEAD
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),
=======
Err(e) => error!("Failed to create WGPU context: {:}", e),
}
}
Err(e) => error!("Failed to create main window: {}", e),
>>>>>>> refs/rewritten/main-6
} }
} }
@ -153,6 +176,7 @@ impl App<'_> {
winit::keyboard::KeyCode::Escape => { winit::keyboard::KeyCode::Escape => {
self.spawn_child_window(event_loop); self.spawn_child_window(event_loop);
} }
winit::keyboard::KeyCode::F11 => self.toggle_fullscreen(window_id),
other => error!("Unimplemented keycode: {:?}", other), other => error!("Unimplemented keycode: {:?}", other),
}, },
_ => debug!("Received a keyboard event with no physical key"), _ => debug!("Received a keyboard event with no physical key"),
@ -182,6 +206,23 @@ impl App<'_> {
warn!("No window context for toggling background: {:?}", window_id); 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) { fn spawn_child_window(&mut self, event_loop: &ActiveEventLoop) {
if let Some(main_ctx) = self.windows.values().find(|ctx| ctx.is_main_window()) { if let Some(main_ctx) = self.windows.values().find(|ctx| ctx.is_main_window()) {
@ -213,7 +254,7 @@ impl App<'_> {
let mut tmp_path: PathBuf = env::temp_dir(); let mut tmp_path: PathBuf = env::temp_dir();
tmp_path.push("cube.obj"); tmp_path.push("cube.obj");
if let Err(e) = fs::write(&tmp_path, 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 { let load_options = tobj::LoadOptions {
@ -228,7 +269,7 @@ impl App<'_> {
wgpu_ctx.add_model(&cube_vertices, &cube_indices); wgpu_ctx.add_model(&cube_vertices, &cube_indices);
} }
Err(e) => { Err(e) => {
error!("Failed to load cube OBJ from temp file: {:?}", e) error!("Failed to load cube OBJ from temp file: {:#}", e)
} }
} }
} }
@ -242,10 +283,10 @@ impl App<'_> {
); );
debug!("Spawned new child window: {:?}", window_id); 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 { } else {
error!("No main window found. Cannot spawn a child window."); error!("No main window found. Cannot spawn a child window.");

View file

@ -46,13 +46,29 @@ async fn main() {
info!("Type 'help' for a list of commands."); info!("Type 'help' for a list of commands.");
let repl_thread = std::thread::spawn(|| { let repl_thread = std::thread::spawn(|| {
let rt = runtime::Builder::new_current_thread() let rt = match runtime::Builder::new_current_thread()
.enable_all() .enable_all()
.build() .build() {
.unwrap(); Ok(rt) => rt,
Err(e) => {
error!("A fatal error has occured: {e}");
std::process::exit(1)
},
};
rt.block_on(core::repl::input::handle_repl()) 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() { if let Err(_) = repl_thread.join() {
error!("REPL thread panicked"); error!("REPL thread panicked");
} }

View file

@ -48,6 +48,12 @@
packages = { packages = {
inherit (pkgs) zenyx; inherit (pkgs) zenyx;
default = 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 { devShells.default = pkgs.mkShell {