Merge pull request 'Improve rendering and reduce rendering related crashes' (#5) from error_handling into main

Reviewed-on: https://codeberg.org/Caznix/Zenyx/pulls/5
This commit is contained in:
Caznix 2025-03-30 00:22:56 +00:00
commit 3b2281b15a
16 changed files with 20593 additions and 484 deletions

View file

@ -1 +1,4 @@
[alias]
[build]
rustflags = ["-Ctarget-cpu=native"]

243
Cargo.lock generated
View file

@ -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"

View file

@ -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

22
Pumpkin.mtl Normal file
View file

@ -0,0 +1,22 @@
# Blender 4.2.3 LTS MTL File: 'None'
# www.blender.org
newmtl Material.001
Ns 0.000000
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.800000 0.800000
Ks 0.000000 0.000000 0.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 1
newmtl Material.003
Ns 0.000000
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.800000 0.800000
Ks 0.000000 0.000000 0.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 1

19331
Pumpkin.obj Normal file

File diff suppressed because it is too large Load diff

View file

@ -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}
'';

View file

@ -3,33 +3,43 @@ 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"

View file

@ -1,3 +1,4 @@
use std::error::Error;
use std::fmt::Write as FmtWrite;
use std::mem;
@ -8,31 +9,30 @@ 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(|_| {
default_hook(info);
std::process::exit(0);
});
if !log_path.exists() {
std::fs::create_dir_all(&log_path).unwrap_or_else(|_| {
if let Err(e) = process_panic(info) {
eprintln!("Error in panic hook: {}", e);
default_hook(info);
}
std::process::exit(0);
}));
});
}
let log_path = log_path.join("panic.log");
let mut file = std::fs::File::create(&log_path).unwrap_or_else(|_| {
default_hook(info);
std::process::exit(0);
});
fn process_panic(info: &std::panic::PanicHookInfo<'_>) -> Result<(), Box<dyn Error>> {
use crate::workspace;
use colored::Colorize;
use std::io::Write;
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 mut file = std::fs::File::create(&log_path)?;
let payload = info.payload();
let payload_str = if let Some(s) = payload.downcast_ref::<&str>() {
*s
@ -42,14 +42,8 @@ pub fn set_panic_hook() {
"<non-string panic payload>"
};
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);
});
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.
@ -64,10 +58,11 @@ We take privacy seriously, and do not perform any automated error collection. In
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!(
"\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}<USER>").to_string()
}
}

Binary file not shown.

View file

