diff --git a/.cargo/config.toml b/.cargo/config.toml index ae0e944..3926aa5 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1 +1,4 @@ [alias] + +[build] +rustflags = ["-Ctarget-cpu=native"] diff --git a/Cargo.lock b/Cargo.lock index a86f202..1531833 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,7 +43,7 @@ dependencies = [ "getrandom", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -112,6 +112,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -315,7 +324,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" dependencies = [ - "approx", + "approx 0.4.0", "num-traits", ] @@ -443,6 +452,34 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -523,6 +560,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "endian-type" version = "0.1.2" @@ -752,6 +795,44 @@ dependencies = [ "gl_generator", ] +[[package]] +name = "glyph_brush" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0060f4ed4ef64a5876d9836d7d6c9ed43a463f3ca431682bec1c326064c8c93e" +dependencies = [ + "glyph_brush_draw_cache", + "glyph_brush_layout", + "ordered-float 5.0.0", + "rustc-hash 2.1.1", + "twox-hash", +] + +[[package]] +name = "glyph_brush_draw_cache" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6c910def52365fef3f439a6b50a4d5c11b28eec4cf6c191f6dfea18e88d7f" +dependencies = [ + "ab_glyph", + "crossbeam-channel", + "crossbeam-deque", + "linked-hash-map", + "rayon", + "rustc-hash 2.1.1", +] + +[[package]] +name = "glyph_brush_layout" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1e288bfd2f6c0313f78bf5aa538356ad481a3bb97e9b7f93220ab0066c5992" +dependencies = [ + "ab_glyph", + "approx 0.5.1", + "xi-unicode", +] + [[package]] name = "gpu-alloc" version = "0.6.0" @@ -1111,6 +1192,12 @@ dependencies = [ "redox_syscall 0.5.10", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1213,7 +1300,7 @@ dependencies = [ "hexf-parse", "indexmap", "log", - "rustc-hash", + "rustc-hash 1.1.0", "spirv", "strum", "termcolor", @@ -1566,6 +1653,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-float" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2c1f9f56e534ac6a9b8a4600bdf0f530fb393b5f393e7b4d03489c3cf0c3f01" +dependencies = [ + "num-traits", +] + [[package]] name = "overload" version = "0.1.1" @@ -1669,6 +1765,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.24", +] + [[package]] name = "presser" version = "0.3.1" @@ -1727,6 +1832,36 @@ dependencies = [ "nibble_vec", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "range-alloc" version = "0.1.4" @@ -1739,6 +1874,26 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1815,6 +1970,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "0.38.44" @@ -2188,6 +2349,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tobj" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04aca6092e5978e708ee784e8ab9b5cf3cdb598b28f99a2f257446e7081a7025" +dependencies = [ + "ahash", + "tokio", +] + [[package]] name = "tokio" version = "1.44.1" @@ -2195,6 +2366,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" dependencies = [ "backtrace", + "bytes", "parking_lot", "pin-project-lite", "tokio-macros", @@ -2291,6 +2463,21 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[package]] +name = "twox-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7b17f197b3050ba473acf9181f7b1d3b66d1cf7356c6cc57886662276e65908" +dependencies = [ + "rand", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -2628,7 +2815,7 @@ dependencies = [ "parking_lot", "profiling", "raw-window-handle", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror 2.0.12", "wgpu-hal", @@ -2665,13 +2852,13 @@ dependencies = [ "ndk-sys 0.5.0+25.2.9519653", "objc", "once_cell", - "ordered-float", + "ordered-float 4.6.0", "parking_lot", "profiling", "range-alloc", "raw-window-handle", "renderdoc-sys", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror 2.0.12", "wasm-bindgen", @@ -2693,6 +2880,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wgpu_text" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd192487eb81eb51f8f0eb82fea0865e71ab4f002f7942bee0bba04fc2a0b8c" +dependencies = [ + "bytemuck", + "glyph_brush", + "log", + "wgpu", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3119,6 +3318,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" +[[package]] +name = "xi-unicode" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" + [[package]] name = "xkbcommon-dl" version = "0.4.2" @@ -3181,6 +3386,7 @@ dependencies = [ name = "zenyx" version = "0.1.0" dependencies = [ + "ahash", "anyhow", "backtrace", "bytemuck", @@ -3196,10 +3402,13 @@ dependencies = [ "regex", "rustyline", "thiserror 2.0.12", + "tobj", "tokio", "tracing", "tracing-subscriber", + "typenum", "wgpu", + "wgpu_text", "winit", ] @@ -3209,7 +3418,16 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive 0.8.24", ] [[package]] @@ -3223,6 +3441,17 @@ dependencies = [ "syn", ] +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index cb0f4ce..4bd4bc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,41 +2,16 @@ resolver = "2" members = ["engine","subcrates/zen_core"] -[profile.dev] - -rpath = false -panic = "abort" -lto = "off" -opt-level = 0 -debug = false -overflow-checks = false -incremental = true -codegen-units = 512 - - -strip = "symbols" -debug-assertions = true - -[profile.dev.package."*"] -opt-level = 0 -debug = false -overflow-checks = false -incremental = true -codegen-units = 512 - - -strip = "symbols" -debug-assertions = true -[profile.dev.build-override] -opt-level = 0 -debug = false -overflow-checks = false -incremental = true -codegen-units = 512 [workspace.dependencies] lazy_static = "1.5.0" parking_lot = "0.12.3" [profile.release] -debug-assertions = false +lto = true +codegen-units = 1 +panic = "abort" +split-debuginfo = "off" + +[profile.dev] +debug = 0 diff --git a/default.nix b/default.nix index 0f91f93..4b49658 100644 --- a/default.nix +++ b/default.nix @@ -40,7 +40,6 @@ in doCheck = false; LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath buildInputs}"; - fixupPhase = '' wrapProgram $out/bin/${pname} --set PATH ${bash}/bin:\$PATH --set LD_LIBRARY_PATH ${pkgs.lib.makeLibraryPath buildInputs} ''; diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 32e799f..62aaf9d 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -3,32 +3,42 @@ name = "zenyx" version = "0.1.0" edition = "2024" repository = "https://github.com/Zenyx-Engine/Zenyx" + [dependencies] +# TBR anyhow = "1.0.94" +# TBR (if possible) backtrace = "0.3.74" +# TBR (if possible) chrono = "0.4.39" colored = "3.0.0" +# TBR (if possible) crashreport = "1.0.1" +# TBR dirs-next = "2.0.0" - +# TBR == (To be removed) +# TBR lazy_static.workspace = true +# TBR once_cell = "1.21.1" parking_lot.workspace = true +# TBR (if possible) regex = "1.11.1" +# TBR (if possible) rustyline = { version = "15.0.0", features = ["derive", "rustyline-derive"] } thiserror = "2.0.11" -tokio = { version = "1.44.1", features = ["macros", "parking_lot","rt-multi-thread"] } +# Tokio is heavy but so far its the best option, we should make better use of it or switch to another runtime. +tokio = { version = "1.44.1", features = ["fs", "macros", "parking_lot", "rt-multi-thread"] } wgpu = "24.0.3" winit = "0.30.9" bytemuck = "1.21.0" +# TBR (if possible) futures = "0.3.31" cgmath = "0.18.0" tracing = "0.1.41" tracing-subscriber = "0.3.19" - - -[profile.dev] -debug-assertions = true - -[profile.release] -debug-assertions = false +# TBR +typenum = { version = "1.18.0", features = ["const-generics"] } +tobj = { version = "4.0.3", features = ["tokio"] } +ahash = "0.8.11" +wgpu_text = "0.9.2" diff --git a/engine/src/core/panic.rs b/engine/src/core/panic.rs index 3091ff4..db207f6 100644 --- a/engine/src/core/panic.rs +++ b/engine/src/core/panic.rs @@ -1,3 +1,4 @@ +use std::error::Error; use std::fmt::Write as FmtWrite; use std::mem; @@ -8,50 +9,43 @@ use regex::Regex; static INIT: parking_lot::Once = Once::new(); pub fn set_panic_hook() { - use std::io::Write; - - use colored::Colorize; - - use crate::workspace; - INIT.call_once(|| { let default_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { - let log_path = workspace::get_working_dir().unwrap_or_else(|_| { + if let Err(e) = process_panic(info) { + eprintln!("Error in panic hook: {}", e); default_hook(info); - std::process::exit(0); - }); - if !log_path.exists() { - std::fs::create_dir_all(&log_path).unwrap_or_else(|_| { - default_hook(info); - std::process::exit(0); - }); } - let log_path = log_path.join("panic.log"); + std::process::exit(0); + })); + }); +} - let mut file = std::fs::File::create(&log_path).unwrap_or_else(|_| { - default_hook(info); - std::process::exit(0); - }); - let payload = info.payload(); - let payload_str = if let Some(s) = payload.downcast_ref::<&str>() { - *s - } else if let Some(s) = payload.downcast_ref::() { - s - } else { - "" - }; +fn process_panic(info: &std::panic::PanicHookInfo<'_>) -> Result<(), Box> { + use crate::workspace; + use colored::Colorize; + use std::io::Write; - writeln!(file, "{}", payload_str).unwrap_or_else(|_| { - default_hook(info); - std::process::exit(0); - }); - writeln!(file, "{}", render_backtrace().sanitize_path()).unwrap_or_else(|_| { - default_hook(info); - std::process::exit(0); - }); + let log_dir = workspace::get_working_dir()?; + if !log_dir.exists() { + std::fs::create_dir_all(&log_dir)?; + } + let log_path = log_dir.join("panic.log"); - let panic_msg = format!( + let mut file = std::fs::File::create(&log_path)?; + let payload = info.payload(); + let payload_str = if let Some(s) = payload.downcast_ref::<&str>() { + *s + } else if let Some(s) = payload.downcast_ref::() { + s + } else { + "" + }; + + writeln!(file, "{}", payload_str)?; + writeln!(file, "{}", render_backtrace().sanitize_path())?; + + let panic_msg = format!( "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. @@ -63,11 +57,12 @@ 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. Thank you kindly!", log_path.display()); - println!("{}", panic_msg.red().bold()); - println!("\nFor future reference, the error summary is as follows:\n{}", payload_str.red().bold()); - std::process::exit(0); - })); - }); + println!("{}", panic_msg.red().bold()); + println!( + "\nFor future reference, the error summary is as follows:\n{}", + payload_str.red().bold() + ); + Ok(()) } fn render_backtrace() -> String { @@ -130,7 +125,7 @@ trait Sanitize { impl Sanitize for str { fn sanitize_path(&self) -> String { let username_pattern = r"(?i)(/home/|/Users/|\\Users\\)([^/\\]+)"; - let re = Regex::new(username_pattern).expect("Failed to sanitize path, aborting operation"); + let re = Regex::new(username_pattern).expect("Failed to compile regex for sanitization"); re.replace_all(self, "${1}").to_string() } } diff --git a/engine/src/core/render/DejaVuSans.ttf b/engine/src/core/render/DejaVuSans.ttf new file mode 100644 index 0000000..5267218 Binary files /dev/null and b/engine/src/core/render/DejaVuSans.ttf differ diff --git a/engine/src/core/render/ctx.rs b/engine/src/core/render/ctx.rs index addd7dc..7024e0f 100644 --- a/engine/src/core/render/ctx.rs +++ b/engine/src/core/render/ctx.rs @@ -1,58 +1,144 @@ -use std::borrow::Cow; +use std::borrow::Cow; +use std::mem::offset_of; use std::sync::Arc; use std::time::Instant; -use cgmath::{Matrix4, Point3, Rad, Vector3, perspective}; +use cgmath::{Deg, Matrix4, Point3, Rad, SquareMatrix, Vector3, perspective}; use futures::executor::block_on; use thiserror::Error; +use tracing::{error, info, trace}; +use wgpu::TextureUsages; use wgpu::{Backends, InstanceDescriptor, util::DeviceExt}; +use wgpu_text::glyph_brush::ab_glyph::FontRef; +use wgpu_text::glyph_brush::{HorizontalAlign, Layout, OwnedSection, OwnedText, VerticalAlign}; +use wgpu_text::{BrushBuilder, TextBrush}; use winit::window::Window; #[derive(Debug, Error)] -pub enum ContextError { - #[error("Failed to create WGPU surface: {0}")] - SurfaceCreationFailure(#[from] wgpu::CreateSurfaceError), +#[error(transparent)] +pub enum ContextErrorKind { + #[error("Surface creation failed")] + SurfaceCreation, + #[error("Surface configuration failed")] + SurfaceConfiguration, + #[error("Adapter request failed")] + AdapterRequest, + #[error("Device request failed")] + DeviceRequest, + #[error("Surface texture acquisition failed")] + SurfaceTexture, } -const CUBE_SHADER: &str = r#" -struct Uniforms { - mvp: mat4x4, -}; - -@group(0) @binding(0) -var u: Uniforms; - -struct VertexInput { - @location(0) position: vec3, - @location(1) normal: vec3, -}; - -struct VertexOutput { - @builtin(position) clip_position: vec4, - @location(0) normal: vec3, -}; - -@vertex -fn vs_main(input: VertexInput) -> VertexOutput { - var output: VertexOutput; - output.clip_position = u.mvp * vec4(input.position, 1.0); - output.normal = input.normal; - return output; +#[derive(Debug, Error)] +pub struct RenderContextError { + kind: ContextErrorKind, + label: Option>, + #[source] + source: Option>, } -@fragment -fn fs_main(input: VertexOutput) -> @location(0) vec4 { - let light_dir = normalize(vec3(0.5, 1.0, 0.5)); - let brightness = clamp(dot(normalize(input.normal), light_dir), 0.0, 1.0); - return vec4(0.7 * brightness, 0.7 * brightness, 0.9 * brightness, 1.0); +impl RenderContextError { + pub fn new( + kind: ContextErrorKind, + label: impl Into>>, + source: impl Into>>, + ) -> Self { + Self { + kind, + label: label.into(), + source: source.into(), + } + } + + pub fn with_label(mut self, label: impl Into>) -> Self { + self.label = Some(label.into()); + self + } } -"#; + +impl std::fmt::Display for RenderContextError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(label) = &self.label { + writeln!(f, "[{}] {}", label, self.kind)?; + } else { + writeln!(f, "{}", self.kind)?; + } + + if let Some(source) = &self.source { + fn fmt_chain( + err: &(dyn std::error::Error + 'static), + indent: usize, + f: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + let indent_str = " ".repeat(indent); + writeln!(f, "{}{}", indent_str, err)?; + if let Some(next) = err.source() { + writeln!(f, "{}Caused by:", indent_str)?; + fmt_chain(next, indent + 1, f)?; + } + Ok(()) + } + writeln!(f, "Caused by:")?; + fmt_chain(source.as_ref(), 1, f)?; + } + Ok(()) + } +} + +trait IntoRenderContextError { + fn ctx_err( + self, + kind: ContextErrorKind, + label: impl Into>, + ) -> Result; +} + +impl IntoRenderContextError for Result +where + E: std::error::Error + Send + Sync + 'static, +{ + fn ctx_err( + self, + kind: ContextErrorKind, + label: impl Into>, + ) -> Result { + self.map_err(|e| { + RenderContextError::new( + kind, + Some(label.into()), + Some(Box::new(e) as Box), + ) + }) + } +} + +impl From for RenderContextError { + fn from(err: wgpu::CreateSurfaceError) -> Self { + RenderContextError::new( + ContextErrorKind::SurfaceCreation, + Some("Surface creation".into()), + Some(Box::new(err) as Box), + ) + } +} + +impl From for RenderContextError { + fn from(err: wgpu::RequestDeviceError) -> Self { + RenderContextError::new( + ContextErrorKind::DeviceRequest, + Some("Device setup".into()), + Some(Box::new(err) as Box), + ) + } +} + +const SHADER_SRC: &str = include_str!("shader.wgsl"); #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct Vertex { - position: [f32; 3], - normal: [f32; 3], +pub struct Vertex { + pub position: [f32; 3], + pub normal: [f32; 3], } impl Vertex { @@ -63,7 +149,7 @@ impl Vertex { format: wgpu::VertexFormat::Float32x3, }, wgpu::VertexAttribute { - offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, + offset: offset_of!(Vertex, normal) as u64, shader_location: 1, format: wgpu::VertexFormat::Float32x3, }, @@ -78,260 +164,288 @@ impl Vertex { } } -static CUBE_VERTICES: &[Vertex] = &[ - Vertex { - position: [-0.5, -0.5, 0.5], - normal: [0.0, 0.0, 1.0], - }, - Vertex { - position: [0.5, -0.5, 0.5], - normal: [0.0, 0.0, 1.0], - }, - Vertex { - position: [0.5, 0.5, 0.5], - normal: [0.0, 0.0, 1.0], - }, - Vertex { - position: [0.5, 0.5, 0.5], - normal: [0.0, 0.0, 1.0], - }, - Vertex { - position: [-0.5, 0.5, 0.5], - normal: [0.0, 0.0, 1.0], - }, - Vertex { - position: [-0.5, -0.5, 0.5], - normal: [0.0, 0.0, 1.0], - }, - Vertex { - position: [0.5, -0.5, -0.5], - normal: [0.0, 0.0, -1.0], - }, - Vertex { - position: [-0.5, -0.5, -0.5], - normal: [0.0, 0.0, -1.0], - }, - Vertex { - position: [-0.5, 0.5, -0.5], - normal: [0.0, 0.0, -1.0], - }, - Vertex { - position: [-0.5, 0.5, -0.5], - normal: [0.0, 0.0, -1.0], - }, - Vertex { - position: [0.5, 0.5, -0.5], - normal: [0.0, 0.0, -1.0], - }, - Vertex { - position: [0.5, -0.5, -0.5], - normal: [0.0, 0.0, -1.0], - }, - Vertex { - position: [0.5, -0.5, 0.5], - normal: [1.0, 0.0, 0.0], - }, - Vertex { - position: [0.5, -0.5, -0.5], - normal: [1.0, 0.0, 0.0], - }, - Vertex { - position: [0.5, 0.5, -0.5], - normal: [1.0, 0.0, 0.0], - }, - Vertex { - position: [0.5, 0.5, -0.5], - normal: [1.0, 0.0, 0.0], - }, - Vertex { - position: [0.5, 0.5, 0.5], - normal: [1.0, 0.0, 0.0], - }, - Vertex { - position: [0.5, -0.5, 0.5], - normal: [1.0, 0.0, 0.0], - }, - Vertex { - position: [-0.5, -0.5, -0.5], - normal: [-1.0, 0.0, 0.0], - }, - Vertex { - position: [-0.5, -0.5, 0.5], - normal: [-1.0, 0.0, 0.0], - }, - Vertex { - position: [-0.5, 0.5, 0.5], - normal: [-1.0, 0.0, 0.0], - }, - Vertex { - position: [-0.5, 0.5, 0.5], - normal: [-1.0, 0.0, 0.0], - }, - Vertex { - position: [-0.5, 0.5, -0.5], - normal: [-1.0, 0.0, 0.0], - }, - Vertex { - position: [-0.5, -0.5, -0.5], - normal: [-1.0, 0.0, 0.0], - }, - Vertex { - position: [-0.5, 0.5, 0.5], - normal: [0.0, 1.0, 0.0], - }, - Vertex { - position: [0.5, 0.5, 0.5], - normal: [0.0, 1.0, 0.0], - }, - Vertex { - position: [0.5, 0.5, -0.5], - normal: [0.0, 1.0, 0.0], - }, - Vertex { - position: [0.5, 0.5, -0.5], - normal: [0.0, 1.0, 0.0], - }, - Vertex { - position: [-0.5, 0.5, -0.5], - normal: [0.0, 1.0, 0.0], - }, - Vertex { - position: [-0.5, 0.5, 0.5], - normal: [0.0, 1.0, 0.0], - }, - Vertex { - position: [-0.5, -0.5, -0.5], - normal: [0.0, -1.0, 0.0], - }, - Vertex { - position: [0.5, -0.5, -0.5], - normal: [0.0, -1.0, 0.0], - }, - Vertex { - position: [0.5, -0.5, 0.5], - normal: [0.0, -1.0, 0.0], - }, - Vertex { - position: [0.5, -0.5, 0.5], - normal: [0.0, -1.0, 0.0], - }, - Vertex { - position: [-0.5, -0.5, 0.5], - normal: [0.0, -1.0, 0.0], - }, - Vertex { - position: [-0.5, -0.5, -0.5], - normal: [0.0, -1.0, 0.0], - }, -]; +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct CameraUniform { + view: [[f32; 4]; 4], + proj: [[f32; 4]; 4], +} -pub struct WgpuCtx<'window> { +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct ModelUniform { + model: [[f32; 4]; 4], +} + +struct Camera { + uniform_buffer: wgpu::Buffer, + bind_group: wgpu::BindGroup, + view: Matrix4, + proj: Matrix4, +} + +impl Camera { + fn new( + device: &wgpu::Device, + bind_group_layout: &wgpu::BindGroupLayout, + width: u32, + height: u32, + ) -> Self { + let view = Matrix4::look_at_rh( + Point3::new(0.0, 0.0, 3.0), + Point3::new(0.0, 0.0, 0.0), + Vector3::unit_y(), + ); + let aspect = width as f32 / height as f32; + let proj = perspective(Rad::from(Deg(45.0)), aspect, 0.1, 100.0); + + let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Camera Uniform Buffer"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Camera Bind Group"), + layout: bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }], + }); + + Self { + uniform_buffer, + bind_group, + view, + proj, + } + } + + fn resize(&mut self, width: u32, height: u32) { + let aspect = width as f32 / height as f32; + self.proj = perspective(Rad::from(Deg(45.0)), aspect, 0.1, 100.0); + } + + fn update(&self, queue: &wgpu::Queue) { + let view_array: [[f32; 4]; 4] = self.view.into(); + let proj_array: [[f32; 4]; 4] = self.proj.into(); + let uniform = CameraUniform { + view: view_array, + proj: proj_array, + }; + queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform)); + } +} + +struct Model { + vertex_buffer: wgpu::Buffer, + index_buffer: wgpu::Buffer, + uniform_buffer: wgpu::Buffer, + bind_group: wgpu::BindGroup, + index_count: u32, + transform: Matrix4, +} + +impl Model { + fn new( + device: &wgpu::Device, + vertices: &[Vertex], + indices: &[u32], + bind_group_layout: &wgpu::BindGroupLayout, + ) -> Self { + let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Vertex Buffer"), + contents: bytemuck::cast_slice(vertices), + usage: wgpu::BufferUsages::VERTEX, + }); + + let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Index Buffer"), + contents: bytemuck::cast_slice(indices), // Use proper indices + usage: wgpu::BufferUsages::INDEX, + }); + + let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Model Uniform Buffer"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Model Bind Group"), + layout: bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }], + }); + Self { + vertex_buffer, + index_buffer, + uniform_buffer, + bind_group, + index_count: indices.len() as u32, + transform: Matrix4::identity(), + } + } + + fn update(&self, queue: &wgpu::Queue) { + let model_array: [[f32; 4]; 4] = self.transform.into(); + let uniform = ModelUniform { model: model_array }; + queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform)); + } + + fn set_transform(&mut self, transform: Matrix4) { + self.transform = transform; + } +} + +pub struct Renderer<'window> { device: wgpu::Device, queue: wgpu::Queue, surface: wgpu::Surface<'window>, surface_config: wgpu::SurfaceConfiguration, - adapter: wgpu::Adapter, + camera: Camera, + models: Vec, render_pipeline: wgpu::RenderPipeline, - uniform_buffer: wgpu::Buffer, - vertex_buffer: wgpu::Buffer, - start_time: Instant, + depth_texture: wgpu::Texture, + depth_texture_view: wgpu::TextureView, + camera_bind_group_layout: wgpu::BindGroupLayout, + model_bind_group_layout: wgpu::BindGroupLayout, bg_color: wgpu::Color, + start_time: Instant, + last_frame_instant: Instant, + frame_count: u32, + fps: f32, + font_state: FontState, } -impl<'window> WgpuCtx<'window> { - pub async fn new(window: Arc) -> Result, ContextError> { +struct FontState { + brush: TextBrush>, + section: OwnedSection, + scale: f32, + color: wgpu::Color, +} + +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"), ..Default::default() }); - let surface = instance.create_surface(Arc::clone(&window))?; + + let surface = instance + .create_surface(Arc::clone(&window)) + .ctx_err(ContextErrorKind::SurfaceCreation, "Surface initialization")?; + let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::default(), - force_fallback_adapter: false, compatible_surface: Some(&surface), + ..Default::default() }) .await - .expect("Failed to obtain render adapter"); + .ok_or_else(|| { + RenderContextError::new( + ContextErrorKind::AdapterRequest, + Some("Adapter selection".into()), + None, + ) + })?; + let (device, queue) = adapter - .request_device( - &wgpu::DeviceDescriptor { - label: None, - required_features: wgpu::Features::empty(), - required_limits: wgpu::Limits::default().using_resolution(adapter.limits()), - memory_hints: wgpu::MemoryHints::Performance, - }, - None, - ) + .request_device(&wgpu::DeviceDescriptor::default(), None) .await - .expect("Failed to create rendering device"); + .ctx_err(ContextErrorKind::DeviceRequest, "Device configuration")?; + let size = window.inner_size(); let width = size.width.max(1); let height = size.height.max(1); - let surface_config = surface.get_default_config(&adapter, width, height).unwrap(); - surface.configure(&device, &surface_config); - let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Uniform Buffer"), - size: 64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Cube Vertex Buffer"), - contents: bytemuck::cast_slice(CUBE_VERTICES), - usage: wgpu::BufferUsages::VERTEX, - }); - let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Cube Shader"), - source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(CUBE_SHADER)), - }); - let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Uniform Bind Group Layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: wgpu::BufferSize::new(64), - }, - count: None, - }], - }); + + let camera_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Camera Bind Group Layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: wgpu::BufferSize::new( + std::mem::size_of::() as u64, + ), + }, + count: None, + }], + }); + + let model_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Model Bind Group Layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: wgpu::BufferSize::new( + std::mem::size_of::() as u64, + ), + }, + count: None, + }], + }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Cube Pipeline Layout"), - bind_group_layouts: &[&bind_group_layout], + label: Some("Pipeline Layout"), + bind_group_layouts: &[&camera_bind_group_layout, &model_bind_group_layout], push_constant_ranges: &[], }); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Main Shader"), + source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(SHADER_SRC)), + }); + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Cube Render Pipeline"), + label: Some("Main Pipeline"), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &shader, entry_point: Some("vs_main"), buffers: &[Vertex::desc()], - compilation_options: wgpu::PipelineCompilationOptions::default(), + compilation_options: Default::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { - format: surface_config.format, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), + format: surface.get_capabilities(&adapter).formats[0], + blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], - compilation_options: wgpu::PipelineCompilationOptions::default(), + compilation_options: Default::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, strip_index_format: None, front_face: wgpu::FrontFace::Ccw, cull_mode: Some(wgpu::Face::Back), + // cull_mode: , polygon_mode: wgpu::PolygonMode::Fill, unclipped_depth: false, conservative: false, }, - depth_stencil: None, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), multisample: wgpu::MultisampleState { count: 1, mask: !0, @@ -340,15 +454,69 @@ impl<'window> WgpuCtx<'window> { multiview: None, cache: None, }); - Ok(WgpuCtx { + + let camera = Camera::new(&device, &camera_bind_group_layout, width, height); + + let surface_caps = surface.get_capabilities(&adapter); + let surface_config = wgpu::SurfaceConfiguration { + width, + height, + format: surface_caps.formats[0], + present_mode: wgpu::PresentMode::AutoNoVsync, + 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, + // surface_config.format, + ); + + let font_bytes = include_bytes!("DejaVuSans.ttf"); + let font = FontRef::try_from_slice(font_bytes).map_err(|e| { + RenderContextError::new( + ContextErrorKind::DeviceRequest, + Some("Font loading".into()), + None, + ) + })?; + + let brush = + BrushBuilder::using_font(font).build(&device, width, height, surface_config.format); + let base_width = 1280.0; + let base_scale = 30.0; + let scale = base_scale * (surface_config.width as f32 / base_width as f32).clamp(0.5, 2.0); + let color = wgpu::Color::WHITE; + + let section = OwnedSection::default() + .add_text(OwnedText::new("FPS: 0.00").with_scale(scale).with_color([ + color.r as f32, + color.g as f32, + color.b as f32, + color.a as f32, + ])) + .with_screen_position((10.0, 10.0)) + .with_bounds((base_scale * 200.0, base_scale * 2.0)) + .with_layout( + Layout::default() + .h_align(HorizontalAlign::Left) + .v_align(VerticalAlign::Top), + ); + + Ok(Self { device, queue, surface, surface_config, - adapter, + camera, + models: Vec::new(), render_pipeline, - uniform_buffer, - vertex_buffer, + camera_bind_group_layout, + model_bind_group_layout, bg_color: wgpu::Color { r: 0.1, g: 0.1, @@ -356,87 +524,208 @@ impl<'window> WgpuCtx<'window> { a: 1.0, }, start_time: Instant::now(), + last_frame_instant: Instant::now(), + frame_count: 0, + depth_texture, + depth_texture_view, + fps: 0f32, + font_state: FontState { + brush, + section, + scale, + color, + }, }) } - pub fn new_blocking(window: Arc) -> Result, ContextError> { + pub fn new_blocking(window: Arc) -> Result { block_on(Self::new(window)) } + pub fn add_model(&mut self, vertices: &[Vertex], indicies: &[u32]) { + let model = Model::new( + &self.device, + vertices, + indicies, + &self.model_bind_group_layout, + ); + self.models.push(model); + } + pub fn resize(&mut self, new_size: (u32, u32)) { let (width, height) = new_size; + let (depth_texture, depth_view) = create_depth_texture(&self.device, width, height); self.surface_config.width = width.max(1); self.surface_config.height = height.max(1); self.surface.configure(&self.device, &self.surface_config); + self.depth_texture = depth_texture; + self.depth_texture_view = depth_view; + self.font_state + .brush + .resize_view(width as f32, height as f32, &self.queue); + let base_width = 1280.0; + let base_scale = 30.0; + let scale = base_scale * (width as f32 / base_width as f32).clamp(0.5, 2.0); + self.font_state.scale = scale; + self.camera.resize(width, height); } pub fn draw(&mut self) { - let elapsed = self.start_time.elapsed().as_secs_f32() * 0.80f32; - let model = Matrix4::from_angle_x(Rad(elapsed)) * Matrix4::from_angle_y(Rad(elapsed)); - let view = Matrix4::look_at_rh( - Point3::new(0.0, 0.0, 3.0), - Point3::new(0.0, 0.0, 0.0), - Vector3::unit_y(), - ); - let aspect = self.surface_config.width as f32 / self.surface_config.height as f32; - let proj = perspective(Rad(std::f32::consts::FRAC_PI_4), aspect, 0.1, 100.0); - let mvp = proj * view * model; - let mvp_array: [[f32; 4]; 4] = [ - [mvp.x.x, mvp.x.y, mvp.x.z, mvp.x.w], - [mvp.y.x, mvp.y.y, mvp.y.z, mvp.y.w], - [mvp.z.x, mvp.z.y, mvp.z.z, mvp.z.w], - [mvp.w.x, mvp.w.y, mvp.w.z, mvp.w.w], - ]; - self.queue - .write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&mvp_array)); + let elapsed = self.start_time.elapsed().as_secs_f32(); + + self.camera.update(&self.queue); + + for (i, model) in self.models.iter_mut().enumerate() { + let angle = Rad(elapsed * 0.8 + i as f32 * 0.3); + model.set_transform(Matrix4::from_angle_y(angle)); + // model.set_transform(Matrix4::from_angle_x(angle) * Matrix4::from_angle_y(angle)); + model.update(&self.queue); + } + let surface_texture = self .surface .get_current_texture() - .expect("Failed to get surface texture"); - let view_texture = surface_texture + .ctx_err( + ContextErrorKind::SurfaceTexture, + "Surface texture acquisition", + ) + .unwrap(); + + let view = surface_texture .texture .create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Cube Command Encoder"), + label: Some("Render Encoder"), }); + let fps_text = format!("FPS: {:.2}", self.fps); + self.font_state.section.text.clear(); + self.font_state.section.text.push( + OwnedText::new(fps_text) + .with_scale(self.font_state.scale) + .with_color([ + self.font_state.color.r as f32, + self.font_state.color.g as f32, + self.font_state.color.b as f32, + self.font_state.color.a as f32, + ]), + ); + if let Err(e) = self.font_state.brush.queue( + &self.device, + &self.queue, + &[self.font_state.section.clone()], + ) { + error!("Failed to queue text: {}", e); + } + { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Cube Render Pass"), + label: Some("Main Render Pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view_texture, + view: &view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(self.bg_color), store: wgpu::StoreOp::Store, }, })], - depth_stencil_attachment: None, - timestamp_writes: None, + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &self.depth_texture_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), occlusion_query_set: None, + timestamp_writes: None, }); + render_pass.set_pipeline(&self.render_pipeline); - let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Uniform Bind Group"), - layout: &self.render_pipeline.get_bind_group_layout(0), - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: self.uniform_buffer.as_entire_binding(), - }], - }); - render_pass.set_bind_group(0, &bind_group, &[]); - render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); - render_pass.draw(0..36, 0..1); + render_pass.set_bind_group(0, &self.camera.bind_group, &[]); + + for model in &self.models { + render_pass.set_bind_group(1, &model.bind_group, &[]); + render_pass.set_vertex_buffer(0, model.vertex_buffer.slice(..)); + render_pass + .set_index_buffer(model.index_buffer.slice(..), wgpu::IndexFormat::Uint32); + render_pass.draw_indexed(0..model.index_count, 0, 0..1); + } } + { + let mut text_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Text Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + + self.font_state.brush.draw(&mut text_pass); + } + self.queue.submit(Some(encoder.finish())); surface_texture.present(); + + self.frame_count += 1; + + let elapsed_secs = self.last_frame_instant.elapsed().as_secs_f32(); + if elapsed_secs >= 1.0 { + let fps = self.frame_count as f32 / elapsed_secs; + // trace!("Renderer FPS: {:.2}", fps); + self.fps = fps; + self.frame_count = 0; + self.last_frame_instant = Instant::now(); + } } - pub fn change_bg_color(&mut self, color: wgpu::Color) { + pub fn set_bg_color(&mut self, color: wgpu::Color) { self.bg_color = color; } - pub fn bg_color(&self) -> wgpu::Color { - self.bg_color.clone() + + pub fn bg_color(&self) -> &wgpu::Color { + &self.bg_color + } + pub fn text_color(&self) -> &wgpu::Color { + &self.font_state.color + } + pub fn set_text_color(&mut self, color: wgpu::Color) { + self.font_state.color = color; } } +fn create_depth_texture( + device: &wgpu::Device, + width: u32, + height: u32, + // format: wgpu::TextureFormat, +) -> (wgpu::Texture, wgpu::TextureView) { + let size = wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }; + + let desc = wgpu::TextureDescriptor { + label: Some("Depth Texture"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Depth32Float, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }; + + let texture = device.create_texture(&desc); + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + (texture, view) +} diff --git a/engine/src/core/render/mod.rs b/engine/src/core/render/mod.rs index b8cf30c..b6e54e1 100644 --- a/engine/src/core/render/mod.rs +++ b/engine/src/core/render/mod.rs @@ -1,19 +1,32 @@ -use std::collections::HashMap; +use std::ops::Deref; use std::sync::Arc; -use ctx::WgpuCtx; +use ctx::{Renderer, Vertex}; +use std::env; +use std::fs; +use std::path::PathBuf; +use tobj::{LoadOptions, Model}; use tracing::{debug, error, info, trace, warn}; +use wgpu::rwh::HasWindowHandle; use winit::application::ApplicationHandler; -use winit::event::WindowEvent; -use winit::event_loop::ControlFlow; -use winit::event_loop::{ActiveEventLoop, EventLoop}; -use winit::window::{Window, WindowId}; +use winit::event::{KeyEvent, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; +use winit::window::Window; +use winit::window::WindowId; + pub mod ctx; struct WindowContext<'window> { window: Arc, - ctx: WgpuCtx<'window>, - main_window: bool + ctx: Renderer<'window>, + main_window: bool, +} +impl Deref for WindowContext<'_> { + type Target = winit::window::Window; + + fn deref(&self) -> &Self::Target { + self.window.as_ref() + } } impl WindowContext<'_> { pub fn is_main_window(&self) -> bool { @@ -23,28 +36,284 @@ impl WindowContext<'_> { #[derive(Default)] pub struct App<'window> { - windows: HashMap>, + windows: ahash::AHashMap>, +} + +static CUBE_OBJ: &str = " +# Blender 4.2.3 LTS +# www.blender.org +mtllib cube.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 +vn -0.0000 1.0000 -0.0000 +vn -0.0000 -0.0000 1.0000 +vn -1.0000 -0.0000 -0.0000 +vn -0.0000 -1.0000 -0.0000 +vn 1.0000 -0.0000 -0.0000 +vn -0.0000 -0.0000 -1.0000 +vt 0.625000 0.500000 +vt 0.875000 0.500000 +vt 0.875000 0.750000 +vt 0.625000 0.750000 +vt 0.375000 0.750000 +vt 0.625000 1.000000 +vt 0.375000 1.000000 +vt 0.375000 0.000000 +vt 0.625000 0.000000 +vt 0.625000 0.250000 +vt 0.375000 0.250000 +vt 0.125000 0.500000 +vt 0.375000 0.500000 +vt 0.125000 0.750000 +s 0 +usemtl Material +f 1/1/1 5/2/1 7/3/1 3/4/1 +f 4/5/2 3/4/2 7/6/2 8/7/2 +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<'_> { + fn create_main_window(&mut self, event_loop: &ActiveEventLoop) { + let win_attr = Window::default_attributes().with_title("Zenyx"); + match event_loop.create_window(win_attr) { + Ok(window) => { + let window = Arc::new(window); + let window_id = window.id(); + match Renderer::new_blocking(window.clone()) { + Ok(mut wgpu_ctx) => { + let obj = match tobj::load_obj( + "Pumpkin.obj", + &LoadOptions { + triangulate: true, + single_index: true, + ..Default::default() + }, + ) { + Ok(obj) => obj, + Err(e) => { + error!("{e}"); + panic!() + } + }; + let (combined_vertices, combined_indices) = parse_obj(&obj.0); + + wgpu_ctx.add_model(&combined_vertices, &combined_indices); + + self.windows.insert( + window_id, + WindowContext { + window, + ctx: wgpu_ctx, + main_window: true, + }, + ); + info!("Main window created: {:?}", window_id); + } + Err(e) => error!("Failed to create WGPU context: {:?}", e), + } + } + Err(e) => error!("Failed to create main window: {:?}", e), + } + } + + fn handle_close_requested(&mut self, window_id: WindowId) { + if self.windows.remove(&window_id).is_some() { + debug!("Window {:?} closed", window_id); + } else { + warn!("Tried to close non-existent window {:?}", window_id); + } + } + + fn handle_keyboard_input( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + key_event: KeyEvent, + ) { + if !key_event.state.is_pressed() { + return; + } + match key_event.physical_key { + winit::keyboard::PhysicalKey::Code(code) => match code { + winit::keyboard::KeyCode::Space => { + self.toggle_background(window_id); + } + winit::keyboard::KeyCode::Escape => { + self.spawn_child_window(event_loop); + } + other => error!("Unimplemented keycode: {:?}", other), + }, + _ => debug!("Received a keyboard event with no physical key"), + } + } + + fn toggle_background(&mut self, window_id: WindowId) { + if let Some(window_context) = self.windows.get_mut(&window_id) { + let current_color = window_context.ctx.bg_color(); + + let new_color = match current_color { + &wgpu::Color::WHITE => wgpu::Color::BLACK, + &wgpu::Color::BLACK => wgpu::Color::WHITE, + _ => wgpu::Color::WHITE, + }; + let new_text_color = match window_context.ctx.text_color() { + &wgpu::Color::WHITE => wgpu::Color::BLACK, + &wgpu::Color::BLACK => wgpu::Color::WHITE, + _ => wgpu::Color::WHITE, + }; + + println!("new text color {new_text_color:#?}"); + window_context.ctx.set_bg_color(new_color); + window_context.ctx.set_text_color(new_text_color); + debug!("Toggled background color for window {:?}", window_id); + } else { + warn!("No window context for toggling background: {:?}", 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()) { + let title = format!("Zenyx - New Window {}", self.windows.len()); + // TODO: Verify that this is safe instead of matching on it + let win_attr = unsafe { + let base = Window::default_attributes().with_title(title); + match main_ctx.window_handle() { + Ok(handle) => base.with_parent_window(Some(handle.as_raw())), + Err(_) => base, + } + }; + match event_loop.create_window(win_attr) { + Ok(window) => { + let window = Arc::new(window); + let window_id = window.id(); + match Renderer::new_blocking(window.clone()) { + Ok(mut wgpu_ctx) => { + { + 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); + } + + let load_options = tobj::LoadOptions { + triangulate: true, + single_index: true, + ..Default::default() + }; + match tobj::load_obj(tmp_path.to_str().unwrap(), &load_options) { + Ok(cube_model) => { + let (cube_vertices, cube_indices) = + parse_obj(&cube_model.0); + wgpu_ctx.add_model(&cube_vertices, &cube_indices); + } + Err(e) => { + error!("Failed to load cube OBJ from temp file: {:?}", e) + } + } + } + self.windows.insert( + window_id, + WindowContext { + window, + ctx: wgpu_ctx, + main_window: false, + }, + ); + 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 child window: {:?}", e), + } + } else { + error!("No main window found. Cannot spawn a child window."); + } + } + + fn handle_redraw_requested(&mut self, window_id: WindowId) { + if let Some(window_context) = self.windows.get_mut(&window_id) { + window_context.ctx.draw(); + window_context.request_redraw(); + trace!( + "Redrew window {:?} with title: {}", + window_id, + window_context.window.title() + ); + } else { + warn!("Received redraw for unknown window {:?}", window_id); + } + } + + fn handle_resize(&mut self, window_id: WindowId, new_size: winit::dpi::PhysicalSize) { + if let Some(window_context) = self.windows.get_mut(&window_id) { + window_context.ctx.resize(new_size.into()); + window_context.window.request_redraw(); + debug!( + "Resized window {:?} to {}x{}", + window_id, new_size.width, new_size.height + ); + } else { + warn!("Received resize for unknown window {:?}", window_id); + } + } + + fn handle_destroyed(&mut self, event_loop: &ActiveEventLoop) { + if self.windows.is_empty() || !self.windows.iter().any(|(_, ctx)| ctx.is_main_window()) { + self.windows.clear(); + debug!("All main windows are closed. Exiting event loop."); + event_loop.exit(); + } + } +} + +fn parse_obj(obj: &Vec) -> (Vec, Vec) { + let mut combined_vertices = Vec::new(); + let mut combined_indices = Vec::new(); + let mut vertex_offset = 0; + + for object in obj { + let mesh: &_ = &object.mesh; + let vertices: Vec = (0..mesh.positions.len() / 3) + .map(|i| Vertex { + position: [ + mesh.positions[i * 3], + mesh.positions[i * 3 + 1], + mesh.positions[i * 3 + 2], + ], + normal: if !mesh.normals.is_empty() { + [ + mesh.normals[i * 3], + mesh.normals[i * 3 + 1], + mesh.normals[i * 3 + 2], + ] + } else { + [0.0; 3] + }, + }) + .collect(); + combined_vertices.extend(vertices); + combined_indices.extend(mesh.indices.iter().map(|&index| index + vertex_offset)); + vertex_offset += (mesh.positions.len() as u32) / 3; + } + + (combined_vertices, combined_indices) } impl ApplicationHandler for App<'_> { fn resumed(&mut self, event_loop: &ActiveEventLoop) { if self.windows.is_empty() { - let win_attr = Window::default_attributes().with_title("Zenyx"); - let window = Arc::new( - event_loop - .create_window(win_attr) - .expect("create window err."), - ); - let window_id = window.id(); - let wgpu_ctx = WgpuCtx::new_blocking(window.clone()).unwrap(); - self.windows.insert( - window_id, - WindowContext { - window, - ctx: wgpu_ctx, - main_window: true, - }, - ); + self.create_main_window(event_loop); } } @@ -56,88 +325,31 @@ impl ApplicationHandler for App<'_> { ) { match event { WindowEvent::CloseRequested => { - if let Some(window_context) = self.windows.remove(&window_id) { - - drop(window_context); - debug!("Window: {:?} closed, exiting", window_id); - } + self.handle_close_requested(window_id); } WindowEvent::KeyboardInput { - device_id, - event, - is_synthetic, - } => match event.physical_key { - winit::keyboard::PhysicalKey::Code(code) => { - if event.state.is_pressed() == false { - return; - } - match code { - winit::keyboard::KeyCode::Space => { - debug!("Space key pressed"); - if let Some(window_context) = self.windows.get_mut(&window_id){ - match window_context.ctx.bg_color() { - wgpu::Color::WHITE => { - window_context.ctx.change_bg_color(wgpu::Color::BLACK) - } - wgpu::Color::BLACK => { - window_context.ctx.change_bg_color(wgpu::Color::WHITE) - } - _ => window_context.ctx.change_bg_color(wgpu::Color::WHITE), - } - } - } - winit::keyboard::KeyCode::Escape => { - debug!("Escape key pressed, spawning new window"); - let win_attr = Window::default_attributes() - .with_title(format!("Zenyx - New Window {}", self.windows.len())); - let new_window = Arc::new( - event_loop - .create_window(win_attr) - .expect("create window err."), - ); - let window_id = new_window.id(); - let wgpu_ctx = WgpuCtx::new_blocking(new_window.clone()).unwrap(); - self.windows.insert( - window_id, - WindowContext { - window: new_window, - ctx: wgpu_ctx, - main_window: false, - }, - ); - } - _ => info!("Unimplemented keycode: {:?}", code), - } - } - _ => {} - }, - WindowEvent::RedrawRequested => { - if let Some(window_context) = self.windows.get_mut(&window_id) { - window_context.ctx.draw(); - window_context.window.request_redraw(); - } + event: key_event, .. + } => { + self.handle_keyboard_input(event_loop, window_id, key_event); } - WindowEvent::Resized(size) => { - if let Some(window_context) = self.windows.get_mut(&window_id) { - window_context.ctx.resize(size.into()); - window_context.window.request_redraw(); - let size_str: String = size.height.to_string() + "x" + &size.width.to_string(); - debug!("Window resized to {:?}", size_str); - } + WindowEvent::RedrawRequested => { + self.handle_redraw_requested(window_id); + } + WindowEvent::Resized(new_size) => { + self.handle_resize(window_id, new_size); } WindowEvent::Destroyed => { - if !self.windows.iter().any(|(_,ctx)| ctx.is_main_window()) || self.windows.is_empty() { - self.windows.clear(); - event_loop.exit(); - } + self.handle_destroyed(event_loop); } - _ => trace!("Unhandled window event"), + _ => trace!("Unhandled window event for window {:?}", window_id), } } } pub fn init_renderer(event_loop: EventLoop<()>) { - event_loop.set_control_flow(ControlFlow::Poll); + event_loop.set_control_flow(ControlFlow::Wait); let mut app = App::default(); - event_loop.run_app(&mut app).unwrap(); + if let Err(e) = event_loop.run_app(&mut app) { + error!("Failed to run application: {:?}", e); + } } diff --git a/engine/src/core/render/shader.wgsl b/engine/src/core/render/shader.wgsl new file mode 100644 index 0000000..90780f1 --- /dev/null +++ b/engine/src/core/render/shader.wgsl @@ -0,0 +1,42 @@ +struct CameraUniform { + view: mat4x4, + proj: mat4x4, +}; + +struct ModelUniform { + model: mat4x4, +}; + +@group(0) @binding(0) +var camera: CameraUniform; + +@group(1) @binding(0) +var model: ModelUniform; + +struct VertexInput { + @location(0) position: vec3, + @location(1) normal: vec3, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) normal: vec3, +}; + +@vertex +fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + let model_pos = model.model * vec4(input.position, 1.0); + output.clip_position = camera.proj * camera.view * model_pos; + output.normal = input.normal; + return output; +} + +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4 { + let ambient: f32 = 0.2; + let light_dir = normalize(vec3(0.5, 1.0, 0.5)); + let diffuse = clamp(dot(normalize(input.normal), light_dir), 0.0, 1.0); + let brightness = ambient + (1.0 - ambient) * diffuse; + return vec4(0.7 * brightness, 0.7 * brightness, 0.9 * brightness, 1.0); +} \ No newline at end of file diff --git a/engine/src/main.rs b/engine/src/main.rs index 8b0784f..25413fe 100644 --- a/engine/src/main.rs +++ b/engine/src/main.rs @@ -5,7 +5,6 @@ use tokio::runtime; #[allow(unused_imports)] use tracing::{debug, error, info, warn}; use tracing::{level_filters::LevelFilter, subscriber::set_global_default}; -use tracing_subscriber::{layer::Filter, util::SubscriberInitExt}; use winit::event_loop::EventLoop; pub mod core; @@ -45,7 +44,6 @@ async fn main() -> anyhow::Result<()> { info!("Type 'help' for a list of commands."); core::render::init_renderer(event_loop); - if let Err(_) = repl_thread.join() { eprintln!("REPL thread panicked"); } diff --git a/flake.lock b/flake.lock index 59dab09..1c67c8f 100644 --- a/flake.lock +++ b/flake.lock @@ -77,11 +77,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1742422364, - "narHash": "sha256-mNqIplmEohk5jRkqYqG19GA8MbQ/D4gQSK0Mu4LvfRQ=", + "lastModified": 1743095683, + "narHash": "sha256-gWd4urRoLRe8GLVC/3rYRae1h+xfQzt09xOfb0PaHSk=", "owner": "nixos", "repo": "nixpkgs", - "rev": "a84ebe20c6bc2ecbcfb000a50776219f48d134cc", + "rev": "5e5402ecbcb27af32284d4a62553c019a3a49ea6", "type": "github" }, "original": { @@ -138,11 +138,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1742610648, - "narHash": "sha256-9jWi3gw3fEIgEslnFjH/s1I+Iyf1+4t5B1Ed1FOiy8o=", + "lastModified": 1743215516, + "narHash": "sha256-52qbrkG65U1hyrQWltgHTgH4nm0SJL+9TWv2UDCEPNI=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "c60d41987df3c853e2a842de2c63ded40400979b", + "rev": "524463199fdee49338006b049bc376b965a2cfed", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index fca13cb..49c15bc 100644 --- a/flake.nix +++ b/flake.nix @@ -52,7 +52,9 @@ devShells.default = pkgs.mkShell { name = "zenyx"; nativeBuildInputs = with pkgs; [ - rust-bin.stable.latest.default + (rust-bin.stable.latest.default.override { + extensions = ["rust-src" "cargo" "rustfmt" "clippy"]; + }) pkg-config ]; buildInputs = buildInputs; diff --git a/test.mtl b/test.mtl new file mode 100644 index 0000000..4704186 --- /dev/null +++ b/test.mtl @@ -0,0 +1,2 @@ +# Blender 4.2.3 LTS MTL File: 'None' +# www.blender.org