@ -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<f32>,
};
@group(0) @binding(0)
var<uniform> u: Uniforms;
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) normal: vec3<f32>,
};
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
output.clip_position = u.mvp * vec4<f32>(input.position, 1.0);
output.normal = input.normal;
return output;
#[derive(Debug, Error)]
pub struct RenderContextError {
kind: ContextErrorKind,
label: Option<Cow<'static, str>>,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
let light_dir = normalize(vec3<f32>(0.5, 1.0, 0.5));
let brightness = clamp(dot(normalize(input.normal), light_dir), 0.0, 1.0);
return vec4<f32>(0.7 * brightness, 0.7 * brightness, 0.9 * brightness, 1.0);
impl RenderContextError {
pub fn new(
kind: ContextErrorKind,
label: impl Into<Option<Cow<'static, str>>>,
source: impl Into<Option<Box<dyn std::error::Error + Send + Sync>>>,
) -> Self {
Self {
kind,
label: label.into(),
source: source.into(),
}
"#;
}
pub fn with_label(mut self, label: impl Into<Cow<'static, str>>) -> 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<T> {
fn ctx_err(
self,
kind: ContextErrorKind,
label: impl Into<Cow<'static, str>>,
) -> Result<T, RenderContextError>;
}
impl<T, E> IntoRenderContextError<T> for Result<T, E>
where
E: std::error::Error + Send + Sync + 'static,
{
fn ctx_err(
self,
kind: ContextErrorKind,
label: impl Into<Cow<'static, str>>,
) -> Result<T, RenderContextError> {
self.map_err(|e| {
RenderContextError::new(
kind,
Some(label.into()),
Some(Box::new(e) as Box<dyn std::error::Error + Send + Sync>),
)
})
}
}
impl From<wgpu::CreateSurfaceError> for RenderContextError {
fn from(err: wgpu::CreateSurfaceError) -> Self {
RenderContextError::new(
ContextErrorKind::SurfaceCreation,
Some("Surface creation".into()),
Some(Box::new(err) as Box<dyn std::error::Error + Send + Sync>),
)
}
}
impl From<wgpu::RequestDeviceError> for RenderContextError {
fn from(err: wgpu::RequestDeviceError) -> Self {
RenderContextError::new(
ContextErrorKind::DeviceRequest,
Some("Device setup".into()),
Some(Box::new(err) as Box<dyn std::error::Error + Send + Sync>),
)
}
}
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<f32>,
proj: Matrix4<f32>,
}
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::<CameraUniform>() 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<f32>,
}
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::<ModelUniform>() 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<f32>) {
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<Model>,
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<Window>) -> Result<WgpuCtx<'window>, ContextError> {
struct FontState {
brush: TextBrush<FontRef<'static>>,
section: OwnedSection,
scale: f32,
color: wgpu::Color,
}
impl<'window> Renderer<'window> {
pub async fn new(window: Arc<Window>) -> Result<Self, RenderContextError> {
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");
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,
},
.ok_or_else(|| {
RenderContextError::new(
ContextErrorKind::AdapterRequest,
Some("Adapter selection".into()),
None,
)
})?;
let (device, queue) = adapter
.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"),
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(64),
min_binding_size: wgpu::BufferSize::new(
std::mem::size_of::<CameraUniform>() 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::<ModelUniform>() 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<Window>) -> Result<WgpuCtx<'window>, ContextError> {
pub fn new_blocking(window: Arc<Window>) -> Result<Self, RenderContextError> {
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);
}
self.queue.submit(Some(encoder.finish()));
surface_texture.present();
}
{
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);
}
pub fn change_bg_color(&mut self, color: wgpu::Color) {
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 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)
}

View file

@ -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<Window>,
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,20 +36,79 @@ impl WindowContext<'_> {
#[derive(Default)]
pub struct App<'window> {
windows: HashMap<WindowId, WindowContext<'window>>,
windows: ahash::AHashMap<WindowId, WindowContext<'window>>,
}
impl ApplicationHandler for App<'_> {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.windows.is_empty() {
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");
let window = Arc::new(
event_loop
.create_window(win_attr)
.expect("create window err."),
);
match event_loop.create_window(win_attr) {
Ok(window) => {
let window = Arc::new(window);
let window_id = window.id();
let wgpu_ctx = WgpuCtx::new_blocking(window.clone()).unwrap();
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 {
@ -45,6 +117,203 @@ impl ApplicationHandler for App<'_> {
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<u32>) {
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<Model>) -> (Vec<Vertex>, Vec<u32>) {
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<Vertex> = (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() {
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;
event: key_event, ..
} => {
self.handle_keyboard_input(event_loop, window_id, key_event);
}
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();
}
}
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);
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);
}
}

View file

@ -0,0 +1,42 @@
struct CameraUniform {
view: mat4x4<f32>,
proj: mat4x4<f32>,
};
struct ModelUniform {
model: mat4x4<f32>,
};
@group(0) @binding(0)
var<uniform> camera: CameraUniform;
@group(1) @binding(0)
var<uniform> model: ModelUniform;
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) normal: vec3<f32>,
};
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
let model_pos = model.model * vec4<f32>(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<f32> {
let ambient: f32 = 0.2;
let light_dir = normalize(vec3<f32>(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<f32>(0.7 * brightness, 0.7 * brightness, 0.9 * brightness, 1.0);
}

View file

@ -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");
}

12
flake.lock generated
View file

@ -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": {

View file

@ -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;

2
test.mtl Normal file
View file

@ -0,0 +1,2 @@
# Blender 4.2.3 LTS MTL File: 'None'
# www.blender.org