diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..2846269 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,9 @@ + +[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'] +rustflags = ["-C", "target-feature=+avx"] + +[target.wasm32-unknown-unknown] +rustflags = ["--cfg=web_sys_unstable_apis"] + +[alias] +run-wasm = "run --release --package run-wasm --" diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0445f2c..d20463b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -38,7 +38,7 @@ jobs: cargo build --workspace --all-targets - name: Run tests run: | - cargo test --workspace --all-targets + cargo test --workspace --all-targets --exclude benches check: name: Rustfmt & Clippy diff --git a/Cargo.toml b/Cargo.toml index aae69eb..6f51291 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "crates/*", "crates/*/macros", + "run-wasm", "benches" ] default-members = [ @@ -18,14 +19,42 @@ edition = "2024" rust-version = "1.86" [workspace.dependencies] +glam = "0.30" +serde = "1.0" +raw-window-handle = "0.6" slotmap = "1.0" +thiserror = "2.0" +tracing = "0.1" +bitflags = "2.9" fnv = "1.0" +radsort = "0.1" +bytemuck = "1.22" +blink-alloc = "0.3" +dynsequence = { version = "0.1.0-alpha.4" } +blocking = "1.6" threadpool = "1.8" backtrace = "0.3" +gametime = "0.5" atomic_refcell = "0.1" +palette = { version = "0.7", default-features = false } +image = { version = "0.25", default-features = false } +encase = { version = "0.11", features = ["glam"], default-features = false } +encase_derive_impl = { version = "0.11" } crossbeam-utils = "0.8" +crossbeam-queue = "0.3" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-wasm = "0.2" +tracing-log = "0.2" +console_error_panic_hook = "0.1" +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +js-sys = "0.3" +web-sys = "0.3" +ndk = "0.9" +ndk-glue = "0.7" darling = "0.20" proc-macro2 = "1.0" syn = "2.0" quote = "1.0" proc-macro-crate = "3.3" + diff --git a/crates/assets-loader/CHANGELOG.md b/crates/assets-loader/CHANGELOG.md new file mode 100644 index 0000000..fa542aa --- /dev/null +++ b/crates/assets-loader/CHANGELOG.md @@ -0,0 +1,6 @@ +# `pulz-assets-loader` Changelog +All notable changes to this crate will be documented in this file. + +## Unreleased (DATE) + + * Initial version diff --git a/crates/assets-loader/Cargo.toml b/crates/assets-loader/Cargo.toml new file mode 100644 index 0000000..ae3ddc8 --- /dev/null +++ b/crates/assets-loader/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "pulz-assets-loader" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +readme = "README.md" + +[dependencies] +pulz-ecs = { path = "../ecs" } +pulz-assets = { path = "../assets" } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +blocking = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = { workspace = true } +wasm-bindgen-futures = { workspace = true } +js-sys = { workspace = true } +web-sys = { workspace = true, features = [ 'Request', 'Response', "Window", ]} + +[target.'cfg(target_os = "android")'.dependencies] +ndk = { workspace = true } diff --git a/crates/assets-loader/README.md b/crates/assets-loader/README.md new file mode 100644 index 0000000..ad064fe --- /dev/null +++ b/crates/assets-loader/README.md @@ -0,0 +1,34 @@ +# `pulz-assets-loader` + + + +[![Crates.io](https://img.shields.io/crates/v/pulz-assets-loader.svg?label=pulz-assets-loader)](https://crates.io/crates/pulz-assets-loader) +[![docs.rs](https://docs.rs/pulz-assets-loader/badge.svg)](https://docs.rs/pulz-assets-loader/) +[![license: MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](#license) +[![Rust CI](https://github.com/HellButcher/pulz/actions/workflows/rust.yml/badge.svg)](https://github.com/HellButcher/pulz/actions/workflows/rust.yml) + + +**TODO** + +## Example + + +**TODO** + +## License + +[license]: #license + +This project is licensed under either of + +* MIT license ([LICENSE-MIT] or ) +* Apache License, Version 2.0, ([LICENSE-APACHE] or ) + +at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +[LICENSE-MIT]: ../../LICENSE-MIT +[LICENSE-APACHE]: ../../LICENSE-APACHE diff --git a/crates/assets-loader/src/lib.rs b/crates/assets-loader/src/lib.rs new file mode 100644 index 0000000..6c57fe1 --- /dev/null +++ b/crates/assets-loader/src/lib.rs @@ -0,0 +1,165 @@ +#![warn( + // missing_docs, + // rustdoc::missing_doc_code_examples, + future_incompatible, + rust_2018_idioms, + unused, + trivial_casts, + trivial_numeric_casts, + unused_lifetimes, + unused_qualifications, + unused_crate_dependencies, + clippy::cargo, + clippy::multiple_crate_versions, + clippy::empty_line_after_outer_attr, + clippy::fallible_impl_from, + clippy::redundant_pub_crate, + clippy::use_self, + clippy::suspicious_operation_groupings, + clippy::useless_let_if_seq, + // clippy::missing_errors_doc, + // clippy::missing_panics_doc, + clippy::wildcard_imports +)] +#![doc(html_logo_url = "https://raw.githubusercontent.com/HellButcher/pulz/master/docs/logo.png")] +#![doc(html_no_source)] +#![doc = include_str!("../README.md")] + +use std::{future::Future, io::Cursor}; + +use path::{AssetPath, AssetPathBuf}; +use platform::AssetOpen; + +pub mod path; +pub mod platform; + +pub trait LoadAsset { + type Future: Future>; + type Error: From; + fn load(&self, load: Load<'_>) -> Self::Future; +} + +impl LoadAsset for T +where + T: Fn(Load<'_>) -> F, + F: Future>, + E: From, +{ + type Future = F; + type Error = E; + fn load(&self, load: Load<'_>) -> Self::Future { + self(load) + } +} + +pub struct Load<'a> { + buffer: Cursor>, + path: AssetPathBuf, + server: &'a AssetServer, +} + +impl Load<'_> { + #[inline] + pub fn path(&self) -> &AssetPath { + &self.path + } + + #[inline] + pub fn into_vec(self) -> Vec { + self.buffer.into_inner() + } + + #[inline] + pub fn cursor_mut(&mut self) -> &'_ mut Cursor> { + &mut self.buffer + } + + #[inline] + pub fn as_slice(&self) -> &'_ [u8] { + self.buffer.get_ref() + } + + #[inline] + pub async fn load(&self, path: impl AsRef) -> std::io::Result> { + self._load(path.as_ref()).await + } + async fn _load(&self, path: &AssetPath) -> std::io::Result> { + let full_path = if self.path.is_directory() { + self.path.join(path) + } else if let Some(parent) = self.path.parent() { + parent.join(path) + } else { + let mut p = AssetPathBuf::from("/"); + p.push(path); + p + }; + self.server._load(full_path).await + } +} + +impl AsRef<[u8]> for Load<'_> { + #[inline] + fn as_ref(&self) -> &[u8] { + self.as_slice() + } +} + +pub struct AssetServer { + io: Box, +} + +impl Default for AssetServer { + fn default() -> Self { + Self::new() + } +} + +impl AssetServer { + #[cfg(not(target_os = "android"))] + pub fn new() -> Self { + #[cfg(not(any(target_os = "android", target_arch = "wasm32")))] + let io = platform::fs::FileSystemAssetLoaderIo::new(); + + #[cfg(target_arch = "wasm32")] + let io = platform::wasm::WasmFetchAssetLoaderIo::new(); + + Self::with(io) + } + + #[cfg(target_os = "android")] + pub fn with_asset_manager(manager: ::ndk::asset::AssetManager) -> Self { + let io = platform::android::AndroidAssetLoaderIo::with_asset_manager(manager); + Self::with(io) + } + + #[inline] + pub fn with(io: impl AssetOpen + 'static) -> Self { + Self { io: Box::new(io) } + } +} + +impl AssetServer { + pub async fn load_with>( + &self, + path: impl AsRef, + loader: L, + ) -> Result { + let load = self.load(path).await?; + loader.load(load).await + } + + pub async fn load(&self, path: impl AsRef) -> std::io::Result> { + let mut abs_base = AssetPathBuf::from("/"); + abs_base.push(path); + self._load(abs_base).await + } + + async fn _load(&self, path: AssetPathBuf) -> std::io::Result> { + let buffer = self.io.load(&path).await?; + Ok(Load { + buffer: Cursor::new(buffer), + path, + server: self, + }) + } +} diff --git a/crates/assets-loader/src/path.rs b/crates/assets-loader/src/path.rs new file mode 100644 index 0000000..0211a93 --- /dev/null +++ b/crates/assets-loader/src/path.rs @@ -0,0 +1,692 @@ +pub use std::path::Path; +use std::{ + borrow::{Borrow, Cow}, + iter::FusedIterator, + ops::Deref, + str::FromStr, +}; + +#[repr(transparent)] +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AssetPathBuf(String); + +#[repr(transparent)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AssetPath(str); + +impl AssetPathBuf { + #[inline] + pub const fn new() -> Self { + Self(String::new()) + } + + #[inline] + pub fn with_capacity(capacity: usize) -> Self { + Self(String::with_capacity(capacity)) + } + + #[inline] + pub fn as_path(&self) -> &AssetPath { + self + } + + #[inline] + pub fn push>(&mut self, path: P) { + self._push(path.as_ref()) + } + + fn _push(&mut self, path: &AssetPath) { + if path.0.is_empty() { + return; + } + // absolute `path` replaces `self` + if path.is_absolute() { + self.0.truncate(0); + } else { + // trim fragment + if let Some(pos) = self.0.find('#') { + self.0.truncate(pos); + } + + // push missing seperator + if !self.0.is_empty() && !self.0.ends_with(AssetPath::is_seperator) { + self.0.push('/'); + } + } + self.0.reserve(path.0.len()); + for comp in path.components() { + match comp { + Component::RootDir => { + self.0.truncate(0); + self.0.push('/'); + } + Component::ParentDir => { + if !self.pop() && self.0.is_empty() { + self.0.push_str("../"); + } + } + Component::File(f) => self.0.push_str(f), + Component::Directory(f) => { + self.0.push_str(f); + self.0.push('/'); + } + Component::Fragment(f) => { + self.0.push('#'); + self.0.push_str(f); + } + } + } + } + + pub fn pop(&mut self) -> bool { + let Some(parent) = self.parent() else { + return false; + }; + let len = parent.0.trim_end_matches(AssetPath::is_seperator).len(); + self.0.truncate(len + 1); + true + } + + #[inline] + pub fn set_file_name>(&mut self, file_name: S) { + self._set_file_name(file_name.as_ref()) + } + + fn _set_file_name(&mut self, file_name: &str) { + if self.file_name().is_some() { + let popped = self.pop(); + debug_assert!(popped); + } + self.push(file_name); + } + + #[inline] + pub fn set_fragment>(&mut self, fragment: S) { + self._set_fragment(fragment.as_ref()) + } + + fn _set_fragment(&mut self, fragment: &str) { + if let Some(pos) = self.0.find('#') { + self.0.truncate(pos + 1); + } else { + self.0.push('#'); + } + self.0.push_str(fragment); + } + + #[inline] + pub fn into_string(self) -> String { + self.0 + } + + #[inline] + pub fn capacity(&self) -> usize { + self.0.capacity() + } + + #[inline] + pub fn clear(&mut self) { + self.0.clear() + } + + #[inline] + pub fn reserve(&mut self, additional: usize) { + self.0.reserve(additional) + } + + #[inline] + pub fn reserve_exact(&mut self, additional: usize) { + self.0.reserve_exact(additional) + } + + #[inline] + pub fn shrink_to_fit(&mut self) { + self.0.shrink_to_fit() + } +} + +impl Deref for AssetPathBuf { + type Target = AssetPath; + #[inline] + fn deref(&self) -> &Self::Target { + AssetPath::new(&self.0) + } +} + +impl> From<&T> for AssetPathBuf { + #[inline] + fn from(s: &T) -> Self { + Self::from(s.as_ref().to_string()) + } +} + +impl From for AssetPathBuf { + #[inline] + fn from(s: String) -> Self { + Self(s) + } +} +impl From for String { + #[inline] + fn from(path_buf: AssetPathBuf) -> Self { + path_buf.0 + } +} +impl FromStr for AssetPathBuf { + type Err = core::convert::Infallible; + + #[inline] + fn from_str(s: &str) -> Result { + Ok(Self::from(s)) + } +} + +impl Borrow for AssetPathBuf { + #[inline] + fn borrow(&self) -> &AssetPath { + self.deref() + } +} + +impl Default for AssetPathBuf { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl AssetPath { + #[inline] + fn is_seperator(c: char) -> bool { + c == '/' || c == '\\' + } + + #[inline] + pub fn new + ?Sized>(s: &S) -> &Self { + let s: *const str = s.as_ref(); + unsafe { &*(s as *const Self) } + } + + #[inline] + pub fn as_str(&self) -> &str { + &self.0 + } + + #[inline] + pub fn len(&self) -> usize { + self.0.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn to_path_buf(&self) -> AssetPathBuf { + AssetPathBuf::from(self.0.to_string()) + } + + #[inline] + pub fn is_absolute(&self) -> bool { + self.0.starts_with(Self::is_seperator) + } + + #[inline] + pub fn is_relative(&self) -> bool { + !self.is_absolute() + } + + #[inline] + pub fn is_directory(&self) -> bool { + let path = if let Some(pos) = self.0.find('#') { + &self.0[..pos] + } else { + &self.0 + }; + path.ends_with(Self::is_seperator) + } + + #[inline] + fn split_fragment(&self) -> (&str, Option<&str>) { + if let Some(pos) = self.0.find('#') { + (&self.0[..pos], Some(&self.0[pos + 1..])) + } else { + (&self.0, None) + } + } + + #[inline] + fn split_parent_fragment(&self) -> (Option<&str>, &str, Option<&str>) { + let (path, frag) = self.split_fragment(); + if let Some(pos) = path + .trim_end_matches(Self::is_seperator) + .rfind(Self::is_seperator) + { + let parent = &path[..pos + 1]; + (Some(parent), &path[parent.len()..], frag) + } else { + (None, path, frag) + } + } + + pub fn parent(&self) -> Option<&Self> { + let (parent, _file, _frag) = self.split_parent_fragment(); + parent.map(Self::new) + } + + pub fn file_name(&self) -> Option<&str> { + let (_parent, file, _frag) = self.split_parent_fragment(); + let file = file.trim_end_matches(Self::is_seperator); + if file.is_empty() { None } else { Some(file) } + } + + #[inline] + fn split_parent_file_extension_fragment( + &self, + ) -> (Option<&str>, Option<&str>, Option<&str>, Option<&str>) { + let (parent, file, frag) = self.split_parent_fragment(); + if file.ends_with(Self::is_seperator) { + let filename = file.trim_end_matches(Self::is_seperator); + if filename.is_empty() { + (parent, None, None, frag) + } else { + (parent, Some(filename), None, frag) + } + } else if file.starts_with('.') { + (parent, Some(file), None, frag) + } else if let Some(pos) = file.find('.') { + (parent, Some(&file[..pos]), Some(&file[pos..]), frag) + } else { + (parent, Some(file), None, frag) + } + } + + #[inline] + pub fn file_stem(&self) -> Option<&str> { + let (_, stem, _, _) = self.split_parent_file_extension_fragment(); + stem + } + + #[inline] + pub fn extension(&self) -> Option<&str> { + let (_, _, ext, _) = self.split_parent_file_extension_fragment(); + ext + } + + #[inline] + pub fn fragment(&self) -> Option<&str> { + let (_path, frag) = self.split_fragment(); + frag + } + + #[inline] + pub fn components(&self) -> Components<'_> { + Components { + path: &self.0, + state: State::Start, + } + } + + pub fn normalize(&self) -> AssetPathBuf { + let mut res = AssetPathBuf::new(); + res.push(self); + res + } + + pub fn iter(&self) -> Iter<'_> { + Iter(self.components()) + } + + pub fn join>(&self, path: P) -> AssetPathBuf { + self._join(path.as_ref()) + } + + fn _join(&self, path: &Self) -> AssetPathBuf { + let mut buf = if path.is_absolute() { + AssetPathBuf::new() + } else { + self.to_path_buf() + }; + buf.push(path); + buf + } + + pub fn with_file_name>(&self, file_name: S) -> AssetPathBuf { + self._with_file_name(file_name.as_ref()) + } + + fn _with_file_name(&self, file_name: &str) -> AssetPathBuf { + let mut buf = self.to_path_buf(); + buf.set_file_name(file_name); + buf + } + + pub fn with_fragment>(&self, fragment: S) -> AssetPathBuf { + self._with_fragment(fragment.as_ref()) + } + + fn _with_fragment(&self, fragment: &str) -> AssetPathBuf { + let mut buf = self.to_path_buf(); + buf.set_fragment(fragment); + buf + } +} + +impl std::fmt::Debug for AssetPath { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(&self.0, formatter) + } +} + +impl std::fmt::Display for AssetPath { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, formatter) + } +} + +impl ToOwned for AssetPath { + type Owned = AssetPathBuf; + #[inline] + fn to_owned(&self) -> Self::Owned { + self.to_path_buf() + } +} + +impl AsRef for AssetPath { + #[inline] + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl AsRef for AssetPath { + #[inline] + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl AsRef for AssetPath { + #[inline] + fn as_ref(&self) -> &Self { + self + } +} + +impl AsRef for AssetPathBuf { + #[inline] + fn as_ref(&self) -> &AssetPath { + self + } +} + +impl AsRef for AssetPathBuf { + #[inline] + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl AsRef for AssetPathBuf { + #[inline] + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl AsRef for str { + #[inline] + fn as_ref(&self) -> &AssetPath { + AssetPath::new(self) + } +} + +impl AsRef for Cow<'_, str> { + #[inline] + fn as_ref(&self) -> &AssetPath { + AssetPath::new(self) + } +} +impl AsRef for String { + #[inline] + fn as_ref(&self) -> &AssetPath { + AssetPath::new(self) + } +} + +impl<'a> IntoIterator for &'a AssetPathBuf { + type Item = &'a str; + type IntoIter = Iter<'a>; + #[inline] + fn into_iter(self) -> Iter<'a> { + self.iter() + } +} + +impl<'a> IntoIterator for &'a AssetPath { + type Item = &'a str; + type IntoIter = Iter<'a>; + #[inline] + fn into_iter(self) -> Iter<'a> { + self.iter() + } +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub enum Component<'a> { + RootDir, + ParentDir, + Directory(&'a str), + File(&'a str), + Fragment(&'a str), +} + +impl<'a> Component<'a> { + pub fn as_str(self) -> &'a str { + match self { + Component::RootDir => "/", + Component::ParentDir => "..", + Component::Directory(s) | Component::File(s) | Component::Fragment(s) => s, + } + } + + fn from_str(comp: &'a str, is_dir: bool) -> Option { + if comp.starts_with('#') { + Some(Component::Fragment(&comp[1..])) + } else if comp == ".." { + Some(Component::ParentDir) + } else if !comp.is_empty() && comp != "." { + if is_dir { + Some(Component::Directory(comp)) + } else { + Some(Component::File(comp)) + } + } else { + None + } + } +} + +impl AsRef for Component<'_> { + #[inline] + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl AsRef for Component<'_> { + #[inline] + fn as_ref(&self) -> &AssetPath { + self.as_str().as_ref() + } +} + +#[derive(Clone)] +pub struct Components<'a> { + // The path left to parse components from + path: &'a str, + + state: State, +} + +#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)] +enum State { + Start = 1, // / or . or nothing + Body = 2, // foo/bar/baz + Done = 4, +} + +impl<'a> Components<'a> { + pub fn as_path(&self) -> &'a AssetPath { + AssetPath::new(self.path) + } +} + +impl AsRef for Components<'_> { + #[inline] + fn as_ref(&self) -> &AssetPath { + self.as_path() + } +} + +impl AsRef for Components<'_> { + #[inline] + fn as_ref(&self) -> &str { + self.as_path().as_str() + } +} + +impl<'a> Iterator for Components<'a> { + type Item = Component<'a>; + + fn next(&mut self) -> Option> { + loop { + match self.state { + State::Done => return None, + State::Start => { + self.state = State::Body; + if self.path.starts_with(AssetPath::is_seperator) { + self.path = self.path.trim_start_matches(AssetPath::is_seperator); + return Some(Component::RootDir); + } + } + State::Body => { + if self.path.is_empty() { + self.state = State::Done; + return None; + } + if self.path.starts_with('#') { + self.state = State::Done; + return Some(Component::Fragment(&self.path[1..])); + } + let mut is_dir = false; + let comp; + if let Some(pos) = self.path.find(|c| AssetPath::is_seperator(c) || c == '#') { + comp = &self.path[..pos]; + self.path = &self.path[pos..]; + if self.path.starts_with(AssetPath::is_seperator) { + is_dir = true; + self.path = self.path.trim_start_matches(AssetPath::is_seperator); + } + } else { + // last component + self.state = State::Done; + comp = self.path; + } + if let Some(comp) = Component::from_str(comp, is_dir) { + return Some(comp); + } + } + } + } + } +} + +impl<'a> DoubleEndedIterator for Components<'a> { + fn next_back(&mut self) -> Option> { + loop { + match self.state { + State::Done => return None, + State::Start => { + self.state = State::Body; + if let Some(pos) = self.path.find('#') { + let frag = &self.path[pos + 1..]; + self.path = &self.path[..pos]; + return Some(Component::Fragment(frag)); + } + } + State::Body => { + let mut is_dir = false; + if self.path.ends_with(AssetPath::is_seperator) { + self.path = self.path.trim_end_matches(AssetPath::is_seperator); + is_dir = true; + } + if self.path.is_empty() { + self.state = State::Done; + if is_dir { + return Some(Component::RootDir); + } else { + return None; + } + } + + let comp; + if let Some(pos) = self.path.rfind(AssetPath::is_seperator) { + comp = &self.path[pos + 1..]; + self.path = &self.path[..pos + 1]; + } else { + // last component + comp = self.path; + self.state = State::Done; + } + if let Some(comp) = Component::from_str(comp, is_dir) { + return Some(comp); + } + } + } + } + } +} + +impl FusedIterator for Components<'_> {} + +#[derive(Clone)] +pub struct Iter<'a>(Components<'a>); + +impl<'a> Iter<'a> { + #[inline] + pub fn as_path(&self) -> &'a AssetPath { + self.0.as_path() + } +} + +impl AsRef for Iter<'_> { + #[inline] + fn as_ref(&self) -> &AssetPath { + self.as_path() + } +} + +impl AsRef for Iter<'_> { + #[inline] + fn as_ref(&self) -> &str { + self.as_path().as_str() + } +} + +impl<'a> Iterator for Iter<'a> { + type Item = &'a str; + #[inline] + fn next(&mut self) -> Option { + self.0.next().map(Component::as_str) + } +} + +impl<'a> DoubleEndedIterator for Iter<'a> { + #[inline] + fn next_back(&mut self) -> Option { + self.0.next_back().map(Component::as_str) + } +} + +impl FusedIterator for Iter<'_> {} diff --git a/crates/assets-loader/src/platform/android.rs b/crates/assets-loader/src/platform/android.rs new file mode 100644 index 0000000..2d2a7ef --- /dev/null +++ b/crates/assets-loader/src/platform/android.rs @@ -0,0 +1,77 @@ +use std::{borrow::Cow, ffi::CString, io::Read}; + +use ndk::asset::{Asset as AndroidAsset, AssetManager as AndroidAssetManager}; + +use crate::{ + path::{AssetPath, AssetPathBuf}, + platform::{AssetOpen, BoxedFuture}, +}; + +pub struct AndroidAssetLoaderIo { + manager: AndroidAssetManager, + base: Cow<'static, str>, +} + +fn clone_mgr(mgr: &AndroidAssetManager) -> AndroidAssetManager { + // SAFETY: pointer is a valid asset manager! + unsafe { AndroidAssetManager::from_ptr(mgr.ptr()) } +} + +impl AndroidAssetLoaderIo { + #[inline] + pub const fn with_asset_manager(manager: AndroidAssetManager) -> Self { + Self { + manager, + base: Cow::Borrowed(""), + } + } + + #[inline] + pub fn with_base(mut self, base: impl Into>) -> Self { + self.set_base(base); + self + } + + #[inline] + pub fn set_base(&mut self, base: impl Into>) -> &mut Self { + self.base = base.into(); + self + } + + fn resolve_path(&self, asset_path: &AssetPath) -> String { + let resolved_path = self.resolve_path(asset_path); + + // make absolute & normalize + let mut norm_asset_path = AssetPathBuf::with_capacity(asset_path.len() + 1); + norm_asset_path.push("/"); + norm_asset_path.push(asset_path); + + let base = self.base.trim_end_matches('/'); + let mut result = String::with_capacity(base.len() + asset_path.len()); + result.push_str(base); + result.push_str(&norm_asset_path.as_str()); + result + } + + async fn open(&self, asset_path: &AssetPath) -> std::io::Result> { + let resolved_path = self.resolve_path(asset_path); + let resolved_path = CString::new(resolved_path.into_bytes())?; + let manager = clone_mgr(&self.manager); + blocking::unblock(move || { + let mut asset = manager + .open(&resolved_path) + .ok_or(std::io::ErrorKind::NotFound)?; + let mut buf = Vec::with_capacity(asset.get_length()); + asset.read_to_end(&mut buf)?; + Ok(buf) + }) + .await + } +} + +impl AssetOpen for AndroidAssetLoaderIo { + fn load(&self, asset_path: &AssetPath) -> BoxedFuture<'_, std::io::Result>> { + let asset_path = asset_path.to_owned(); + Box::pin(async move { self.open(&asset_path).await }) + } +} diff --git a/crates/assets-loader/src/platform/fs.rs b/crates/assets-loader/src/platform/fs.rs new file mode 100644 index 0000000..0af240d --- /dev/null +++ b/crates/assets-loader/src/platform/fs.rs @@ -0,0 +1,105 @@ +use std::{ + fs::{File, Metadata}, + io::Read, + path::PathBuf, +}; + +use super::{AssetOpen, BoxedFuture}; +use crate::path::{AssetPath, Component}; + +pub struct FileSystemAssetLoaderIo { + dirs: Vec, +} + +fn join_path(base_path: &mut PathBuf, asset_path: &AssetPath) { + // TODO: check for invalid characters in path components (except fragment) ([a-zA-Z0-9._-]) + base_path.reserve_exact(asset_path.as_str().len()); + let mut num_comps = 0; // count components to not escape from `base_path` + for comp in asset_path.components() { + match comp { + Component::Directory(dir) | Component::File(dir) => { + num_comps += 1; + base_path.push(dir); + } + Component::ParentDir => { + if num_comps > 0 { + num_comps -= 1; + base_path.pop(); + } + } + Component::RootDir | Component::Fragment(_) => {} + } + } +} + +impl Default for FileSystemAssetLoaderIo { + fn default() -> Self { + Self::new() + } +} + +impl FileSystemAssetLoaderIo { + pub const fn new() -> Self { + Self { dirs: Vec::new() } + } + + #[inline] + pub fn with_directory(mut self, path: impl Into) -> Self { + self._push_directory(path.into()); + self + } + + #[inline] + pub fn push_directory(&mut self, path: impl Into) -> &mut Self { + self._push_directory(path.into()); + self + } + + fn _push_directory(&mut self, path: PathBuf) { + self.dirs.push(path); + } + + async fn resolve_path(&self, asset_path: &AssetPath) -> Option<(PathBuf, Metadata)> { + for base in &self.dirs { + let mut path = base.to_owned(); + join_path(&mut path, asset_path); + if let Some(r) = blocking::unblock(move || { + let metadata = path.metadata().ok()?; + Some((path, metadata)) + }) + .await + { + return Some(r); + } + } + None + } + + async fn open(&self, asset_path: &AssetPath) -> std::io::Result> { + let (full_path, metadata) = self + .resolve_path(asset_path) + .await + .ok_or(std::io::ErrorKind::NotFound)?; + let len = metadata.len(); + if len >= usize::MAX as u64 { + // TODO: smaller max file size? + // TODO: use this when stabelized + // return Err(std::io::ErrorKind::FileTooLarge.into()); + return Err(std::io::Error::other("file to large")); + } + blocking::unblock(move || { + let mut file = File::open(full_path)?; + let mut buf = Vec::with_capacity(len as usize); + file.read_to_end(&mut buf)?; + Ok(buf) + }) + .await + } +} + +impl AssetOpen for FileSystemAssetLoaderIo { + fn load(&self, asset_path: &AssetPath) -> BoxedFuture<'_, std::io::Result>> { + let asset_path = asset_path.to_owned(); + Box::pin(async move { self.open(&asset_path).await }) + } +} diff --git a/crates/assets-loader/src/platform/mod.rs b/crates/assets-loader/src/platform/mod.rs new file mode 100644 index 0000000..4578cb1 --- /dev/null +++ b/crates/assets-loader/src/platform/mod.rs @@ -0,0 +1,18 @@ +use std::{future::Future, pin::Pin}; + +use crate::path::AssetPath; + +#[cfg(not(target_arch = "wasm32"))] +pub mod fs; + +#[cfg(target_os = "android")] +pub mod android; + +#[cfg(target_arch = "wasm32")] +pub mod wasm; + +type BoxedFuture<'l, O> = Pin + Send + Sync + 'l>>; + +pub trait AssetOpen { + fn load(&self, asset_path: &AssetPath) -> BoxedFuture<'_, std::io::Result>>; +} diff --git a/crates/assets-loader/src/platform/wasm.rs b/crates/assets-loader/src/platform/wasm.rs new file mode 100644 index 0000000..d46168a --- /dev/null +++ b/crates/assets-loader/src/platform/wasm.rs @@ -0,0 +1,60 @@ +use std::io::Cursor; + +use js_sys::Uint8Array; +use wasm_bindgen_futures::JsFuture; + +pub struct WasmFetchAssetLoaderIo { + base: Cow<'static, str>, +} + +impl WasmFetchAssetLoaderIo { + pub const fn new() -> Self { + Self { + base: Cow::Borrowed("."), + } + } + + #[inline] + pub fn with_base(mut self, base: impl Into>) -> Self { + self.set_base(base); + self + } + + #[inline] + pub fn set_base(&mut self, base: impl Into>) -> &mut Self { + self.base = base.into(); + self + } + + fn resolve_url(&self, asset_path: &AssetPath) -> String { + // make absolute & normalize + let norm_asset_path = AssetPathBuf::with_capacity(asset_path.as_str().len() + 1); + norm_asset_path.push("/"); + norm_asset_path.push(asset_path); + + let base = self.base.trim_end_matches('/'); + let result = String::with_capacity(base.len() + asset_path.len()); + result.push_str(base); + result.push_str(&norm_asset_path); + result + } + + async fn open(&self, resolved_url: &str) -> std::io::Result> { + let window = web_sys::window().ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::Other, "window not available") + })?; + let resp: Response = JsFuture::from(window.fetch_with_str(resolved_url)) + .await? + .dyn_into()?; + let buffer = JsFuture::from(resp.array_buffer()?).await?; + let data = Uint8Array::new(&buffer).to_vec(); + Ok(data) + } +} + +impl AssetOpen for WasmFetchAssetLoaderIo { + fn open(&self, asset_path: AssetPathBuf) -> BoxedFuture<'_, std::io::Result>> { + let resolved_url = self.resolve_url(asset_path); + Box::pin(blocking::unblock(move || self.open(&resolved_url))) + } +} diff --git a/crates/assets/CHANGELOG.md b/crates/assets/CHANGELOG.md new file mode 100644 index 0000000..852b81f --- /dev/null +++ b/crates/assets/CHANGELOG.md @@ -0,0 +1,6 @@ +# `pulz-assets` Changelog +All notable changes to this crate will be documented in this file. + +## Unreleased (DATE) + + * Initial version diff --git a/crates/assets/Cargo.toml b/crates/assets/Cargo.toml new file mode 100644 index 0000000..8ed52f1 --- /dev/null +++ b/crates/assets/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pulz-assets" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +readme = "README.md" + +[dependencies] +pulz-schedule = { path = "../schedule" } + +slotmap = { workspace = true } diff --git a/crates/assets/README.md b/crates/assets/README.md new file mode 100644 index 0000000..6c2cb1e --- /dev/null +++ b/crates/assets/README.md @@ -0,0 +1,34 @@ +# `pulz-assets` + + + +[![Crates.io](https://img.shields.io/crates/v/pulz-assets.svg?label=pulz-assets)](https://crates.io/crates/pulz-assets) +[![docs.rs](https://docs.rs/pulz-assets/badge.svg)](https://docs.rs/pulz-assets/) +[![license: MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](#license) +[![Rust CI](https://github.com/HellButcher/pulz/actions/workflows/rust.yml/badge.svg)](https://github.com/HellButcher/pulz/actions/workflows/rust.yml) + + +**TODO** + +## Example + + +**TODO** + +## License + +[license]: #license + +This project is licensed under either of + +* MIT license ([LICENSE-MIT] or ) +* Apache License, Version 2.0, ([LICENSE-APACHE] or ) + +at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +[LICENSE-MIT]: ../../LICENSE-MIT +[LICENSE-APACHE]: ../../LICENSE-APACHE diff --git a/crates/assets/src/lib.rs b/crates/assets/src/lib.rs new file mode 100644 index 0000000..c72a1ab --- /dev/null +++ b/crates/assets/src/lib.rs @@ -0,0 +1,241 @@ +#![warn( + // missing_docs, + // rustdoc::missing_doc_code_examples, + future_incompatible, + rust_2018_idioms, + unused, + trivial_casts, + trivial_numeric_casts, + unused_lifetimes, + unused_qualifications, + unused_crate_dependencies, + clippy::cargo, + clippy::multiple_crate_versions, + clippy::empty_line_after_outer_attr, + clippy::fallible_impl_from, + clippy::redundant_pub_crate, + clippy::use_self, + clippy::suspicious_operation_groupings, + clippy::useless_let_if_seq, + // clippy::missing_errors_doc, + // clippy::missing_panics_doc, + clippy::wildcard_imports +)] +#![doc(html_logo_url = "https://raw.githubusercontent.com/HellButcher/pulz/master/docs/logo.png")] +#![doc(html_no_source)] +#![doc = include_str!("../README.md")] + +use std::{ + fmt::Debug, + hash::{Hash, Hasher}, + marker::PhantomData, +}; + +use pulz_schedule::{ + define_label_enum, + event::{EventWriter, Events}, + label::{CoreSystemPhase, SystemPhase}, + prelude::*, +}; +use slotmap::{Key, KeyData, SlotMap}; + +#[repr(transparent)] +pub struct Handle(KeyData, PhantomData T>); + +impl Copy for Handle {} +impl Clone for Handle { + #[inline] + fn clone(&self) -> Self { + Self(self.0, PhantomData) + } +} +impl Default for Handle { + #[inline] + fn default() -> Self { + Self(KeyData::default(), PhantomData) + } +} +impl Debug for Handle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple(&format!("Handle<{}>", ::std::any::type_name::())) + .field(&self.0) + .finish() + } +} +impl PartialEq for Handle { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} +impl Eq for Handle {} +impl PartialOrd for Handle { + fn partial_cmp(&self, other: &Self) -> Option { + self.0.partial_cmp(&other.0) + } +} +impl Ord for Handle { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.cmp(&other.0) + } +} +impl Hash for Handle { + fn hash(&self, state: &mut H) { + self.0.hash(state) + } +} +impl From for Handle { + #[inline] + fn from(k: KeyData) -> Self { + Self(k, PhantomData) + } +} +unsafe impl Key for Handle { + #[inline] + fn data(&self) -> KeyData { + self.0 + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AssetEvent { + Created(Handle), + Modified(Handle), + Removed(Handle), +} + +struct AssetEntry { + asset: T, + changed_since_last_update: bool, +} + +pub struct Assets { + map: SlotMap, AssetEntry>, + events: Vec>, +} + +impl Assets { + pub fn new() -> Self { + Self { + map: SlotMap::with_key(), + events: Vec::new(), + } + } + #[inline] + pub fn capacity(&self) -> usize { + self.map.capacity() + } + + #[inline] + pub fn reserve(&mut self, additional_capacity: usize) { + self.map.reserve(additional_capacity) + } + + #[inline] + pub fn len(&self) -> usize { + self.map.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.map.is_empty() + } + + pub fn clear(&mut self) { + for (handle, _) in self.map.drain() { + self.events.push(AssetEvent::Removed(handle)) + } + } + + #[inline] + pub fn insert(&mut self, asset: T) -> Handle { + let handle = self.map.insert(AssetEntry { + asset, + changed_since_last_update: true, + }); + self.events.push(AssetEvent::Created(handle)); + handle + } + + #[inline] + pub fn contains(&self, handle: Handle) -> bool { + self.map.contains_key(handle) + } + + #[inline] + pub fn get(&self, handle: Handle) -> Option<&T> { + Some(&self.map.get(handle)?.asset) + } + + #[inline] + pub fn get_mut(&mut self, handle: Handle) -> Option<&mut T> { + let entry = self.map.get_mut(handle)?; + if !entry.changed_since_last_update { + entry.changed_since_last_update = true; + self.events.push(AssetEvent::Modified(handle)); + } + Some(&mut entry.asset) + } + + #[inline] + pub fn remove(&mut self, handle: Handle) -> Option { + let entry = self.map.remove(handle)?; + self.events.push(AssetEvent::Removed(handle)); + Some(entry.asset) + } + + pub fn update(&mut self, mut events_writer: EventWriter<'_, AssetEvent>) { + for (_, entry) in self.map.iter_mut() { + entry.changed_since_last_update = false; + } + events_writer.send_batch(self.events.drain(..)) + } + + pub fn install_into(res: &mut Resources) + where + T: Send + Sync + 'static, + { + if res.try_init::().is_ok() { + Events::>::install_into(res); + let mut schedule = res.borrow_res_mut::().unwrap(); + // update assets after FIRST(events), and before UPDATE + schedule.add_phase_chain([ + AssetSystemPhase::LoadAssets.as_label(), + AssetSystemPhase::UpdateAssets.as_label(), + CoreSystemPhase::Update.as_label(), + ]); + schedule + .add_system(Self::update) + .into_phase(AssetSystemPhase::UpdateAssets); + } + } +} + +define_label_enum! { + pub enum AssetSystemPhase: SystemPhase { + LoadAssets, + UpdateAssets, + } +} + +impl Default for Assets { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl std::ops::Index> for Assets { + type Output = T; + #[inline] + fn index(&self, handle: Handle) -> &T { + self.get(handle).expect("invalid handle") + } +} + +impl std::ops::IndexMut> for Assets { + #[inline] + fn index_mut(&mut self, handle: Handle) -> &mut T { + self.get_mut(handle).expect("invalid handle") + } +} diff --git a/crates/input/CHANGELOG.md b/crates/input/CHANGELOG.md new file mode 100644 index 0000000..5ab2eec --- /dev/null +++ b/crates/input/CHANGELOG.md @@ -0,0 +1,6 @@ +# `pulz-input` Changelog +All notable changes to this crate will be documented in this file. + +## Unreleased (DATE) + + * Initial version diff --git a/crates/input/Cargo.toml b/crates/input/Cargo.toml new file mode 100644 index 0000000..65901b9 --- /dev/null +++ b/crates/input/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "pulz-input" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +readme = "README.md" + +[dependencies] +pulz-ecs = { path = "../ecs" } diff --git a/crates/input/README.md b/crates/input/README.md new file mode 100644 index 0000000..417d95b --- /dev/null +++ b/crates/input/README.md @@ -0,0 +1,34 @@ +# `pulz-input` + + + +[![Crates.io](https://img.shields.io/crates/v/pulz-input.svg?label=pulz-input)](https://crates.io/crates/pulz-input) +[![docs.rs](https://docs.rs/pulz-input/badge.svg)](https://docs.rs/pulz-input/) +[![license: MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](#license) +[![Rust CI](https://github.com/HellButcher/pulz/actions/workflows/rust.yml/badge.svg)](https://github.com/HellButcher/pulz/actions/workflows/rust.yml) + + +**TODO** + +## Example + + +**TODO** + +## License + +[license]: #license + +This project is licensed under either of + +* MIT license ([LICENSE-MIT] or ) +* Apache License, Version 2.0, ([LICENSE-APACHE] or ) + +at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +[LICENSE-MIT]: ../../LICENSE-MIT +[LICENSE-APACHE]: ../../LICENSE-APACHE diff --git a/crates/input/src/lib.rs b/crates/input/src/lib.rs new file mode 100644 index 0000000..c5cdfb4 --- /dev/null +++ b/crates/input/src/lib.rs @@ -0,0 +1,35 @@ +#![warn( + // missing_docs, + // rustdoc::missing_doc_code_examples, + future_incompatible, + rust_2018_idioms, + unused, + trivial_casts, + trivial_numeric_casts, + unused_lifetimes, + unused_qualifications, + unused_crate_dependencies, + clippy::cargo, + clippy::multiple_crate_versions, + clippy::empty_line_after_outer_attr, + clippy::fallible_impl_from, + clippy::redundant_pub_crate, + clippy::use_self, + clippy::suspicious_operation_groupings, + clippy::useless_let_if_seq, + // clippy::missing_errors_doc, + // clippy::missing_panics_doc, + clippy::wildcard_imports +)] +#![doc(html_logo_url = "https://raw.githubusercontent.com/HellButcher/pulz/master/docs/logo.png")] +#![doc(html_no_source)] +#![doc = include_str!("../README.md")] + +#[cfg(test)] +mod tests { + + #[test] + fn it_works() { + assert_eq!(2, 1 + 1); + } +} diff --git a/crates/render-ash/CHANGELOG.md b/crates/render-ash/CHANGELOG.md new file mode 100644 index 0000000..71d66ea --- /dev/null +++ b/crates/render-ash/CHANGELOG.md @@ -0,0 +1,6 @@ +# `pulz-render-ash` Changelog +All notable changes to this crate will be documented in this file. + +## Unreleased (DATE) + + * Initial version diff --git a/crates/render-ash/Cargo.toml b/crates/render-ash/Cargo.toml new file mode 100644 index 0000000..3d1a4c9 --- /dev/null +++ b/crates/render-ash/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "pulz-render-ash" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +readme = "README.md" + +[dependencies] +pulz-ecs = { path = "../ecs" } +pulz-window = { path = "../window" } +pulz-render = { path = "../render" } +pulz-bitset = { path = "../bitset" } +pulz-assets = { path = "../assets" } + +thiserror = { workspace = true } +tracing = { workspace = true } +bitflags = { workspace = true } +slotmap = { workspace = true } +serde = { workspace = true } +fnv = { workspace = true } +raw-window-handle = { workspace = true } +ash = "0.38" +scratchbuffer = "0.1.0-alpha.1" +gpu-alloc = "0.6" +gpu-alloc-ash = "0.7" +gpu-descriptor = "0.3" +crossbeam-queue = { workspace = true } + +[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] +raw-window-metal = "1.1" + +[dev-dependencies] +pulz-window-winit = { path = "../window-winit" } +pulz-render-pipeline-core = { path = "../render-pipeline-core" } +tracing-subscriber = { workspace = true } diff --git a/crates/render-ash/README.md b/crates/render-ash/README.md new file mode 100644 index 0000000..2f869a9 --- /dev/null +++ b/crates/render-ash/README.md @@ -0,0 +1,34 @@ +# `pulz-render-ash` + + + +[![Crates.io](https://img.shields.io/crates/v/pulz-render-ash.svg?label=pulz-render-ash)](https://crates.io/crates/pulz-render-ash) +[![docs.rs](https://docs.rs/pulz-render-ash/badge.svg)](https://docs.rs/pulz-render-ash/) +[![license: MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](#license) +[![Rust CI](https://github.com/HellButcher/pulz/actions/workflows/rust.yml/badge.svg)](https://github.com/HellButcher/pulz/actions/workflows/rust.yml) + + +**TODO** + +## Example + + +**TODO** + +## License + +[license]: #license + +This project is licensed under either of + +* MIT license ([LICENSE-MIT] or ) +* Apache License, Version 2.0, ([LICENSE-APACHE] or ) + +at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +[LICENSE-MIT]: ../../LICENSE-MIT +[LICENSE-APACHE]: ../../LICENSE-APACHE diff --git a/crates/render-ash/examples/render-ash-demo.rs b/crates/render-ash/examples/render-ash-demo.rs new file mode 100644 index 0000000..f51528d --- /dev/null +++ b/crates/render-ash/examples/render-ash-demo.rs @@ -0,0 +1,87 @@ +use std::error::Error; + +use pulz_ecs::prelude::*; +use pulz_render::camera::{Camera, RenderTarget}; +use pulz_render_ash::AshRenderer; +use pulz_render_pipeline_core::core_3d::CoreShadingModule; +use pulz_window::{WindowAttributes, WindowId, WindowModule}; +use pulz_window_winit::{Application, winit::event_loop::EventLoop}; +use tracing::*; + +fn init() -> Resources { + info!("Initializing..."); + let mut resources = Resources::new(); + resources.install(CoreShadingModule); + + /* + let (window_system, window_id, window) = + WinitWindowModule::new(WindowDescriptor::default(), &event_loop) + .unwrap() + .install(&mut resources); + */ + + resources.install(AshRenderer::new().unwrap()); + + // let mut schedule = resources.remove::().unwrap(); + // schedule.init(&mut resources); + // schedule.debug_dump_if_env(None).unwrap(); + // resources.insert_again(schedule); + + let windows = resources.install(WindowModule); + let window_id = windows.create(WindowAttributes::new()); + + setup_demo_scene(&mut resources, window_id); + + resources +} + +fn setup_demo_scene(resources: &mut Resources, window: WindowId) { + let mut world = resources.world_mut(); + + world + .spawn() + .insert(Camera::new()) + .insert(RenderTarget::Window(window)); +} + +#[cfg(not(target_arch = "wasm32"))] +fn main() -> Result<(), Box> { + use tracing_subscriber::{EnvFilter, fmt::format::FmtSpan}; + let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + tracing_subscriber::fmt() + .with_env_filter(env_filter) + .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE) + .init(); + + let event_loop = EventLoop::new().unwrap(); + let resources = init(); + let mut app = Application::new(resources); + event_loop.run_app(&mut app).map_err(Into::into) +} + +#[cfg(target_arch = "wasm32")] +fn main() { + use pulz_window_winit::winit::event_loop; + use wasm_bindgen::prelude::*; + use winit::platform::web::WindowExtWebSys; + + console_error_panic_hook::set_once(); + tracing_log::LogTracer::init().expect("unable to create log-tracer"); + tracing_wasm::set_as_global_default(); + + let event_loop = EventLoop::new().unwrap(); + let resources = init(); + let app = Application::new(resources); + + /* + let canvas = window.canvas(); + canvas.style().set_css_text("background-color: teal;"); + web_sys::window() + .and_then(|win| win.document()) + .and_then(|doc| doc.body()) + .and_then(|body| body.append_child(&web_sys::Element::from(canvas)).ok()) + .expect("couldn't append canvas to document body"); + */ + + event_loop.spawn_app(app); +} diff --git a/crates/render-ash/src/alloc.rs b/crates/render-ash/src/alloc.rs new file mode 100644 index 0000000..e0c148e --- /dev/null +++ b/crates/render-ash/src/alloc.rs @@ -0,0 +1,136 @@ +use std::{mem::ManuallyDrop, sync::Arc}; + +use ash::vk; + +use crate::{ + Result, + device::AshDevice, + instance::{AshInstance, VK_API_VERSION}, +}; + +type GpuAllocator = gpu_alloc::GpuAllocator; +pub type GpuMemoryBlock = gpu_alloc::MemoryBlock; +pub use gpu_alloc::AllocationError; + +pub struct AshAllocator { + device: Arc, + gpu_allocator: GpuAllocator, +} + +impl AshAllocator { + pub fn new(device: &Arc) -> Result { + let gpu_alloc_props = unsafe { + gpu_alloc_ash::device_properties( + device.instance(), + VK_API_VERSION, + device.physical_device(), + )? + }; + + // TODO: Config + let gpu_alloc_config = gpu_alloc::Config::i_am_potato(); + + Ok(Self { + device: device.clone(), + gpu_allocator: GpuAllocator::new(gpu_alloc_config, gpu_alloc_props), + }) + } + + #[inline] + pub fn instance(&self) -> &AshInstance { + self.device.instance() + } + + #[inline] + pub fn instance_arc(&self) -> Arc { + self.device.instance_arc() + } + + #[inline] + pub fn device(&self) -> &AshDevice { + &self.device + } + + #[inline] + pub fn device_arc(&self) -> Arc { + self.device.clone() + } + + #[inline] + pub unsafe fn alloc( + &mut self, + request: gpu_alloc::Request, + ) -> Result, AllocationError> { + unsafe { + let block = self + .gpu_allocator + .alloc(gpu_alloc_ash::AshMemoryDevice::wrap(&self.device), request)?; + Ok(AshMemoryBlockGuard { + allocator: self, + block: ManuallyDrop::new(block), + }) + } + } + + #[inline] + pub unsafe fn alloc_with_dedicated( + &mut self, + request: gpu_alloc::Request, + dedicated: gpu_alloc::Dedicated, + ) -> Result, AllocationError> { + unsafe { + let block = self.gpu_allocator.alloc_with_dedicated( + gpu_alloc_ash::AshMemoryDevice::wrap(&self.device), + request, + dedicated, + )?; + Ok(AshMemoryBlockGuard { + allocator: self, + block: ManuallyDrop::new(block), + }) + } + } + + #[inline] + pub unsafe fn dealloc(&mut self, block: GpuMemoryBlock) { + unsafe { + self.gpu_allocator + .dealloc(gpu_alloc_ash::AshMemoryDevice::wrap(&self.device), block) + } + } +} + +pub struct AshMemoryBlockGuard<'a> { + block: ManuallyDrop, + allocator: &'a mut AshAllocator, +} + +impl AshMemoryBlockGuard<'_> { + #[inline] + pub fn take(mut self) -> GpuMemoryBlock { + let block = unsafe { ManuallyDrop::take(&mut self.block) }; + std::mem::forget(self); + block + } +} +impl Drop for AshMemoryBlockGuard<'_> { + fn drop(&mut self) { + unsafe { + self.allocator.dealloc(ManuallyDrop::take(&mut self.block)); + } + } +} + +impl std::ops::Deref for AshMemoryBlockGuard<'_> { + type Target = GpuMemoryBlock; + #[inline] + fn deref(&self) -> &Self::Target { + &self.block + } +} +impl std::ops::DerefMut for AshMemoryBlockGuard<'_> { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.block + } +} diff --git a/crates/render-ash/src/convert.rs b/crates/render-ash/src/convert.rs new file mode 100644 index 0000000..780af2f --- /dev/null +++ b/crates/render-ash/src/convert.rs @@ -0,0 +1,1275 @@ +use ash::vk; +use pulz_bitset::BitSet; +use pulz_render::{ + buffer::{BufferDescriptor, BufferUsage}, + graph::{ + access::{Access, Stage}, + pass::PipelineBindPoint, + }, + math::{USize2, USize3}, + pipeline::{ + BindGroupLayoutDescriptor, BlendFactor, BlendOperation, CompareFunction, + ComputePipelineDescriptor, DepthStencilState, Face, FrontFace, GraphicsPassDescriptor, + GraphicsPipelineDescriptor, IndexFormat, LoadOp, PipelineLayoutDescriptor, PrimitiveState, + PrimitiveTopology, RayTracingPipelineDescriptor, StencilFaceState, StencilOperation, + StoreOp, VertexFormat, + }, + texture::{TextureAspects, TextureDescriptor, TextureDimensions, TextureFormat, TextureUsage}, +}; +use scratchbuffer::ScratchBuffer; + +use crate::resources::AshResources; + +pub trait VkFrom { + fn from(val: &T) -> Self; +} + +pub trait VkInto { + fn vk_into(&self) -> U; +} + +impl VkInto for T +where + U: VkFrom, +{ + #[inline] + fn vk_into(&self) -> U { + U::from(self) + } +} + +impl, V> VkFrom> for Option { + #[inline] + fn from(val: &Option) -> Self { + val.as_ref().map(T::from) + } +} + +impl VkFrom for vk::BufferCreateInfo<'static> { + #[inline] + fn from(val: &BufferDescriptor) -> Self { + Self::default() + .size(val.size as u64) + .usage(val.usage.vk_into()) + .sharing_mode(vk::SharingMode::EXCLUSIVE) + } +} + +impl VkFrom for vk::BufferUsageFlags { + #[inline] + fn from(val: &BufferUsage) -> Self { + let mut result = Self::empty(); + if val.intersects(BufferUsage::INDIRECT) { + result |= Self::INDIRECT_BUFFER; + } + if val.intersects(BufferUsage::INDEX) { + result |= Self::INDEX_BUFFER; + } + if val.intersects(BufferUsage::VERTEX) { + result |= Self::VERTEX_BUFFER; + } + if val.intersects(BufferUsage::UNIFORM) { + result |= Self::UNIFORM_BUFFER; + } + if val.intersects(BufferUsage::STORAGE) { + result |= Self::STORAGE_BUFFER; + } + if val.intersects(BufferUsage::UNIFORM_TEXEL) { + result |= Self::UNIFORM_TEXEL_BUFFER; + } + if val.intersects(BufferUsage::STORAGE_TEXEL) { + result |= Self::STORAGE_TEXEL_BUFFER; + } + if val.intersects(BufferUsage::TRANSFER_READ) { + result |= Self::TRANSFER_SRC; + } + if val.intersects(BufferUsage::TRANSFER_WRITE) { + result |= Self::TRANSFER_DST; + } + if val.intersects(BufferUsage::ACCELERATION_STRUCTURE_STORAGE) { + result |= Self::ACCELERATION_STRUCTURE_STORAGE_KHR; + } + if val.intersects(BufferUsage::ACCELERATION_STRUCTURE_BUILD_INPUT) { + result |= Self::ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_KHR; + } + if val.intersects(BufferUsage::SHADER_BINDING_TABLE) { + result |= Self::SHADER_BINDING_TABLE_KHR; + } + result + } +} + +fn get_array_layers(dimensions: &TextureDimensions) -> u32 { + match dimensions { + TextureDimensions::Cube(_) => 6, + TextureDimensions::D2Array { array_len, .. } => *array_len, + TextureDimensions::CubeArray { array_len, .. } => *array_len * 6, + _ => 1, + } +} + +impl VkFrom for vk::ImageCreateInfo<'static> { + fn from(val: &TextureDescriptor) -> Self { + Self::default() + .image_type(val.dimensions.vk_into()) + .format(val.format.vk_into()) + .extent(val.dimensions.vk_into()) + .array_layers(get_array_layers(&val.dimensions)) + .mip_levels(val.mip_level_count) + .samples(vk::SampleCountFlags::from_raw(val.sample_count as u32)) + .usage(val.usage.vk_into()) + .tiling(vk::ImageTiling::OPTIMAL) + .sharing_mode(vk::SharingMode::EXCLUSIVE) + .initial_layout(vk::ImageLayout::UNDEFINED) + } +} + +impl VkFrom for vk::ImageViewCreateInfo<'static> { + fn from(val: &TextureDescriptor) -> Self { + Self::default() + .view_type(val.dimensions.vk_into()) + .format(val.format.vk_into()) + .subresource_range( + vk::ImageSubresourceRange::default() + .aspect_mask(val.aspects().vk_into()) + .base_mip_level(0) + .level_count(1) + .base_array_layer(0) + .layer_count(get_array_layers(&val.dimensions)), + ) + } +} + +impl VkFrom for vk::Extent3D { + #[inline] + fn from(val: &TextureDescriptor) -> Self { + val.dimensions.vk_into() + } +} + +impl VkFrom for vk::Extent3D { + #[inline] + fn from(val: &TextureDimensions) -> Self { + match *val { + TextureDimensions::D1(len) => Self { + width: len, + height: 1, + depth: 1, + }, + TextureDimensions::D2(size) => Self { + width: size.x, + height: size.y, + depth: 1, + }, + TextureDimensions::D2Array { size, array_len: _ } => Self { + width: size.x, + height: size.y, + depth: 1, + }, + TextureDimensions::Cube(size) => Self { + width: size.x, + height: size.y, + depth: 1, + }, + TextureDimensions::CubeArray { size, array_len: _ } => Self { + width: size.x, + height: size.y, + depth: 1, + }, + TextureDimensions::D3(size) => Self { + width: size.x, + height: size.y, + depth: size.z, + }, + } + } +} + +impl VkFrom for vk::ImageAspectFlags { + #[inline] + fn from(val: &TextureAspects) -> Self { + let mut result = Self::empty(); + if val.contains(TextureAspects::COLOR) { + result |= Self::COLOR; + } + if val.contains(TextureAspects::DEPTH) { + result |= Self::DEPTH; + } + if val.contains(TextureAspects::STENCIL) { + result |= Self::STENCIL; + } + result + } +} + +impl VkFrom for vk::ImageViewType { + #[inline] + fn from(val: &TextureDimensions) -> Self { + match val { + TextureDimensions::D1(_) => Self::TYPE_1D, + TextureDimensions::D2 { .. } => Self::TYPE_2D, + TextureDimensions::D2Array { .. } => Self::TYPE_2D_ARRAY, + TextureDimensions::Cube { .. } => Self::CUBE, + TextureDimensions::CubeArray { .. } => Self::CUBE_ARRAY, + TextureDimensions::D3 { .. } => Self::TYPE_3D, + } + } +} + +impl VkFrom for vk::ImageType { + #[inline] + fn from(val: &TextureDimensions) -> Self { + match val { + TextureDimensions::D1(_) => Self::TYPE_1D, + TextureDimensions::D2 { .. } + | TextureDimensions::D2Array { .. } + | TextureDimensions::Cube { .. } + | TextureDimensions::CubeArray { .. } => Self::TYPE_2D, + TextureDimensions::D3 { .. } => Self::TYPE_3D, + } + } +} + +impl VkFrom for vk::Extent3D { + #[inline] + fn from(val: &USize3) -> Self { + Self { + width: val.x, + height: val.y, + depth: val.z, + } + } +} + +impl VkFrom<[u32; 3]> for vk::Extent3D { + #[inline] + fn from(val: &[u32; 3]) -> Self { + Self { + width: val[0], + height: val[1], + depth: val[2], + } + } +} + +impl VkFrom for vk::Extent2D { + #[inline] + fn from(val: &USize2) -> Self { + Self { + width: val.x, + height: val.y, + } + } +} + +impl VkFrom<[u32; 2]> for vk::Extent2D { + #[inline] + fn from(val: &[u32; 2]) -> Self { + Self { + width: val[0], + height: val[1], + } + } +} + +impl VkFrom for USize2 { + #[inline] + fn from(val: &vk::Extent2D) -> Self { + Self::new(val.width, val.height) + } +} + +impl VkFrom for vk::Format { + #[inline] + fn from(val: &TextureFormat) -> Self { + match val { + // 8-bit formats + TextureFormat::R8Unorm => Self::R8_UNORM, + TextureFormat::R8Snorm => Self::R8_SNORM, + TextureFormat::R8Uint => Self::R8_UINT, + TextureFormat::R8Sint => Self::R8_SINT, + + // 16-bit formats + TextureFormat::R16Uint => Self::R16_UINT, + TextureFormat::R16Sint => Self::R16_SINT, + TextureFormat::R16Float => Self::R16_SFLOAT, + TextureFormat::Rg8Unorm => Self::R8G8_UNORM, + TextureFormat::Rg8Snorm => Self::R8G8_SNORM, + TextureFormat::Rg8Uint => Self::R8G8_UINT, + TextureFormat::Rg8Sint => Self::R8G8_SINT, + + // 32-bit formats + TextureFormat::R32Uint => Self::R32_UINT, + TextureFormat::R32Sint => Self::R32_SINT, + TextureFormat::R32Float => Self::R32_SFLOAT, + TextureFormat::Rg16Uint => Self::R16G16_UINT, + TextureFormat::Rg16Sint => Self::R16G16_SINT, + TextureFormat::Rg16Float => Self::R16G16_SFLOAT, + TextureFormat::Rgba8Unorm => Self::R8G8B8A8_UNORM, + TextureFormat::Rgba8UnormSrgb => Self::R8G8B8A8_SRGB, + TextureFormat::Rgba8Snorm => Self::R8G8B8A8_SNORM, + TextureFormat::Rgba8Uint => Self::R8G8B8A8_UINT, + TextureFormat::Rgba8Sint => Self::R8G8B8A8_SINT, + TextureFormat::Bgra8Unorm => Self::B8G8R8A8_UNORM, + TextureFormat::Bgra8UnormSrgb => Self::B8G8R8A8_SRGB, + + // Packed 32-bit formats + TextureFormat::Rgb9E5Ufloat => Self::E5B9G9R9_UFLOAT_PACK32, + TextureFormat::Rgb10A2Unorm => Self::A2R10G10B10_UNORM_PACK32, + TextureFormat::Rg11B10Float => Self::B10G11R11_UFLOAT_PACK32, + + // 64-bit formats + TextureFormat::Rg32Uint => Self::R32G32_UINT, + TextureFormat::Rg32Sint => Self::R32G32_SINT, + TextureFormat::Rg32Float => Self::R32G32_SFLOAT, + TextureFormat::Rgba16Uint => Self::R16G16B16A16_UINT, + TextureFormat::Rgba16Sint => Self::R16G16B16A16_SINT, + TextureFormat::Rgba16Float => Self::R16G16B16A16_SFLOAT, + + // 128-bit formats + TextureFormat::Rgba32Uint => Self::R32G32B32A32_UINT, + TextureFormat::Rgba32Sint => Self::R32G32B32A32_SINT, + TextureFormat::Rgba32Float => Self::R32G32B32A32_SFLOAT, + + // Depth and stencil formats + TextureFormat::Stencil8 => Self::S8_UINT, + TextureFormat::Depth16Unorm => Self::D16_UNORM, + TextureFormat::Depth24Plus => Self::X8_D24_UNORM_PACK32, + TextureFormat::Depth24PlusStencil8 => Self::D24_UNORM_S8_UINT, + TextureFormat::Depth32Float => Self::D32_SFLOAT, + + _ => panic!("unsupported texture format: {val:?}"), + } + } +} + +impl VkFrom for TextureFormat { + #[inline] + fn from(val: &vk::Format) -> Self { + use vk::Format; + match *val { + // 8-bit formats + Format::R8_UNORM => Self::R8Unorm, + Format::R8_SNORM => Self::R8Snorm, + Format::R8_UINT => Self::R8Uint, + Format::R8_SINT => Self::R8Sint, + + // 16-bit formats + Format::R16_UINT => Self::R16Uint, + Format::R16_SINT => Self::R16Sint, + Format::R16_SFLOAT => Self::R16Float, + Format::R8G8_UNORM => Self::Rg8Unorm, + Format::R8G8_SNORM => Self::Rg8Snorm, + Format::R8G8_UINT => Self::Rg8Uint, + Format::R8G8_SINT => Self::Rg8Sint, + + // 32-bit formats + Format::R32_UINT => Self::R32Uint, + Format::R32_SINT => Self::R32Sint, + Format::R32_SFLOAT => Self::R32Float, + Format::R16G16_UINT => Self::Rg16Uint, + Format::R16G16_SINT => Self::Rg16Sint, + Format::R16G16_SFLOAT => Self::Rg16Float, + Format::R8G8B8A8_UNORM => Self::Rgba8Unorm, + Format::R8G8B8A8_SRGB => Self::Rgba8UnormSrgb, + Format::R8G8B8A8_SNORM => Self::Rgba8Snorm, + Format::R8G8B8A8_UINT => Self::Rgba8Uint, + Format::R8G8B8A8_SINT => Self::Rgba8Sint, + Format::B8G8R8A8_UNORM => Self::Bgra8Unorm, + Format::B8G8R8A8_SRGB => Self::Bgra8UnormSrgb, + + // Packed 32-bit formats + Format::E5B9G9R9_UFLOAT_PACK32 => Self::Rgb9E5Ufloat, + Format::A2R10G10B10_UNORM_PACK32 => Self::Rgb10A2Unorm, + Format::B10G11R11_UFLOAT_PACK32 => Self::Rg11B10Float, + + // 64-bit formats + Format::R32G32_UINT => Self::Rg32Uint, + Format::R32G32_SINT => Self::Rg32Sint, + Format::R32G32_SFLOAT => Self::Rg32Float, + Format::R16G16B16A16_UINT => Self::Rgba16Uint, + Format::R16G16B16A16_SINT => Self::Rgba16Sint, + Format::R16G16B16A16_SFLOAT => Self::Rgba16Float, + + // 128-bit formats + Format::R32G32B32A32_UINT => Self::Rgba32Uint, + Format::R32G32B32A32_SINT => Self::Rgba32Sint, + Format::R32G32B32A32_SFLOAT => Self::Rgba32Float, + + // Depth and stencil formats + Format::S8_UINT => Self::Stencil8, + Format::D16_UNORM => Self::Depth16Unorm, + Format::X8_D24_UNORM_PACK32 => Self::Depth24Plus, + Format::D24_UNORM_S8_UINT => Self::Depth24PlusStencil8, + Format::D32_SFLOAT => Self::Depth32Float, + + _ => panic!("unsupported texture format: {:x}", val.as_raw()), + } + } +} + +pub fn default_clear_value_for_format(format: vk::Format) -> vk::ClearValue { + match format { + // Depth and stencil formats + vk::Format::S8_UINT + | vk::Format::D16_UNORM + | vk::Format::X8_D24_UNORM_PACK32 + | vk::Format::D24_UNORM_S8_UINT + | vk::Format::D32_SFLOAT => vk::ClearValue { + depth_stencil: vk::ClearDepthStencilValue { + depth: 1.0, + stencil: 0, + }, + }, + + _ => vk::ClearValue { + color: default_clear_color_value_for_format(format), + }, + } +} + +pub fn default_clear_color_value_for_format(format: vk::Format) -> vk::ClearColorValue { + match format { + vk::Format::R8_SINT + | vk::Format::R8G8_SINT + | vk::Format::R8G8B8_SINT + | vk::Format::B8G8R8_SINT + | vk::Format::R8G8B8A8_SINT + | vk::Format::B8G8R8A8_SINT + | vk::Format::A8B8G8R8_SINT_PACK32 + | vk::Format::A2R10G10B10_SINT_PACK32 + | vk::Format::A2B10G10R10_SINT_PACK32 + | vk::Format::R16_SINT + | vk::Format::R16G16_SINT + | vk::Format::R16G16B16_SINT + | vk::Format::R16G16B16A16_SINT + | vk::Format::R32_SINT + | vk::Format::R32G32_SINT + | vk::Format::R32G32B32_SINT + | vk::Format::R32G32B32A32_SINT + | vk::Format::R64_SINT + | vk::Format::R64G64_SINT + | vk::Format::R64G64B64_SINT + | vk::Format::R64G64B64A64_SINT => vk::ClearColorValue { + int32: [i32::MIN, i32::MIN, i32::MIN, i32::MAX], + }, + + vk::Format::R8_UINT + | vk::Format::R8G8_UINT + | vk::Format::R8G8B8_UINT + | vk::Format::B8G8R8_UINT + | vk::Format::R8G8B8A8_UINT + | vk::Format::B8G8R8A8_UINT + | vk::Format::A8B8G8R8_UINT_PACK32 + | vk::Format::A2R10G10B10_UINT_PACK32 + | vk::Format::A2B10G10R10_UINT_PACK32 + | vk::Format::R16_UINT + | vk::Format::R16G16_UINT + | vk::Format::R16G16B16_UINT + | vk::Format::R16G16B16A16_UINT + | vk::Format::R32_UINT + | vk::Format::R32G32_UINT + | vk::Format::R32G32B32_UINT + | vk::Format::R32G32B32A32_UINT + | vk::Format::R64_UINT + | vk::Format::R64G64_UINT + | vk::Format::R64G64B64_UINT + | vk::Format::R64G64B64A64_UINT => vk::ClearColorValue { + uint32: [0, 0, 0, u32::MAX], + }, + + _ => vk::ClearColorValue { + float32: [0.0, 0.0, 0.0, 1.0], + }, + } +} + +impl VkFrom for vk::Format { + #[inline] + fn from(val: &VertexFormat) -> Self { + match val { + VertexFormat::Uint8x2 => Self::R8G8_UINT, + VertexFormat::Uint8x4 => Self::R8G8B8A8_UINT, + VertexFormat::Sint8x2 => Self::R8G8_SINT, + VertexFormat::Sint8x4 => Self::R8G8B8A8_SINT, + VertexFormat::Unorm8x2 => Self::R8G8_UNORM, + VertexFormat::Unorm8x4 => Self::R8G8B8A8_UNORM, + VertexFormat::Snorm8x2 => Self::R8G8_SNORM, + VertexFormat::Snorm8x4 => Self::R8G8B8A8_SNORM, + VertexFormat::Uint16x2 => Self::R16G16_UINT, + VertexFormat::Uint16x4 => Self::R16G16B16A16_UINT, + VertexFormat::Sint16x2 => Self::R16G16_SINT, + VertexFormat::Sint16x4 => Self::R16G16B16A16_SINT, + VertexFormat::Unorm16x2 => Self::R16G16_UNORM, + VertexFormat::Unorm16x4 => Self::R16G16B16A16_UNORM, + VertexFormat::Snorm16x2 => Self::R16G16_SNORM, + VertexFormat::Snorm16x4 => Self::R16G16B16A16_SNORM, + VertexFormat::Uint32 => Self::R32_UINT, + VertexFormat::Uint32x2 => Self::R32G32_UINT, + VertexFormat::Uint32x3 => Self::R32G32B32_UINT, + VertexFormat::Uint32x4 => Self::R32G32B32A32_UINT, + VertexFormat::Sint32 => Self::R32_SINT, + VertexFormat::Sint32x2 => Self::R32G32_SINT, + VertexFormat::Sint32x3 => Self::R32G32B32_SINT, + VertexFormat::Sint32x4 => Self::R32G32B32A32_SINT, + VertexFormat::Float16 => Self::R16_SFLOAT, + VertexFormat::Float16x2 => Self::R16G16_SFLOAT, + VertexFormat::Float16x4 => Self::R16G16B16A16_SFLOAT, + VertexFormat::Float32 => Self::R32_SFLOAT, + VertexFormat::Float32x2 => Self::R32G32_SFLOAT, + VertexFormat::Float32x3 => Self::R32G32B32_SFLOAT, + VertexFormat::Float32x4 => Self::R32G32B32A32_SFLOAT, + VertexFormat::Float64 => Self::R64_SFLOAT, + VertexFormat::Float64x2 => Self::R64G64_SFLOAT, + VertexFormat::Float64x3 => Self::R64G64B64_SFLOAT, + VertexFormat::Float64x4 => Self::R64G64B64A64_SFLOAT, + + _ => panic!("unsupported vertex format: {val:?}"), + } + } +} + +impl VkFrom for vk::ImageUsageFlags { + fn from(val: &TextureUsage) -> Self { + let mut result = Self::empty(); + if val.intersects(TextureUsage::INPUT_ATTACHMENT) { + result |= Self::INPUT_ATTACHMENT; + } + if val.intersects(TextureUsage::COLOR_ATTACHMENT) { + result |= Self::COLOR_ATTACHMENT; + } + if val.intersects(TextureUsage::DEPTH_STENCIL_ATTACHMENT) { + result |= Self::DEPTH_STENCIL_ATTACHMENT; + } + if val.intersects(TextureUsage::TRANSFER_READ) { + result |= Self::TRANSFER_SRC; + } + if val.intersects(TextureUsage::TRANSFER_WRITE) { + result |= Self::TRANSFER_DST; + } + if val.intersects(TextureUsage::SAMPLED) { + result |= Self::SAMPLED; + } + if val.intersects(TextureUsage::STORAGE) { + result |= Self::STORAGE; + } + result + } +} + +impl VkFrom for vk::ImageLayout { + #[inline] + fn from(val: &Access) -> Self { + let mut num = 0; + let mut r = Self::UNDEFINED; + if val.intersects(Access::COLOR_ATTACHMENT_READ | Access::COLOR_ATTACHMENT_WRITE) { + r = Self::COLOR_ATTACHMENT_OPTIMAL; + num += 1; + } + if val.intersects(Access::DEPTH_STENCIL_ATTACHMENT_WRITE) { + r = Self::DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + // TODO: single write variants + num += 1; + } else if val.intersects(Access::DEPTH_STENCIL_ATTACHMENT_READ) { + r = Self::DEPTH_STENCIL_READ_ONLY_OPTIMAL; + num += 1; + } + if val.intersects(Access::TRANSFER_READ) { + r = Self::TRANSFER_SRC_OPTIMAL; + num += 1; + } + if val.intersects(Access::TRANSFER_WRITE) { + r = Self::TRANSFER_DST_OPTIMAL; + num += 1; + } + if val.intersects(Access::PRESENT) { + r = Self::PRESENT_SRC_KHR; + num += 1; + } + if num <= 1 { + return r; + } + if !val.intersects(Access::ANY_WRITE) { + Self::READ_ONLY_OPTIMAL + } else { + Self::GENERAL + } + } +} + +impl VkFrom for vk::AccessFlags { + #[inline] + fn from(val: &Access) -> Self { + let mut result = Self::empty(); + if *val == Access::GENERAL { + return Self::MEMORY_READ | Self::MEMORY_WRITE; + } else if *val == Access::MEMORY_READ { + return Self::MEMORY_READ; + } else if *val == Access::MEMORY_WRITE { + return Self::MEMORY_WRITE; + } + if val.intersects(Access::INDIRECT_COMMAND_READ) { + result |= Self::INDIRECT_COMMAND_READ; + } + if val.intersects(Access::INDEX_READ) { + result |= Self::INDEX_READ; + } + if val.intersects(Access::VERTEX_ATTRIBUTE_READ) { + result |= Self::VERTEX_ATTRIBUTE_READ; + } + if val.intersects( + Access::COLOR_INPUT_ATTACHMENT_READ | Access::DEPTH_STENCIL_INPUT_ATTACHMENT_READ, + ) { + result |= Self::INPUT_ATTACHMENT_READ; + } + if val.intersects(Access::UNIFORM_READ) { + result |= Self::UNIFORM_READ; + } + if val.intersects(Access::SHADER_READ | Access::SAMPLED_READ) { + result |= Self::SHADER_READ; + } + if val.intersects(Access::COLOR_ATTACHMENT_READ) { + result |= Self::COLOR_ATTACHMENT_READ; + } + if val.intersects(Access::COLOR_ATTACHMENT_WRITE) { + result |= Self::COLOR_ATTACHMENT_WRITE; + } + if val.intersects(Access::DEPTH_STENCIL_ATTACHMENT_READ) { + result |= Self::DEPTH_STENCIL_ATTACHMENT_READ; + } + if val.intersects(Access::DEPTH_STENCIL_ATTACHMENT_WRITE) { + result |= Self::DEPTH_STENCIL_ATTACHMENT_WRITE; + } + if val.intersects(Access::TRANSFER_READ) { + result |= Self::TRANSFER_READ; + } + if val.intersects(Access::TRANSFER_WRITE) { + result |= Self::TRANSFER_WRITE; + } + if val.intersects(Access::HOST_READ) { + result |= Self::HOST_READ; + } + if val.intersects(Access::HOST_WRITE) { + result |= Self::HOST_WRITE; + } + if val.intersects( + Access::ACCELERATION_STRUCTURE_READ | Access::ACCELERATION_STRUCTURE_BUILD_READ, + ) { + result |= Self::ACCELERATION_STRUCTURE_READ_KHR; + } + if val.intersects(Access::ACCELERATION_STRUCTURE_BUILD_WRITE) { + result |= Self::ACCELERATION_STRUCTURE_WRITE_KHR; + } + result + } +} + +impl VkFrom for vk::PipelineBindPoint { + #[inline] + fn from(val: &PipelineBindPoint) -> Self { + match val { + PipelineBindPoint::Graphics => Self::GRAPHICS, + PipelineBindPoint::Compute => Self::COMPUTE, + PipelineBindPoint::RayTracing => Self::RAY_TRACING_KHR, + } + } +} + +impl VkFrom for vk::PipelineStageFlags { + #[inline] + fn from(val: &Stage) -> Self { + let mut result = Self::empty(); + if val.contains(Stage::DRAW_INDIRECT) { + result |= Self::DRAW_INDIRECT; + } + if val.contains(Stage::VERTEX_INPUT) { + result |= Self::VERTEX_INPUT; + } + if val.contains(Stage::VERTEX_SHADER) { + result |= Self::VERTEX_SHADER; + } + if val.contains(Stage::TESSELLATION_CONTROL_SHADER) { + result |= Self::TESSELLATION_CONTROL_SHADER; + } + if val.contains(Stage::TESSELLATION_EVALUATION_SHADER) { + result |= Self::TESSELLATION_EVALUATION_SHADER; + } + if val.contains(Stage::GEOMETRY_SHADER) { + result |= Self::GEOMETRY_SHADER; + } + if val.contains(Stage::FRAGMENT_SHADER) { + result |= Self::FRAGMENT_SHADER; + } + if val.contains(Stage::EARLY_FRAGMENT_TESTS) { + result |= Self::EARLY_FRAGMENT_TESTS; + } + if val.contains(Stage::LATE_FRAGMENT_TESTS) { + result |= Self::LATE_FRAGMENT_TESTS; + } + if val.contains(Stage::COLOR_ATTACHMENT_OUTPUT) { + result |= Self::COLOR_ATTACHMENT_OUTPUT; + } + if val.contains(Stage::COMPUTE_SHADER) { + result |= Self::COMPUTE_SHADER; + } + if val.contains(Stage::ACCELERATION_STRUCTURE_BUILD) { + result |= Self::ACCELERATION_STRUCTURE_BUILD_KHR; + } + if val.contains(Stage::RAY_TRACING_SHADER) { + result |= Self::RAY_TRACING_SHADER_KHR; + } + if val.contains(Stage::TRANSFER) { + result |= Self::TRANSFER; + } + if val.contains(Stage::HOST) { + result |= Self::HOST; + } + result + } +} + +impl VkFrom for vk::PipelineInputAssemblyStateCreateInfo<'static> { + #[inline] + fn from(val: &PrimitiveState) -> Self { + Self::default().topology(val.topology.vk_into()) + } +} + +impl VkFrom for vk::PipelineRasterizationStateCreateInfo<'static> { + #[inline] + fn from(val: &PrimitiveState) -> Self { + let mut r = Self::default() + .polygon_mode(vk::PolygonMode::FILL) + .front_face(val.front_face.vk_into()) + .line_width(1.0); + if let Some(cull_mode) = val.cull_mode { + r = r.cull_mode(cull_mode.vk_into()) + } + r + } +} + +impl VkFrom for vk::PipelineDepthStencilStateCreateInfo<'static> { + #[inline] + fn from(val: &DepthStencilState) -> Self { + let mut r = Self::default(); + if val.is_depth_enabled() { + r = r + .depth_test_enable(true) + .depth_write_enable(val.depth.write_enabled) + .depth_compare_op(val.depth.compare.vk_into()); + } + if val.stencil.is_enabled() { + r = r + .stencil_test_enable(true) + .front(val.stencil.front.vk_into()) + .back(val.stencil.back.vk_into()); + } + r + } +} + +impl VkFrom for vk::StencilOpState { + #[inline] + fn from(val: &StencilFaceState) -> Self { + Self { + compare_op: val.compare.vk_into(), + fail_op: val.fail_op.vk_into(), + depth_fail_op: val.depth_fail_op.vk_into(), + pass_op: val.pass_op.vk_into(), + compare_mask: !0, + write_mask: !0, + reference: !0, + } + } +} + +impl VkFrom for vk::PrimitiveTopology { + #[inline] + fn from(val: &PrimitiveTopology) -> Self { + match val { + PrimitiveTopology::PointList => Self::POINT_LIST, + PrimitiveTopology::LineList => Self::LINE_LIST, + PrimitiveTopology::LineStrip => Self::LINE_STRIP, + PrimitiveTopology::TriangleList => Self::TRIANGLE_LIST, + PrimitiveTopology::TriangleStrip => Self::TRIANGLE_STRIP, + } + } +} + +impl VkFrom for vk::FrontFace { + #[inline] + fn from(val: &FrontFace) -> Self { + match val { + FrontFace::CounterClockwise => Self::COUNTER_CLOCKWISE, + FrontFace::Clockwise => Self::CLOCKWISE, + } + } +} + +impl VkFrom for vk::CullModeFlags { + #[inline] + fn from(val: &Face) -> Self { + match val { + Face::Front => Self::FRONT, + Face::Back => Self::BACK, + } + } +} + +impl VkFrom for vk::BlendOp { + #[inline] + fn from(val: &BlendOperation) -> Self { + match val { + BlendOperation::Add => Self::ADD, + BlendOperation::Subtract => Self::SUBTRACT, + BlendOperation::ReverseSubtract => Self::REVERSE_SUBTRACT, + BlendOperation::Min => Self::MIN, + BlendOperation::Max => Self::MAX, + } + } +} + +impl VkFrom for vk::BlendFactor { + #[inline] + fn from(val: &BlendFactor) -> Self { + match val { + BlendFactor::Zero => Self::ZERO, + BlendFactor::One => Self::ONE, + BlendFactor::Src => Self::SRC_COLOR, + BlendFactor::OneMinusSrc => Self::ONE_MINUS_SRC_COLOR, + BlendFactor::SrcAlpha => Self::SRC_ALPHA, + BlendFactor::OneMinusSrcAlpha => Self::ONE_MINUS_SRC_ALPHA, + BlendFactor::Dst => Self::DST_COLOR, + BlendFactor::OneMinusDst => Self::ONE_MINUS_DST_COLOR, + BlendFactor::DstAlpha => Self::DST_ALPHA, + BlendFactor::OneMinusDstAlpha => Self::ONE_MINUS_DST_ALPHA, + BlendFactor::SrcAlphaSaturated => Self::SRC_ALPHA_SATURATE, + BlendFactor::Constant => Self::CONSTANT_COLOR, + BlendFactor::OneMinusConstant => Self::ONE_MINUS_CONSTANT_COLOR, + } + } +} + +impl VkFrom for vk::IndexType { + #[inline] + fn from(val: &IndexFormat) -> Self { + match val { + IndexFormat::Uint16 => Self::UINT16, + IndexFormat::Uint32 => Self::UINT32, + } + } +} + +impl VkFrom for vk::CompareOp { + #[inline] + fn from(val: &CompareFunction) -> Self { + match val { + CompareFunction::Never => Self::NEVER, + CompareFunction::Less => Self::LESS, + CompareFunction::Equal => Self::EQUAL, + CompareFunction::LessEqual => Self::LESS_OR_EQUAL, + CompareFunction::Greater => Self::GREATER, + CompareFunction::NotEqual => Self::NOT_EQUAL, + CompareFunction::GreaterEqual => Self::GREATER_OR_EQUAL, + CompareFunction::Always => Self::ALWAYS, + } + } +} + +impl VkFrom for vk::StencilOp { + #[inline] + fn from(val: &StencilOperation) -> Self { + match val { + StencilOperation::Keep => Self::KEEP, + StencilOperation::Zero => Self::ZERO, + StencilOperation::Replace => Self::REPLACE, + StencilOperation::Invert => Self::INVERT, + StencilOperation::IncrementClamp => Self::INCREMENT_AND_CLAMP, + StencilOperation::DecrementClamp => Self::DECREMENT_AND_CLAMP, + StencilOperation::IncrementWrap => Self::INCREMENT_AND_WRAP, + StencilOperation::DecrementWrap => Self::DECREMENT_AND_WRAP, + } + } +} + +impl VkFrom for vk::AttachmentLoadOp { + #[inline] + fn from(val: &LoadOp) -> Self { + match val { + LoadOp::Load => Self::LOAD, + LoadOp::Clear => Self::CLEAR, + LoadOp::DontCare => Self::DONT_CARE, + } + } +} + +impl VkFrom for vk::AttachmentStoreOp { + #[inline] + fn from(val: &StoreOp) -> Self { + match val { + StoreOp::Store => Self::STORE, + StoreOp::DontCare => Self::DONT_CARE, + } + } +} + +pub struct CreateInfoConverter6( + ScratchBuffer, + ScratchBuffer, + ScratchBuffer, + ScratchBuffer, + ScratchBuffer, + ScratchBuffer, +); + +pub struct CreateInfoConverter2(ScratchBuffer, ScratchBuffer); + +struct SubpassDepTracker<'a> { + subpass_deps: &'a mut ScratchBuffer, + attachment_dep_data: Vec<(u32, vk::PipelineStageFlags, vk::AccessFlags)>, + attachment_usage: &'a mut BitSet, +} + +impl SubpassDepTracker<'_> { + fn get_or_create_subpass_dep(&mut self, src: u32, dst: u32) -> &mut vk::SubpassDependency { + self.subpass_deps.binary_search_insert_by_key_with( + &(src, dst), + |d| (d.src_subpass, d.dst_subpass), + || { + vk::SubpassDependency::default() + .src_subpass(src) + .dst_subpass(dst) + // use BY-REGION by default + .dependency_flags(vk::DependencyFlags::BY_REGION) + }, + ) + } + + fn mark_subpass_attachment( + &mut self, + attachment: usize, + dst: usize, + dst_stage: vk::PipelineStageFlags, + dst_access: vk::AccessFlags, + next_access: vk::AccessFlags, + ) { + let (src, src_stage, src_access) = self.attachment_dep_data[attachment]; + if src != vk::SUBPASS_EXTERNAL { + let dep = self.get_or_create_subpass_dep(src, dst as u32); + dep.src_stage_mask |= src_stage; + dep.dst_access_mask |= src_access; + dep.dst_stage_mask |= dst_stage; + dep.dst_access_mask |= dst_access; + } + + if next_access != vk::AccessFlags::NONE { + self.attachment_dep_data[attachment] = (dst as u32, dst_stage, next_access); + } + + let num_attachments = self.attachment_dep_data.len(); + self.attachment_usage + .insert(dst * num_attachments + attachment); + } +} + +impl CreateInfoConverter6 { + #[inline] + pub const fn new() -> Self { + Self( + ScratchBuffer::new(), + ScratchBuffer::new(), + ScratchBuffer::new(), + ScratchBuffer::new(), + ScratchBuffer::new(), + ScratchBuffer::new(), + ) + } + + pub fn graphics_pass( + &mut self, + desc: &GraphicsPassDescriptor, + ) -> &vk::RenderPassCreateInfo<'_> { + // collect attachments + let attachments = self.0.clear_and_use_as::(); + let num_attachments = desc.attachments().len(); + attachments.reserve(num_attachments); + let mut attachment_dep_data = Vec::with_capacity(num_attachments); + for (i, a) in desc.attachments().iter().enumerate() { + let load_store_ops = desc.load_store_ops().get(i).copied().unwrap_or_default(); + attachments.push( + vk::AttachmentDescription::default() + .format(a.format.vk_into()) + .samples(vk::SampleCountFlags::from_raw(a.samples as u32)) + .load_op(load_store_ops.load_op.vk_into()) + .store_op(load_store_ops.store_op.vk_into()) + .stencil_load_op(load_store_ops.load_op.vk_into()) + .stencil_store_op(load_store_ops.store_op.vk_into()) + .initial_layout(if load_store_ops.load_op == LoadOp::Load { + a.initial_access.vk_into() + } else { + vk::ImageLayout::UNDEFINED + }) + .final_layout(a.final_access.vk_into()), + ); + attachment_dep_data.push(( + vk::SUBPASS_EXTERNAL, + vk::PipelineStageFlags::NONE, + vk::AccessFlags::NONE, + )); + } + + // calculate subpass deps + let sp_deps = self.1.clear_and_use_as::(); + let a_refs = self.2.clear_and_use_as::(); + let num_subpasses = desc.subpasses().len(); + let mut attachment_usage = BitSet::with_capacity_for(num_attachments * num_subpasses); + let mut subpass_tracker = SubpassDepTracker { + attachment_dep_data, + subpass_deps: sp_deps, + attachment_usage: &mut attachment_usage, + }; + for (dst, sp) in desc.subpasses().iter().enumerate() { + // TODO: handle Write>Read>Write! + // TODO: also non-attachment dubpass-deps + for &(a, _u) in sp.input_attachments() { + subpass_tracker.mark_subpass_attachment( + a as usize, + dst, + vk::PipelineStageFlags::FRAGMENT_SHADER, + vk::AccessFlags::INPUT_ATTACHMENT_READ, + vk::AccessFlags::NONE, + ); + + //attachments[a].final_layout = vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL; + a_refs.push(vk::AttachmentReference { + attachment: a as u32, // if a==!0 => vk::ATTACHMENT_UNUSED + //layout: u.vk_into(), + layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + }); + } + for &(a, _u) in sp.color_attachments() { + subpass_tracker.mark_subpass_attachment( + a as usize, + dst, + vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, + vk::AccessFlags::COLOR_ATTACHMENT_READ, + vk::AccessFlags::COLOR_ATTACHMENT_WRITE, + ); + + //attachments[a].final_layout = vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL; + a_refs.push(vk::AttachmentReference { + attachment: a as u32, // if a==!0 => vk::ATTACHMENT_UNUSED + //layout: u.vk_into(), + layout: vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL, + }); + } + if let Some((a, _u)) = sp.depth_stencil_attachment() { + subpass_tracker.mark_subpass_attachment( + a as usize, + dst, + vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS + | vk::PipelineStageFlags::LATE_FRAGMENT_TESTS, + vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_READ, + vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE, + ); + + //attachments[a].final_layout = vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + a_refs.push(vk::AttachmentReference { + attachment: a as u32, + //layout: u.vk_into(), + layout: vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + }); + } + } + drop(subpass_tracker); + + // preserve attachment + let mut a_preserve_tmp: Vec> = Vec::new(); + a_preserve_tmp.resize_with(desc.subpasses().len(), Vec::new); + loop { + let mut changed = false; + for dep in sp_deps.iter() { + if dep.src_subpass == vk::SUBPASS_EXTERNAL + || dep.dst_subpass == vk::SUBPASS_EXTERNAL + { + continue; + } + let src = dep.src_subpass as usize; + let dst = dep.dst_subpass as usize; + // There is a subpass dependency from S1 (`src`) to S (`dst`). + let a_start = src * num_subpasses; + let a_preserve_tmp_offset = a_preserve_tmp[dst].len(); + for a in attachment_usage.iter_range(a_start..a_start + num_attachments) { + // There is a subpass S1 that uses or preserves the attachment (`a`) + if !attachment_usage.contains(dst * num_attachments + a) { + // The attachment is not used or preserved in subpass S. + a_preserve_tmp[dst].push(a as u32); + changed = true; + } + } + for &a in &a_preserve_tmp[dst][a_preserve_tmp_offset..] { + // mark as used (perserved) + attachment_usage.insert(dst * num_attachments + a as usize); + } + } + if !changed { + break; + } + } + let a_preserves = self.3.clear_and_use_as::(); + for a in a_preserve_tmp.iter().flatten().copied() { + a_preserves.push(a); + } + + let a_refs = a_refs.as_slice(); + let a_preserves = a_preserves.as_slice(); + let subpasses = self.4.clear_and_use_as::>(); + let mut a_refs_offset = 0; + let mut a_preserves_offset = 0; + for (i, s) in desc.subpasses().iter().enumerate() { + let end_input_offset = a_refs_offset + s.input_attachments().len(); + let end_color_offset = end_input_offset + s.color_attachments().len(); + let end_preserves_offset = a_preserves_offset + a_preserve_tmp[i].len(); + let mut b = vk::SubpassDescription::default() + .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS) + .preserve_attachments(&a_preserves[a_preserves_offset..end_preserves_offset]) + .input_attachments(&a_refs[a_refs_offset..end_input_offset]) + .color_attachments(&a_refs[end_input_offset..end_color_offset]); + // TODO: resolve + a_refs_offset = end_color_offset; + a_preserves_offset = end_preserves_offset; + if s.depth_stencil_attachment().is_some() { + a_refs_offset += 1; + b = b.depth_stencil_attachment(&a_refs[end_color_offset]); + } + subpasses.push(b); + } + + let buf = self.5.clear_and_use_as::>(); + buf.reserve(1); + buf.push( + vk::RenderPassCreateInfo::default() + .attachments(attachments.as_slice()) + .subpasses(subpasses.as_slice()) + .dependencies(sp_deps.as_slice()), + ); + &buf.as_slice()[0] + } +} + +impl CreateInfoConverter2 { + #[inline] + pub const fn new() -> Self { + Self(ScratchBuffer::new(), ScratchBuffer::new()) + } + + pub fn bind_group_layout( + &mut self, + desc: &BindGroupLayoutDescriptor<'_>, + ) -> &vk::DescriptorSetLayoutCreateInfo<'_> { + let buf0 = self + .0 + .clear_and_use_as::>(); + buf0.reserve(desc.entries.len()); + for e in desc.entries.as_ref() { + buf0.push( + vk::DescriptorSetLayoutBinding::default() + .binding(e.binding) + .descriptor_count(e.count), + ); + // TODO: descriptor_type, stage_flags, immutable_samplers + todo!(); + } + + let buf = self + .1 + .clear_and_use_as::>(); + buf.reserve(1); + buf.push(vk::DescriptorSetLayoutCreateInfo::default().bindings(buf0.as_slice())); + &buf.as_slice()[0] + } + + pub fn pipeline_layout( + &mut self, + res: &AshResources, + desc: &PipelineLayoutDescriptor<'_>, + ) -> &vk::PipelineLayoutCreateInfo<'_> { + let buf0 = self.0.clear_and_use_as::(); + buf0.reserve(desc.bind_group_layouts.len()); + for bgl in desc.bind_group_layouts.as_ref() { + buf0.push(res.bind_group_layouts[*bgl]); + } + + let buf = self + .1 + .clear_and_use_as::>(); + buf.reserve(1); + buf.push(vk::PipelineLayoutCreateInfo::default().set_layouts(buf0.as_slice())); + &buf.as_slice()[0] + } + + pub fn graphics_pipeline_descriptor( + &mut self, + res: &AshResources, + descs: &[GraphicsPipelineDescriptor<'_>], + ) -> &[vk::GraphicsPipelineCreateInfo<'_>] { + let buf = self + .0 + .clear_and_use_as::>(); + buf.reserve(descs.len()); + for desc in descs { + let layout = desc + .layout + .map_or(vk::PipelineLayout::null(), |l| res.pipeline_layouts[l]); + buf.push(vk::GraphicsPipelineCreateInfo::default().layout(layout)); + // TODO: vertex, primitive, depth_stencil, fragment, samples, specialization + todo!(" implement graphics_pipeline_descriptor"); + } + buf.as_slice() + } + + pub fn compute_pipeline_descriptor( + &mut self, + res: &AshResources, + descs: &[ComputePipelineDescriptor<'_>], + ) -> &[vk::ComputePipelineCreateInfo<'_>] { + let buf = self + .0 + .clear_and_use_as::>(); + buf.reserve(descs.len()); + for desc in descs { + let layout = desc + .layout + .map_or(vk::PipelineLayout::null(), |l| res.pipeline_layouts[l]); + // TODO: module, entry_point, specialization + buf.push(vk::ComputePipelineCreateInfo::default().layout(layout)); + todo!(" implement compute_pipeline_descriptor"); + } + buf.as_slice() + } + + pub fn ray_tracing_pipeline_descriptor( + &mut self, + res: &AshResources, + descs: &[RayTracingPipelineDescriptor<'_>], + ) -> &[vk::RayTracingPipelineCreateInfoKHR<'_>] { + let buf = self + .0 + .clear_and_use_as::>(); + buf.reserve(descs.len()); + for desc in descs { + let layout = desc + .layout + .map_or(vk::PipelineLayout::null(), |l| res.pipeline_layouts[l]); + // TODO: modules, groups, specialization + buf.push( + vk::RayTracingPipelineCreateInfoKHR::default() + .layout(layout) + .max_pipeline_ray_recursion_depth(desc.max_recursion_depth), + ); + todo!(" implement ray_tracing_pipeline_descriptor"); + } + buf.as_slice() + } +} diff --git a/crates/render-ash/src/debug_utils.rs b/crates/render-ash/src/debug_utils.rs new file mode 100644 index 0000000..981e024 --- /dev/null +++ b/crates/render-ash/src/debug_utils.rs @@ -0,0 +1,336 @@ +use std::{ + ffi::{CStr, c_void}, + os::raw::c_char, +}; + +use ash::vk::{self, Handle}; +use tracing::{debug, error, info, warn}; + +pub const EXT_NAME: &CStr = ash::ext::debug_utils::NAME; + +unsafe fn c_str_from_ptr<'a>(str_ptr: *const c_char) -> &'a CStr { + unsafe { + if str_ptr.is_null() { + c"" + } else { + CStr::from_ptr(str_ptr) + } + } +} + +unsafe extern "system" fn debug_callback( + message_severity: vk::DebugUtilsMessageSeverityFlagsEXT, + message_type: vk::DebugUtilsMessageTypeFlagsEXT, + p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>, + _p_user_data: *mut c_void, +) -> vk::Bool32 { + unsafe { + use vk::DebugUtilsMessageSeverityFlagsEXT; + + if std::thread::panicking() { + return vk::FALSE; + } + + let message = c_str_from_ptr((*p_callback_data).p_message); + let message_id_name = c_str_from_ptr((*p_callback_data).p_message_id_name); + let message_id_number = (*p_callback_data).message_id_number; + + // TODO: queues, labels, objects, ... + + match message_severity { + DebugUtilsMessageSeverityFlagsEXT::VERBOSE => { + debug!( + "Vk[{:?},#{},{:?}]: {}", + message_type, + message_id_number, + message_id_name, + message.to_string_lossy() + ) + } + DebugUtilsMessageSeverityFlagsEXT::INFO => { + info!( + "Vk[{:?},#{},{:?}]: {}", + message_type, + message_id_number, + message_id_name, + message.to_string_lossy() + ) + } + DebugUtilsMessageSeverityFlagsEXT::WARNING => { + warn!( + "Vk[{:?},#{},{:?}]: {}", + message_type, + message_id_number, + message_id_name, + message.to_string_lossy() + ) + } + DebugUtilsMessageSeverityFlagsEXT::ERROR => { + error!( + "Vk[{:?},#{},{:?}]: {}", + message_type, + message_id_number, + message_id_name, + message.to_string_lossy() + ) + } + _ => { + warn!( + "Vk[{:?},#{},{:?}]: {}", + message_type, + message_id_number, + message_id_name, + message.to_string_lossy() + ) + } + }; + + vk::FALSE + } +} + +// stack-allocated buffer for keeping a copy of the object_name (for appending \0-byte) +// + optional Vector for allocations +struct CStrBuf { + buf: [u8; 64], + alloc: Vec, +} + +impl CStrBuf { + #[inline] + const fn new() -> Self { + Self { + buf: [0; 64], + alloc: Vec::new(), + } + } + + #[inline] + fn get_cstr<'a>(&'a mut self, s: &'a str) -> &'a CStr { + if s.ends_with('\0') { + // SAFETY: string always ends with 0-byte. + // Don't care, if there are 0-bytes before end. + unsafe { CStr::from_bytes_with_nul_unchecked(s.as_bytes()) } + } else { + let bytes = s.as_bytes(); + let len = bytes.len(); + if len < self.buf.len() { + self.buf[..len].copy_from_slice(bytes); + self.buf[len] = 0; + // SAFETY: string always ends with 0-byte. + // Don't care, if there are 0-bytes before end. + return unsafe { CStr::from_bytes_with_nul_unchecked(&self.buf[..len + 1]) }; + } + self.alloc.clear(); + self.alloc.reserve_exact(len + 1); + self.alloc.extend_from_slice(s.as_bytes()); + self.alloc.push(0); + // SAFETY: string always ends with 0-byte. + // Don't care, if there are 0-bytes before end. + unsafe { CStr::from_bytes_with_nul_unchecked(&self.alloc) } + } + } +} + +pub struct InstanceDebugUtils { + functions: ash::ext::debug_utils::Instance, + utils_messenger: vk::DebugUtilsMessengerEXT, +} +#[repr(transparent)] +pub struct DeviceDebugUtils(ash::ext::debug_utils::Device); + +impl InstanceDebugUtils { + pub fn new( + entry: &ash::Entry, + instance: &ash::Instance, + message_severities: vk::DebugUtilsMessageSeverityFlagsEXT, + ) -> Self { + let functions = ash::ext::debug_utils::Instance::new(entry, instance); + if message_severities.is_empty() { + Self { + functions, + utils_messenger: vk::DebugUtilsMessengerEXT::null(), + } + } else { + let utils_messenger = unsafe { + functions + .create_debug_utils_messenger( + &vk::DebugUtilsMessengerCreateInfoEXT::default() + .message_severity(message_severities) + .message_type( + vk::DebugUtilsMessageTypeFlagsEXT::GENERAL + | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION + | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE, + ) + .pfn_user_callback(Some(debug_callback)), + None, + ) + .expect("Debug Utils Callback") + }; + + Self { + functions, + utils_messenger, + } + } + } + + #[inline(always)] + pub fn is_messenger_enabled(&self) -> bool { + !self.utils_messenger.is_null() + } +} + +impl DeviceDebugUtils { + pub fn new(instance: &ash::Instance, device: &ash::Device) -> Self { + let functions = ash::ext::debug_utils::Device::new(instance, device); + Self(functions) + } + + #[inline(always)] + pub unsafe fn object_name(&self, handle: H, object_name: &str) { + unsafe { self._object_name(H::TYPE, handle.as_raw(), object_name) } + } + #[inline(always)] + pub unsafe fn object_name_cstr(&self, handle: H, object_name: &CStr) { + unsafe { self._object_name_cstr(H::TYPE, handle.as_raw(), object_name) } + } + + #[inline] + unsafe fn _object_name( + &self, + object_type: vk::ObjectType, + object_handle: u64, + object_name: &str, + ) { + unsafe { + if object_handle == 0 { + return; + } + + let mut cstr_buf = CStrBuf::new(); + self._object_name_cstr(object_type, object_handle, cstr_buf.get_cstr(object_name)) + } + } + + unsafe fn _object_name_cstr( + &self, + object_type: vk::ObjectType, + object_handle: u64, + object_name: &CStr, + ) { + unsafe { + if object_handle == 0 { + return; + } + let _result = self + .0 + .set_debug_utils_object_name(&vk::DebugUtilsObjectNameInfoEXT { + object_handle, + object_type, + p_object_name: object_name.as_ptr(), + ..Default::default() + }); + } + } + + #[inline] + pub unsafe fn cmd_insert_debug_label(&self, command_buffer: vk::CommandBuffer, label: &str) { + unsafe { + let mut cstr_buf = CStrBuf::new(); + self.cmd_insert_debug_label_cstr(command_buffer, cstr_buf.get_cstr(label)) + } + } + pub unsafe fn cmd_insert_debug_label_cstr( + &self, + command_buffer: vk::CommandBuffer, + label: &CStr, + ) { + unsafe { + self.0.cmd_insert_debug_utils_label( + command_buffer, + &vk::DebugUtilsLabelEXT::default().label_name(label), + ); + } + } + + #[inline] + pub unsafe fn cmd_begin_debug_label(&self, command_buffer: vk::CommandBuffer, label: &str) { + unsafe { + let mut cstr_buf = CStrBuf::new(); + self.cmd_begin_debug_label_cstr(command_buffer, cstr_buf.get_cstr(label)) + } + } + pub unsafe fn cmd_begin_debug_label_cstr( + &self, + command_buffer: vk::CommandBuffer, + label: &CStr, + ) { + unsafe { + self.0.cmd_begin_debug_utils_label( + command_buffer, + &vk::DebugUtilsLabelEXT::default().label_name(label), + ); + } + } + + #[inline] + pub unsafe fn cmd_end_debug_label(&self, command_buffer: vk::CommandBuffer) { + unsafe { + self.0.cmd_end_debug_utils_label(command_buffer); + } + } + + #[inline] + pub unsafe fn queue_insert_debug_label(&self, queue: vk::Queue, label: &str) { + unsafe { + let mut cstr_buf = CStrBuf::new(); + self.queue_insert_debug_label_cstr(queue, cstr_buf.get_cstr(label)) + } + } + + pub unsafe fn queue_insert_debug_label_cstr(&self, queue: vk::Queue, label: &CStr) { + unsafe { + self.0.queue_insert_debug_utils_label( + queue, + &vk::DebugUtilsLabelEXT::default().label_name(label), + ); + } + } + + #[inline] + pub unsafe fn queue_begin_debug_label(&self, queue: vk::Queue, label: &str) { + unsafe { + let mut cstr_buf = CStrBuf::new(); + self.queue_begin_debug_label_cstr(queue, cstr_buf.get_cstr(label)) + } + } + + pub unsafe fn queue_begin_debug_label_cstr(&self, queue: vk::Queue, label: &CStr) { + unsafe { + self.0.queue_begin_debug_utils_label( + queue, + &vk::DebugUtilsLabelEXT::default().label_name(label), + ); + } + } + + #[inline] + pub unsafe fn queue_end_debug_label(&self, queue: vk::Queue) { + unsafe { + self.0.queue_end_debug_utils_label(queue); + } + } +} + +impl Drop for InstanceDebugUtils { + fn drop(&mut self) { + if self.utils_messenger != vk::DebugUtilsMessengerEXT::null() { + let utils_messenger = std::mem::take(&mut self.utils_messenger); + unsafe { + self.functions + .destroy_debug_utils_messenger(utils_messenger, None); + } + } + } +} diff --git a/crates/render-ash/src/device.rs b/crates/render-ash/src/device.rs new file mode 100644 index 0000000..1844600 --- /dev/null +++ b/crates/render-ash/src/device.rs @@ -0,0 +1,413 @@ +use std::{ffi::CStr, ops::Deref, os::raw::c_char, sync::Arc}; + +use ash::vk; +use pulz_render::graph::pass::PipelineBindPoint; +use tracing::{debug, info, warn}; + +use crate::{Error, ErrorNoExtension, Result, debug_utils, instance::AshInstance}; + +pub struct AshDevice { + device_raw: ash::Device, + instance: Arc, + physical_device: vk::PhysicalDevice, + device_extensions: Vec<&'static CStr>, + debug_utils: Option, + ext_swapchain: Option, + ext_sync2: Option, + ext_raytracing_pipeline: Option, + queues: Queues, +} + +impl Deref for AshDevice { + type Target = ash::Device; + #[inline] + fn deref(&self) -> &ash::Device { + &self.device_raw + } +} + +impl AshInstance { + pub(crate) fn new_device( + self: &Arc, + surface_opt: vk::SurfaceKHR, + ) -> Result> { + let (physical_device, indices, device_extensions) = + self.pick_physical_device(surface_opt)?; + AshDevice::new(self, physical_device, indices, device_extensions) + } +} + +impl AshDevice { + fn new( + instance: &Arc, + physical_device: vk::PhysicalDevice, + indices: QueueFamilyIndices, + device_extensions: Vec<&'static CStr>, + ) -> Result> { + let (device_raw, queues) = instance.create_logical_device( + physical_device, + indices, + device_extensions.iter().copied(), + )?; + + let mut device = Self { + instance: instance.clone(), + physical_device, + device_raw, + device_extensions, + debug_utils: None, + ext_swapchain: None, + ext_sync2: None, + ext_raytracing_pipeline: None, + queues, + }; + + if instance.debug_utils().is_ok() { + device.debug_utils = Some(debug_utils::DeviceDebugUtils::new(instance, &device)); + } + if device.has_device_extension(ash::khr::swapchain::NAME) { + device.ext_swapchain = Some(ash::khr::swapchain::Device::new(instance, &device)); + } + if device.has_device_extension(ash::khr::synchronization2::NAME) { + device.ext_sync2 = Some(ash::khr::synchronization2::Device::new(instance, &device)) + } + if device.has_device_extension(ash::khr::ray_tracing_pipeline::NAME) { + device.ext_raytracing_pipeline = Some(ash::khr::ray_tracing_pipeline::Device::new( + instance, &device, + )) + } + + Ok(Arc::new(device)) + } + + #[inline] + pub fn instance(&self) -> &AshInstance { + &self.instance + } + + #[inline] + pub fn instance_arc(&self) -> Arc { + self.instance.clone() + } + + #[inline] + pub fn physical_device(&self) -> vk::PhysicalDevice { + self.physical_device + } + + #[inline] + pub fn has_device_extension(&self, name: &CStr) -> bool { + self.device_extensions.contains(&name) + } + + #[inline] + pub fn queues(&self) -> &Queues { + &self.queues + } + + #[inline] + pub(crate) fn debug_utils(&self) -> Result<&debug_utils::DeviceDebugUtils, ErrorNoExtension> { + self.debug_utils + .as_ref() + .ok_or(ErrorNoExtension(ash::ext::debug_utils::NAME)) + } + + #[inline] + pub(crate) fn ext_swapchain(&self) -> Result<&ash::khr::swapchain::Device, ErrorNoExtension> { + self.ext_swapchain + .as_ref() + .ok_or(ErrorNoExtension(ash::khr::swapchain::NAME)) + } + + #[inline] + pub(crate) fn ext_sync2( + &self, + ) -> Result<&ash::khr::synchronization2::Device, ErrorNoExtension> { + self.ext_sync2 + .as_ref() + .ok_or(ErrorNoExtension(ash::khr::synchronization2::NAME)) + } + + #[inline] + pub(crate) fn ext_raytracing_pipeline( + &self, + ) -> Result<&ash::khr::ray_tracing_pipeline::Device, ErrorNoExtension> { + self.ext_raytracing_pipeline + .as_ref() + .ok_or(ErrorNoExtension(ash::khr::ray_tracing_pipeline::NAME)) + } + + #[inline] + pub unsafe fn object_name(&self, handle: H, name: &str) { + unsafe { + if let Some(debug_utils) = &self.debug_utils { + debug_utils.object_name(handle, name) + } + } + } +} + +impl Drop for AshDevice { + fn drop(&mut self) { + if self.device_raw.handle() != vk::Device::null() { + unsafe { + self.device_raw.destroy_device(None); + } + } + } +} + +fn get_device_extensions( + instance: &ash::Instance, + physical_device: vk::PhysicalDevice, +) -> Result> { + let available_extensions = + unsafe { instance.enumerate_device_extension_properties(physical_device)? }; + + let mut extensions = Vec::with_capacity(4); + extensions.push(ash::khr::swapchain::NAME); + extensions.push(ash::khr::synchronization2::NAME); + + // Only keep available extensions. + extensions.retain(|&ext| { + if available_extensions + .iter() + .any(|avail_ext| unsafe { CStr::from_ptr(avail_ext.extension_name.as_ptr()) == ext }) + { + debug!("Device extension ✅ YES {:?}", ext); + true + } else { + warn!("Device extension ❌ NO {:?}", ext); + false + } + }); + + Ok(extensions) +} + +impl AshInstance { + fn pick_physical_device( + &self, + for_surface_opt: vk::SurfaceKHR, + ) -> Result<(vk::PhysicalDevice, QueueFamilyIndices, Vec<&'static CStr>)> { + let physical_devices = unsafe { self.enumerate_physical_devices()? }; + + info!( + "{} devices (GPU) found with vulkan support.", + physical_devices.len() + ); + + let mut result = None; + for (i, &physical_device) in physical_devices.iter().enumerate() { + self.log_device_infos(physical_device, i); + + if let Some((indices, extensions)) = + self.check_physical_device_suitable(physical_device, for_surface_opt) + { + if result.is_none() { + result = Some((i, physical_device, indices, extensions)) + } + } + } + + match result { + Some((i, physical_device, indices, extensions)) => { + info!("Selected device: #{}", i); + Ok((physical_device, indices, extensions)) + } + None => { + warn!("Unable to find a suitable GPU!"); + Err(Error::NoAdapter) + } + } + } + + fn log_device_infos(&self, physical_device: vk::PhysicalDevice, device_index: usize) { + let device_properties = unsafe { self.get_physical_device_properties(physical_device) }; + let _device_features = unsafe { self.get_physical_device_features(physical_device) }; + let device_queue_families = + unsafe { self.get_physical_device_queue_family_properties(physical_device) }; + + let device_name = unsafe { CStr::from_ptr(device_properties.device_name.as_ptr()) }; + + info!( + "Device #{}\tName: {:?}, id: {:?}, type: {:?}", + device_index, device_name, device_properties.device_id, device_properties.device_type + ); + + info!("\tQueue Families: {}", device_queue_families.len()); + for (i, queue_family) in device_queue_families.iter().enumerate() { + info!( + "\t #{}:{:4} x {:?}", + i, queue_family.queue_count, queue_family.queue_flags + ); + } + } + + fn check_physical_device_suitable( + &self, + physical_device: vk::PhysicalDevice, + for_surface_opt: vk::SurfaceKHR, + ) -> Option<(QueueFamilyIndices, Vec<&'static CStr>)> { + let indices = + QueueFamilyIndices::from_physical_device(self, physical_device, for_surface_opt)?; + let extensions = get_device_extensions(self, physical_device).ok()?; + + if for_surface_opt != vk::SurfaceKHR::null() + && (!extensions.contains(&ash::khr::swapchain::NAME) + || self + .query_swapchain_support(for_surface_opt, physical_device) + .is_none()) + { + return None; + } + + Some((indices, extensions)) + } + + #[inline] + fn create_logical_device<'a>( + &self, + physical_device: vk::PhysicalDevice, + indices: QueueFamilyIndices, + extensions: impl IntoIterator, + ) -> Result<(ash::Device, Queues)> { + let extensions_ptr: Vec<_> = extensions.into_iter().map(CStr::as_ptr).collect(); + self._create_logical_device(physical_device, indices, &extensions_ptr) + } + + fn _create_logical_device( + &self, + physical_device: vk::PhysicalDevice, + indices: QueueFamilyIndices, + extensions_ptr: &[*const c_char], + ) -> Result<(ash::Device, Queues)> { + let device = unsafe { + self.create_device( + physical_device, + &vk::DeviceCreateInfo::default() + .queue_create_infos(&[vk::DeviceQueueCreateInfo::default() + .queue_family_index(indices.graphics_family) + .queue_priorities(&[1.0_f32])]) + .enabled_extension_names(extensions_ptr), + // .enabled_features(&vk::PhysicalDeviceFeatures { + // ..Default::default() // default just enable no feature. + // }) + None, + )? + }; + + let queues = Queues::from_device(&device, indices); + + Ok((device, queues)) + } +} + +pub struct QueueFamilyIndices { + pub graphics_family: u32, + pub compute_family: u32, + pub present_family: u32, +} + +impl QueueFamilyIndices { + fn from_physical_device( + instance: &AshInstance, + physical_device: vk::PhysicalDevice, + for_surface_opt: vk::SurfaceKHR, + ) -> Option { + let queue_families = + unsafe { instance.get_physical_device_queue_family_properties(physical_device) }; + + #[derive(Default)] + struct OptIndices { + graphics: Option, + compute: Option, + present: Option, + } + + impl OptIndices { + fn check_complete(&self) -> Option { + Some(QueueFamilyIndices { + graphics_family: self.graphics?, + compute_family: self.compute?, + present_family: self.present?, + }) + } + } + + let mut indices = OptIndices::default(); + for (i, queue_family) in queue_families.iter().enumerate() { + let i = i as u32; + if queue_family.queue_count == 0 { + continue; + } + if indices.graphics.is_none() + && queue_family.queue_flags.contains(vk::QueueFlags::GRAPHICS) + { + indices.graphics = Some(i); + if for_surface_opt == vk::SurfaceKHR::null() { + indices.present = Some(i); + } + } + + if indices.compute.is_none() + && queue_family.queue_flags.contains(vk::QueueFlags::COMPUTE) + { + indices.compute = Some(i); + } + + if indices.present.is_none() + && for_surface_opt != vk::SurfaceKHR::null() + && instance.get_physical_device_surface_support(physical_device, i, for_surface_opt) + { + indices.present = Some(i); + } + + if let Some(result) = indices.check_complete() { + return Some(result); + } + } + + None + } +} + +pub struct Queues { + pub indices: QueueFamilyIndices, + pub graphics: vk::Queue, + pub compute: vk::Queue, + pub present: vk::Queue, +} + +impl Queues { + pub fn from_device(device: &ash::Device, indices: QueueFamilyIndices) -> Self { + unsafe { + let graphics = device.get_device_queue(indices.graphics_family, 0); + let compute = device.get_device_queue(indices.compute_family, 0); + let present = device.get_device_queue(indices.present_family, 0); + Self { + indices, + graphics, + compute, + present, + } + } + } + + pub fn for_bind_point(&self, bind_point: PipelineBindPoint) -> (vk::Queue, u32) { + match bind_point { + PipelineBindPoint::Graphics | PipelineBindPoint::RayTracing => { + (self.graphics, self.indices.graphics_family) + } + PipelineBindPoint::Compute => (self.compute, self.indices.compute_family), + } + } +} + +impl Deref for Queues { + type Target = QueueFamilyIndices; + #[inline] + fn deref(&self) -> &QueueFamilyIndices { + &self.indices + } +} diff --git a/crates/render-ash/src/drop_guard.rs b/crates/render-ash/src/drop_guard.rs new file mode 100644 index 0000000..e32bc2b --- /dev/null +++ b/crates/render-ash/src/drop_guard.rs @@ -0,0 +1,203 @@ +use std::{collections::VecDeque, mem::ManuallyDrop}; + +use ash::vk; + +use crate::{AshDevice, Result, instance::AshInstance}; + +pub trait Destroy { + type Context; + unsafe fn destroy(self, context: &Self::Context); +} + +pub trait CreateWithInfo: Destroy + Sized { + type CreateInfo<'a>; + unsafe fn create(context: &Self::Context, create_info: &Self::CreateInfo<'_>) -> Result; +} + +impl AshDevice { + #[inline] + pub unsafe fn create(&self, create_info: &C::CreateInfo<'_>) -> Result> + where + C: CreateWithInfo, + { + Ok(Guard::new(self, unsafe { C::create(self, create_info)? })) + } + + #[inline] + pub unsafe fn destroy(&self, handle: D) + where + D: Destroy, + { + unsafe { handle.destroy(self) } + } + + #[inline] + pub(crate) fn hold(&self, item: D) -> Guard<'_, D> + where + D: Destroy, + { + Guard::new(self, item) + } +} + +impl AshInstance { + #[inline] + pub unsafe fn create(&self, create_info: &C::CreateInfo<'_>) -> Result> + where + C: CreateWithInfo, + { + Ok(Guard::new(self, unsafe { C::create(self, create_info)? })) + } + + #[inline] + pub unsafe fn destroy(&self, handle: D) + where + D: Destroy, + { + unsafe { handle.destroy(self) } + } + + #[inline] + pub(crate) fn hold(&self, item: D) -> Guard<'_, D> + where + D: Destroy, + { + Guard::new(self, item) + } +} + +pub struct Guard<'a, D: Destroy> { + item: ManuallyDrop, + context: &'a D::Context, +} + +impl<'a, D: Destroy> Guard<'a, D> { + #[inline] + pub fn new(context: &'a D::Context, item: D) -> Self { + Self { + item: ManuallyDrop::new(item), + context, + } + } + + #[inline] + pub fn take(mut self) -> D { + let item = unsafe { ManuallyDrop::take(&mut self.item) }; + std::mem::forget(self); + item + } + + #[inline] + pub fn as_ref(&self) -> &D { + &self.item + } + + #[inline] + pub fn as_mut(&mut self) -> &mut D { + &mut self.item + } +} + +impl<'a, I, D: Destroy + std::ops::Index> std::ops::Index for Guard<'a, D> { + type Output = D::Output; + #[inline] + fn index(&self, i: I) -> &D::Output { + &self.item[i] + } +} + +impl<'a, D: Destroy + Copy> Guard<'a, D> { + #[inline] + pub fn raw(&self) -> D { + *self.item + } +} + +impl Drop for Guard<'_, D> { + fn drop(&mut self) { + unsafe { + let item = ManuallyDrop::take(&mut self.item); + item.destroy(self.context); + } + } +} + +impl Destroy for Vec { + type Context = D::Context; + #[inline] + unsafe fn destroy(self, device: &D::Context) { + unsafe { self.into_iter().for_each(|d| d.destroy(device)) } + } +} + +impl Destroy for VecDeque { + type Context = D::Context; + #[inline] + unsafe fn destroy(self, device: &D::Context) { + unsafe { self.into_iter().for_each(|d| d.destroy(device)) } + } +} + +impl Destroy for std::vec::Drain<'_, D> { + type Context = D::Context; + #[inline] + unsafe fn destroy(self, device: &D::Context) { + unsafe { self.for_each(|d| d.destroy(device)) } + } +} + +impl Destroy for std::collections::vec_deque::Drain<'_, D> { + type Context = D::Context; + #[inline] + unsafe fn destroy(self, device: &D::Context) { + unsafe { self.for_each(|d| d.destroy(device)) } + } +} + +macro_rules! impl_create_destroy { + ($ctx:ty { + <$lt:tt> + $( + $vktype:ty : ($destroy:ident $(, $create:ident $createinfo:ty)?) + ),* $(,)? + }) => { + $( + + impl Destroy for $vktype { + type Context = $ctx; + #[inline] + unsafe fn destroy(self, ctx: &Self::Context) { unsafe { + ctx.$destroy(self, None); + }} + } + + $( + impl CreateWithInfo for $vktype { + type CreateInfo<$lt> = $createinfo; + #[inline] + unsafe fn create(ctx: &Self::Context, create_info: &Self::CreateInfo<'_>) -> Result { unsafe { + Ok(ctx.$create(create_info, None)?) + }} + } + )? + )* + }; +} + +impl_create_destroy! { + AshDevice { <'a> + vk::Fence : (destroy_fence, create_fence vk::FenceCreateInfo<'a>), + vk::Semaphore : (destroy_semaphore, create_semaphore vk::SemaphoreCreateInfo<'a>), + vk::Event : (destroy_event, create_event vk::EventCreateInfo<'a>), + vk::CommandPool : (destroy_command_pool, create_command_pool vk::CommandPoolCreateInfo<'a>), + vk::Buffer : (destroy_buffer, create_buffer vk::BufferCreateInfo<'a>), + vk::Image : (destroy_image, create_image vk::ImageCreateInfo<'a>), + vk::ImageView : (destroy_image_view, create_image_view vk::ImageViewCreateInfo<'a>), + vk::Framebuffer : (destroy_framebuffer, create_framebuffer vk::FramebufferCreateInfo<'a>), + vk::RenderPass : (destroy_render_pass, create_render_pass vk::RenderPassCreateInfo<'a>), + vk::ShaderModule : (destroy_shader_module, create_shader_module vk::ShaderModuleCreateInfo<'a>), + vk::DescriptorSetLayout : (destroy_descriptor_set_layout, create_descriptor_set_layout vk::DescriptorSetLayoutCreateInfo<'a>), + vk::PipelineLayout : (destroy_pipeline_layout, create_pipeline_layout vk::PipelineLayoutCreateInfo<'a>), + vk::Pipeline : (destroy_pipeline), + } +} diff --git a/crates/render-ash/src/encoder.rs b/crates/render-ash/src/encoder.rs new file mode 100644 index 0000000..6e4389e --- /dev/null +++ b/crates/render-ash/src/encoder.rs @@ -0,0 +1,391 @@ +use std::{cell::Cell, collections::VecDeque, sync::Arc}; + +use ash::{prelude::VkResult, vk}; +use pulz_render::backend::CommandEncoder; + +use crate::{Result, device::AshDevice}; + +pub struct AshCommandPool { + device: Arc, + queue_family_index: u32, + pool: vk::CommandPool, + fresh_buffers: VecDeque, + done_buffers: Vec, + new_allocation_count: u32, + semaphores_pool: VecDeque, + used_semaphores: Vec, // semaphores to return to pool after frame finished +} + +impl AshDevice { + pub(crate) fn new_command_pool( + self: &Arc, + queue_family_index: u32, + ) -> VkResult { + let pool = unsafe { + self.create_command_pool( + &vk::CommandPoolCreateInfo::default() + .queue_family_index(queue_family_index) + .flags(vk::CommandPoolCreateFlags::TRANSIENT), + None, + )? + }; + Ok(AshCommandPool { + device: self.clone(), + queue_family_index, + pool, + fresh_buffers: VecDeque::new(), + done_buffers: Vec::new(), + new_allocation_count: 1, + semaphores_pool: VecDeque::new(), + used_semaphores: Vec::new(), + }) + } +} + +impl AshCommandPool { + #[inline] + pub fn device(&self) -> &AshDevice { + &self.device + } + + pub unsafe fn reset(&mut self) -> VkResult<()> { + unsafe { + self.device + .reset_command_pool(self.pool, vk::CommandPoolResetFlags::empty())?; + self.fresh_buffers.extend(self.done_buffers.drain(..)); + + // return all semaphores to pool + self.semaphores_pool.extend(self.used_semaphores.drain(..)); + + Ok(()) + } + } + + pub fn request_semaphore(&mut self) -> Result { + // TODO: drop guard + let s = if let Some(s) = self.semaphores_pool.pop_front() { + s + } else { + unsafe { + self.device + .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)? + } + }; + self.used_semaphores.push(s); + Ok(s) + } + + pub fn encoder(&mut self) -> Result, vk::Result> { + if self.fresh_buffers.is_empty() { + let new_buffers = unsafe { + self.device.allocate_command_buffers( + &vk::CommandBufferAllocateInfo::default() + .command_pool(self.pool) + .level(vk::CommandBufferLevel::PRIMARY) + .command_buffer_count(self.new_allocation_count), + )? + }; + self.fresh_buffers.extend(new_buffers); + } + let buffer = self.fresh_buffers.pop_front().unwrap(); + unsafe { + self.device.begin_command_buffer( + buffer, + &vk::CommandBufferBeginInfo::default() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT), + )?; + } + Ok(AshCommandEncoder { + pool: self, + buffer, + debug_levels: Cell::new(0), + }) + } + + unsafe fn free_command_buffers(&self, buffers: &[vk::CommandBuffer]) { + unsafe { + if !buffers.is_empty() { + self.device.free_command_buffers(self.pool, buffers); + } + } + } +} + +impl Drop for AshCommandPool { + fn drop(&mut self) { + self.semaphores_pool.extend(self.used_semaphores.drain(..)); + for semaphore in self.semaphores_pool.drain(..) { + unsafe { + self.device.destroy_semaphore(semaphore, None); + } + } + + unsafe { + let (a, b) = self.fresh_buffers.as_slices(); + self.free_command_buffers(a); + self.free_command_buffers(b); + self.fresh_buffers.clear(); + self.free_command_buffers(&self.done_buffers); + self.done_buffers.clear(); + } + if self.pool != vk::CommandPool::null() { + unsafe { + self.device.destroy_command_pool(self.pool, None); + } + self.pool = vk::CommandPool::null(); + } + } +} +pub struct AshCommandEncoder<'l> { + pool: &'l mut AshCommandPool, + buffer: vk::CommandBuffer, + debug_levels: Cell, +} + +impl AshCommandEncoder<'_> { + pub fn submit(self, submission: &SubmissionGroup) -> VkResult<()> { + self.end_remaining_debug_labels(); + unsafe { + self.pool.device.end_command_buffer(self.buffer)?; + } + submission.queue.push(self.buffer); + Ok(()) + } + + #[inline] + pub fn request_semaphore(&mut self) -> Result { + self.pool.request_semaphore() + } + + pub fn insert_debug_label(&self, label: &str) { + if let Ok(debug_utils) = self.pool.device.debug_utils() { + unsafe { + debug_utils.cmd_begin_debug_label(self.buffer, label); + } + } + } + + pub fn begin_debug_label(&self, label: &str) { + if let Ok(debug_utils) = self.pool.device.debug_utils() { + unsafe { + debug_utils.cmd_begin_debug_label(self.buffer, label); + } + let debug_levels = self.debug_levels.get(); + self.debug_levels.set(debug_levels + 1); + } + } + + pub fn end_debug_label(&self) { + if let Ok(debug_utils) = self.pool.device.debug_utils() { + unsafe { + debug_utils.cmd_end_debug_label(self.buffer); + } + let debug_levels = self.debug_levels.get(); + if debug_levels > 0 { + self.debug_levels.set(debug_levels - 1); + } + } + } + + fn end_remaining_debug_labels(&self) { + let debug_levels = self.debug_levels.get(); + if debug_levels > 0 { + self.debug_levels.set(0); + if let Ok(debug_utils) = self.pool.device.debug_utils() { + for _i in 0..debug_levels { + unsafe { + debug_utils.cmd_end_debug_label(self.buffer); + } + } + } + } + } + + pub unsafe fn clear_color_image( + &self, + image: vk::Image, + image_layout: vk::ImageLayout, + clear_value: &vk::ClearColorValue, + ranges: &[vk::ImageSubresourceRange], + ) { + unsafe { + self.pool.device().cmd_clear_color_image( + self.buffer, + image, + image_layout, + clear_value, + ranges, + ) + } + } + + pub unsafe fn clear_depth_stencil_image( + &self, + image: vk::Image, + image_layout: vk::ImageLayout, + clear_value: &vk::ClearDepthStencilValue, + ranges: &[vk::ImageSubresourceRange], + ) { + unsafe { + self.pool.device().cmd_clear_depth_stencil_image( + self.buffer, + image, + image_layout, + clear_value, + ranges, + ) + } + } + + pub unsafe fn pipeline_barrier( + &self, + src_stage_mask: vk::PipelineStageFlags, + dst_stage_mask: vk::PipelineStageFlags, + memory_barriers: &[vk::MemoryBarrier<'_>], + buffer_memory_barriers: &[vk::BufferMemoryBarrier<'_>], + image_memory_barriers: &[vk::ImageMemoryBarrier<'_>], + ) { + unsafe { + self.pool.device().cmd_pipeline_barrier( + self.buffer, + src_stage_mask, + dst_stage_mask, + vk::DependencyFlags::empty(), + memory_barriers, + buffer_memory_barriers, + image_memory_barriers, + ) + } + } + + pub unsafe fn begin_render_pass( + &self, + create_info: &vk::RenderPassBeginInfo<'_>, + contents: vk::SubpassContents, + ) { + unsafe { + self.pool + .device() + .cmd_begin_render_pass(self.buffer, create_info, contents); + } + } + + pub unsafe fn next_subpass(&self, contents: vk::SubpassContents) { + unsafe { + self.pool.device().cmd_next_subpass(self.buffer, contents); + } + } + + pub unsafe fn end_render_pass(&self) { + unsafe { + self.pool.device().cmd_end_render_pass(self.buffer); + } + } +} + +impl CommandEncoder for AshCommandEncoder<'_> { + #[inline] + fn insert_debug_marker(&mut self, label: &str) { + self.insert_debug_label(label); + } + #[inline] + fn push_debug_group(&mut self, label: &str) { + self.begin_debug_label(label) + } + #[inline] + fn pop_debug_group(&mut self) { + self.end_debug_label(); + } +} + +impl Drop for AshCommandEncoder<'_> { + fn drop(&mut self) { + if self.buffer != vk::CommandBuffer::null() { + self.pool.done_buffers.push(self.buffer); + self.buffer = vk::CommandBuffer::null(); + } + } +} + +pub struct SubmissionGroup { + wait_semaphores: Vec, + wait_semaphores_dst_stages: Vec, + command_buffers: Vec, + signal_semaphores: Vec, + queue: crossbeam_queue::SegQueue, +} + +impl SubmissionGroup { + #[inline] + pub fn new() -> Self { + Self { + wait_semaphores: Vec::new(), + wait_semaphores_dst_stages: Vec::new(), + command_buffers: Vec::new(), + signal_semaphores: Vec::new(), + queue: crossbeam_queue::SegQueue::new(), + } + } + + #[inline] + pub fn wait(&mut self, sem: vk::Semaphore, dst_stages: vk::PipelineStageFlags) -> &mut Self { + self.wait_semaphores.push(sem); + self.wait_semaphores_dst_stages.push(dst_stages); + self + } + + #[inline] + pub(crate) fn get_wait_semaphores(&self) -> &[vk::Semaphore] { + &self.wait_semaphores + } + + #[inline] + pub fn push(&mut self, buf: vk::CommandBuffer) -> &mut Self { + self.command_buffers.push(buf); + self + } + + #[inline] + pub fn flush_queue(&mut self) -> &mut Self { + while let Some(buf) = self.queue.pop() { + self.command_buffers.push(buf); + } + self + } + + #[inline] + pub fn signal(&mut self, sem: vk::Semaphore) -> &mut Self { + self.signal_semaphores.push(sem); + self + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.command_buffers.is_empty() + } + + pub fn submit_info(&self) -> vk::SubmitInfo<'_> { + vk::SubmitInfo::default() + .wait_semaphores(&self.wait_semaphores) + .wait_dst_stage_mask(&self.wait_semaphores_dst_stages) + .command_buffers(&self.command_buffers) + .signal_semaphores(&self.signal_semaphores) + } + + pub fn submit(&mut self, device: &AshDevice, fence: vk::Fence) -> VkResult<&mut Self> { + unsafe { + device.queue_submit(device.queues().graphics, &[self.submit_info()], fence)?; + } + self.reset(); + Ok(self) + } + + #[inline] + pub fn reset(&mut self) -> &mut Self { + self.wait_semaphores.clear(); + self.wait_semaphores_dst_stages.clear(); + self.command_buffers.clear(); + self.signal_semaphores.clear(); + self + } +} diff --git a/crates/render-ash/src/graph.rs b/crates/render-ash/src/graph.rs new file mode 100644 index 0000000..cdb655e --- /dev/null +++ b/crates/render-ash/src/graph.rs @@ -0,0 +1,392 @@ +use ash::vk::{self, PipelineStageFlags}; +use pulz_assets::Handle; +use pulz_render::{ + backend::PhysicalResourceResolver, + buffer::Buffer, + camera::RenderTarget, + draw::DrawPhases, + graph::{ + PassDescription, PassIndex, RenderGraph, + access::Access, + pass::PipelineBindPoint, + resources::{PhysicalResource, PhysicalResourceAccessTracker, PhysicalResources}, + }, + math::USize2, + pipeline::{ExtendedGraphicsPassDescriptor, GraphicsPass}, + texture::{Texture, TextureDescriptor, TextureDimensions, TextureFormat}, +}; +use pulz_window::WindowsMirror; +use tracing::debug; + +use crate::{ + Result, + convert::{VkInto, default_clear_value_for_format}, + encoder::{AshCommandPool, SubmissionGroup}, + resources::AshResources, + swapchain::AshSurfaceSwapchain, +}; + +pub struct AshRenderGraph { + physical_resources: PhysicalResources, + physical_resource_access: PhysicalResourceAccessTracker, + topo: Vec, + barriers: Vec, + hash: u64, +} + +#[derive(Default, Debug)] +pub struct TopoGroup { + render_passes: Vec, // pass-index + compute_passes: Vec, // sub-pass-index + ray_tracing_passes: Vec, // sub-pass-index +} + +#[derive(Debug)] +struct TopoRenderPass { + index: PassIndex, + render_pass: vk::RenderPass, + framebuffer: vk::Framebuffer, + attachment_resource_indices: Vec, + //framebuffers_cache: U64HashMap, + size: USize2, +} + +#[derive(Debug)] +pub struct Barrier { + image: Vec>, + buffer: Vec>, +} + +// implement Send+Sync manually, because vk::*MemoryBarrier have unused p_next pointers +// SAFETY: p_next pointers are not used +unsafe impl Send for Barrier {} +unsafe impl Sync for Barrier {} + +struct AshPhysicalResourceResolver<'a> { + submission_group: &'a mut SubmissionGroup, + res: &'a mut AshResources, + command_pool: &'a mut AshCommandPool, + surfaces: &'a mut WindowsMirror, +} + +impl PhysicalResourceResolver for AshPhysicalResourceResolver<'_> { + fn resolve_render_target( + &mut self, + render_target: &RenderTarget, + ) -> Option> { + match render_target { + RenderTarget::Image(_i) => todo!("implement resolve_render_target (image)"), + RenderTarget::Window(w) => { + let surface = self.surfaces.get_mut(*w).expect("resolve window"); + assert!(!surface.is_acquired()); + + let sem = self + .command_pool + .request_semaphore() + .expect("request semaphore"); + self.submission_group + .wait(sem, PipelineStageFlags::TOP_OF_PIPE); // TODO: better sync + let aquired_texture = surface + .acquire_next_image(self.res, sem) + .expect("aquire failed") + .expect("aquire failed(2)"); + + Some(PhysicalResource { + resource: aquired_texture.texture, + format: surface.texture_format(), + access: Access::PRESENT, + size: TextureDimensions::D2(surface.size()), + }) + } + } + } + + fn resolve_buffer(&mut self, _handle: &Handle) -> Option> { + todo!("implement resolve_buffer") + } + + fn create_transient_texture( + &mut self, + format: TextureFormat, + dimensions: TextureDimensions, + access: Access, + ) -> Option { + let t = self + .res + .create::(&TextureDescriptor { + format, + dimensions, + usage: access.as_texture_usage(), + ..Default::default() + }) + .ok()?; + // TODO: destroy texture + self.res.current_frame_garbage_mut().texture_handles.push(t); + // TODO: reuse textures + Some(t) + } + + fn create_transient_buffer(&mut self, _size: usize, _access: Access) -> Option { + // TODO: destroy buffers + // TODO: reuse buffers + todo!("implement create_transient_buffer") + } +} + +impl TopoRenderPass { + fn from_graph( + res: &mut AshResources, + src: &RenderGraph, + phys: &PhysicalResources, + current_access: &mut PhysicalResourceAccessTracker, + pass: &PassDescription, + ) -> Result { + let pass_descr = + ExtendedGraphicsPassDescriptor::from_graph(src, phys, current_access, pass).unwrap(); + let graphics_pass = res.create::(&pass_descr.graphics_pass)?; + let render_pass = res[graphics_pass]; + Ok(Self { + index: pass.index(), + render_pass, + framebuffer: vk::Framebuffer::null(), + attachment_resource_indices: pass_descr.resource_indices, + //framebuffers_cache: U64HashMap::default(), + size: pass_descr.size, + }) + } + + fn cleanup(&mut self, res: &mut AshResources) { + if self.framebuffer != vk::Framebuffer::null() { + res.current_frame_garbage_mut() + .framebuffers + .push(self.framebuffer); + self.framebuffer = vk::Framebuffer::null(); + } + /* + for (_, fb) in self.framebuffers_cache.drain() { + unsafe { + res.device().destroy_framebuffer(fb, None); + } + } + */ + } + + fn update_framebuffer( + &mut self, + res: &mut AshResources, + phys: &PhysicalResources, + ) -> Result<()> { + self.cleanup(res); + + let mut image_views = Vec::with_capacity(self.attachment_resource_indices.len()); + for &i in &self.attachment_resource_indices { + let physical_resource = phys.get_texture(i).expect("unassigned resource"); + let dim = physical_resource.size.subimage_extents(); + if dim != self.size { + // TODO: handle size changed! + // TODO: error handling + panic!("all framebuffer textures need to have the same dimensions"); + } + image_views.push(res[physical_resource.resource].1); + } + + /* + let mut hasher = DefaultHasher::new(); + image_views.hash(&mut hasher); + let key = hasher.finish(); + if let Some(fb) = self.framebuffers_cache.get(&key).copied() { + self.framebuffer = fb; + return Ok(()); + } + */ + + let create_info = vk::FramebufferCreateInfo::default() + .render_pass(self.render_pass) + // TODO + .attachments(&image_views) + .width(self.size.x) + .height(self.size.y) + .layers(1); + let fb = unsafe { res.device().create(&create_info)? }; + self.framebuffer = fb.take(); + //self.framebuffers_cache.insert(key, self.framebuffer); + Ok(()) + } +} + +impl AshRenderGraph { + #[inline] + pub const fn new() -> Self { + Self { + physical_resources: PhysicalResources::new(), + physical_resource_access: PhysicalResourceAccessTracker::new(), + topo: Vec::new(), + barriers: Vec::new(), + hash: 0, + } + } + + pub fn cleanup(&mut self, res: &mut AshResources) { + self.hash = 0; + for mut topo in self.topo.drain(..) { + for mut topo_render_pass in &mut topo.render_passes.drain(..) { + topo_render_pass.cleanup(res); + } + } + } + + pub fn update( + &mut self, + src_graph: &RenderGraph, + submission_group: &mut SubmissionGroup, + res: &mut AshResources, + command_pool: &mut AshCommandPool, + surfaces: &mut WindowsMirror, + ) -> Result { + let mut resolver = AshPhysicalResourceResolver { + submission_group, + res, + command_pool, + surfaces, + }; + // TODO: update render-pass, if resource-formats changed + // TODO: update framebuffer if render-pass or dimensions changed + let formats_changed = self + .physical_resources + .assign_physical(src_graph, &mut resolver); + if src_graph.was_updated() || src_graph.hash() != self.hash || formats_changed { + self.do_update(src_graph, res)?; + debug!( + "graph updated: topo={:?}, barriers={:?}, formats_changed={:?}", + self.topo, self.barriers, formats_changed, + ); + self.do_update_framebuffers(res)?; + Ok(true) + } else { + self.do_update_framebuffers(res)?; + Ok(false) + } + } + + fn do_update(&mut self, src: &RenderGraph, res: &mut AshResources) -> Result<()> { + self.cleanup(res); + self.hash = src.hash(); + self.barriers.clear(); + + let num_topological_groups = src.get_num_topological_groups(); + self.topo + .resize_with(num_topological_groups, Default::default); + + self.physical_resource_access + .reset(&self.physical_resources); + // TODO: get initial layout of external textures + + for topo_index in 0..num_topological_groups { + let topo_group = &mut self.topo[topo_index]; + for pass in src.get_topological_group(topo_index) { + match pass.bind_point() { + PipelineBindPoint::Graphics => { + topo_group.render_passes.push(TopoRenderPass::from_graph( + res, + src, + &self.physical_resources, + &mut self.physical_resource_access, + pass, + )?); + } + PipelineBindPoint::Compute => { + let range = pass.sub_pass_range(); + assert_eq!(range.start + 1, range.end); + topo_group.compute_passes.push(range.start); + } + PipelineBindPoint::RayTracing => { + let range = pass.sub_pass_range(); + assert_eq!(range.start + 1, range.end); + topo_group.ray_tracing_passes.push(range.start); + } + } + } + } + + Ok(()) + } + + fn do_update_framebuffers(&mut self, res: &mut AshResources) -> Result<()> { + for topo in &mut self.topo { + for topo_render_pass in &mut topo.render_passes { + topo_render_pass.update_framebuffer(res, &self.physical_resources)?; + } + } + Ok(()) + } + + pub fn execute( + &self, + src_graph: &RenderGraph, + submission_group: &mut SubmissionGroup, + command_pool: &mut AshCommandPool, + draw_phases: &DrawPhases, + ) -> Result<()> { + let mut encoder = command_pool.encoder()?; + let mut clear_values = Vec::new(); + for (topo_index, topo) in self.topo.iter().enumerate() { + // render-passes + for topo_render_pass in &topo.render_passes { + let pass = src_graph.get_pass(topo_render_pass.index).unwrap(); + let has_multiple_subpass = pass.sub_pass_range().len() > 1; + if has_multiple_subpass { + encoder.begin_debug_label(pass.name()); + } + clear_values.clear(); + for &i in &topo_render_pass.attachment_resource_indices { + let physical_resource = self.physical_resources.get_texture(i).unwrap(); + let format: vk::Format = physical_resource.format.vk_into(); + let clear_value = default_clear_value_for_format(format); + clear_values.push(clear_value); + } + unsafe { + // TODO: caching of framebuffer + // TODO: clear-values, render-area, ... + encoder.begin_render_pass( + &vk::RenderPassBeginInfo::default() + .render_pass(topo_render_pass.render_pass) + .framebuffer(topo_render_pass.framebuffer) + .clear_values(&clear_values) + .render_area( + vk::Rect2D::default() + .offset(vk::Offset2D { x: 0, y: 0 }) + .extent(topo_render_pass.size.vk_into()), + ), + vk::SubpassContents::INLINE, + ); + let mut first = true; + for subpass_index in pass.sub_pass_range() { + if first { + first = false; + } else { + encoder.next_subpass(vk::SubpassContents::INLINE); + } + let subpass = src_graph.get_subpass(subpass_index).unwrap(); + encoder.begin_debug_label(subpass.name()); + src_graph.execute_sub_pass(subpass_index, &mut encoder, draw_phases); + encoder.end_debug_label(); + } + encoder.end_render_pass(); + } + if has_multiple_subpass { + encoder.end_debug_label(); + } + } + // TODO: compute passes, raytracing-passes + + if let Some(barrier) = self.barriers.get(topo_index) { + // TODO: add barriers + todo!("implement barriers {barrier:?}"); + } + } + + encoder.submit(submission_group)?; + + Ok(()) + } +} diff --git a/crates/render-ash/src/instance.rs b/crates/render-ash/src/instance.rs new file mode 100644 index 0000000..5231a62 --- /dev/null +++ b/crates/render-ash/src/instance.rs @@ -0,0 +1,214 @@ +use std::{ffi::CStr, ops::Deref, os::raw::c_char, sync::Arc}; + +use ash::vk; +use tracing::{debug, warn}; + +use crate::{AshRendererFlags, ErrorNoExtension, Result, debug_utils}; + +pub const ENGINE_NAME: &CStr = + match CStr::from_bytes_with_nul(concat!(env!("CARGO_PKG_NAME"), "\0").as_bytes()) { + Ok(s) => s, + Err(_) => panic!("invalid CStr"), + }; +pub const ENGINE_VERSION: u32 = parse_version(env!("CARGO_PKG_VERSION")); +pub const VK_API_VERSION: u32 = vk::API_VERSION_1_1; + +pub struct AshInstance { + instance_raw: ash::Instance, + entry: ash::Entry, + instance_extensions: Vec<&'static CStr>, + debug_utils: Option, + ext_surface: Option, + flags: AshRendererFlags, +} + +impl Deref for AshInstance { + type Target = ash::Instance; + #[inline] + fn deref(&self) -> &ash::Instance { + &self.instance_raw + } +} + +impl AshInstance { + pub(crate) fn new(flags: AshRendererFlags) -> Result> { + let entry = unsafe { ash::Entry::load()? }; + let instance_extensions = get_instance_extensions(&entry, flags)?; + let instance_raw = create_instance(&entry, instance_extensions.iter().copied())?; + + let mut instance = Self { + entry, + instance_raw, + instance_extensions, + debug_utils: None, + ext_surface: None, + flags, + }; + + if instance.has_instance_extension(debug_utils::EXT_NAME) { + instance.debug_utils = Some(debug_utils::InstanceDebugUtils::new( + instance.entry(), + &instance, + // TODO: filter activated severities + vk::DebugUtilsMessageSeverityFlagsEXT::ERROR + | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING + | vk::DebugUtilsMessageSeverityFlagsEXT::INFO + | vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE, + )); + } + + if instance.has_instance_extension(ash::khr::surface::NAME) { + instance.ext_surface = Some(ash::khr::surface::Instance::new( + instance.entry(), + &instance, + )); + } + + Ok(Arc::new(instance)) + } + + #[inline] + pub fn entry(&self) -> &ash::Entry { + &self.entry + } + + #[inline] + pub fn has_instance_extension(&self, name: &CStr) -> bool { + self.instance_extensions.contains(&name) + } + + #[inline] + pub(crate) fn ext_surface(&self) -> Result<&ash::khr::surface::Instance, ErrorNoExtension> { + self.ext_surface + .as_ref() + .ok_or(ErrorNoExtension(ash::khr::surface::NAME)) + } + + #[inline] + pub(crate) fn debug_utils(&self) -> Result<&debug_utils::InstanceDebugUtils, ErrorNoExtension> { + self.debug_utils + .as_ref() + .ok_or(ErrorNoExtension(debug_utils::EXT_NAME)) + } + + #[inline] + pub fn flags(&self) -> AshRendererFlags { + self.flags + } +} + +impl Drop for AshInstance { + fn drop(&mut self) { + self.debug_utils = None; + unsafe { self.instance_raw.destroy_instance(None) } + } +} + +fn get_instance_extensions( + entry: &ash::Entry, + flags: AshRendererFlags, +) -> Result> { + let available_extensions = unsafe { entry.enumerate_instance_extension_properties(None)? }; + + let mut extensions = Vec::with_capacity(5); + extensions.push(ash::khr::surface::NAME); + + if cfg!(target_os = "windows") { + extensions.push(ash::khr::win32_surface::NAME); + } else if cfg!(target_os = "android") { + extensions.push(ash::khr::android_surface::NAME); + } else if cfg!(any(target_os = "macos", target_os = "ios")) { + extensions.push(ash::ext::metal_surface::NAME); + } else if cfg!(unix) { + extensions.push(ash::khr::xlib_surface::NAME); + extensions.push(ash::khr::xcb_surface::NAME); + extensions.push(ash::khr::wayland_surface::NAME); + } + + if flags.contains(AshRendererFlags::DEBUG) { + extensions.push(debug_utils::EXT_NAME); + } + + // Only keep available extensions. + extensions.retain(|&ext| { + if available_extensions + .iter() + .any(|avail_ext| unsafe { CStr::from_ptr(avail_ext.extension_name.as_ptr()) == ext }) + { + debug!("Instance extension ✅ YES {:?}", ext); + true + } else { + warn!("Instance extension ❌ NO {:?}", ext); + false + } + }); + + Ok(extensions) +} + +#[inline] +fn create_instance<'a>( + entry: &ash::Entry, + extensions: impl IntoIterator, +) -> Result { + let extensions_ptr: Vec<_> = extensions.into_iter().map(CStr::as_ptr).collect(); + _create_instance(entry, &extensions_ptr) +} + +fn _create_instance(entry: &ash::Entry, extensions_ptr: &[*const c_char]) -> Result { + let instance = unsafe { + entry.create_instance( + &vk::InstanceCreateInfo::default() + .application_info( + &vk::ApplicationInfo::default() + .application_name(ENGINE_NAME) + .application_version(ENGINE_VERSION) + .engine_name(ENGINE_NAME) + .engine_version(ENGINE_VERSION) + .api_version(VK_API_VERSION), + ) + .enabled_extension_names(extensions_ptr), + None, + )? + }; + Ok(instance) +} + +macro_rules! parse_int_iteration { + ($value:ident += $input:ident[$pos:expr]) => { + if $input.len() <= $pos { + return ($value, $pos); + } + $value *= 10; + let c = $input[$pos]; + if c < b'0' || c > b'9' { + if c != b'.' && c != b'-' { + panic!("invalid character in version"); + } + return ($value, $pos + 1); + } + $value += c as u32 - '0' as u32; + }; +} + +#[inline] +const fn const_parse_decimal_u32(input: &[u8], offset: usize) -> (u32, usize) { + let mut value = 0; + // manual unroll of loop for const compability + parse_int_iteration!(value += input[offset]); + parse_int_iteration!(value += input[offset + 1]); + parse_int_iteration!(value += input[offset + 2]); + parse_int_iteration!(value += input[offset + 3]); + parse_int_iteration!(value += input[offset + 4]); + parse_int_iteration!(value += input[offset + 5]); + (value, offset + 6) +} + +#[inline] +const fn parse_version(version: &str) -> u32 { + let version = version.as_bytes(); + let (major, i) = const_parse_decimal_u32(version, 0); + let (minor, j) = const_parse_decimal_u32(version, i); + let (patch, _) = const_parse_decimal_u32(version, j); + vk::make_api_version(0, major, minor, patch) +} diff --git a/crates/render-ash/src/lib.rs b/crates/render-ash/src/lib.rs new file mode 100644 index 0000000..84c17f6 --- /dev/null +++ b/crates/render-ash/src/lib.rs @@ -0,0 +1,570 @@ +#![warn( + // missing_docs, + // rustdoc::missing_doc_code_examples, + future_incompatible, + rust_2018_idioms, + unused, + trivial_casts, + trivial_numeric_casts, + unused_lifetimes, + unused_qualifications, + unused_crate_dependencies, + clippy::cargo, + clippy::multiple_crate_versions, + clippy::empty_line_after_outer_attr, + clippy::fallible_impl_from, + clippy::redundant_pub_crate, + clippy::use_self, + clippy::suspicious_operation_groupings, + clippy::useless_let_if_seq, + // clippy::missing_errors_doc, + // clippy::missing_panics_doc, + clippy::wildcard_imports +)] +#![doc(html_logo_url = "https://raw.githubusercontent.com/HellButcher/pulz/master/docs/logo.png")] +#![doc(html_no_source)] +#![doc = include_str!("../README.md")] + +use std::{backtrace::Backtrace, ffi::CStr, sync::Arc}; + +use ash::vk; +use bitflags::bitflags; +use convert::default_clear_color_value_for_format; +use device::AshDevice; +use encoder::{AshCommandPool, SubmissionGroup}; +use graph::AshRenderGraph; +use instance::AshInstance; +use pulz_ecs::prelude::*; +use pulz_render::{RenderModule, RenderSystemPhase, draw::DrawPhases, graph::RenderGraph}; +use resources::AshResources; +use thiserror::Error; + +mod alloc; +mod convert; +mod debug_utils; +mod device; +mod drop_guard; +mod encoder; +mod graph; +mod instance; +mod resources; +mod shader; +mod swapchain; + +use pulz_window::{ + DisplayHandle, Window, WindowHandle, WindowId, Windows, WindowsMirror, + listener::WindowSystemListener, +}; + +// wrapper object for printing backtrace, until provide() is stable +pub struct VkError { + result: vk::Result, + backtrace: Backtrace, +} + +impl From for VkError { + fn from(result: vk::Result) -> Self { + Self { + result, + backtrace: Backtrace::capture(), + } + } +} +impl std::error::Error for VkError {} +impl std::fmt::Debug for VkError { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + ::fmt(self, f) + } +} +impl std::fmt::Display for VkError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}\nVkResult backtrace:\n{}", + self.result, self.backtrace + ) + } +} + +#[derive(Error, Debug)] +#[non_exhaustive] +pub enum Error { + #[error("library loading error")] + LoadingError(#[from] ash::LoadingError), + + #[error("Vulkan driver does not support {0:?}")] + ExtensionNotSupported(&'static CStr), + + #[error("The used Window-System is not supported")] + UnsupportedWindowSystem, + + #[error("The window is not available, or it has no raw-window-handle")] + WindowNotAvailable, + + #[error("No suitable GPU adapters found on the system!")] + NoAdapter, + + #[error("Device doesn't have swapchain support")] + NoSwapchainSupport, + + #[error("Swapchain supports {supported:?}, but {requested:?} was requested")] + SwapchainUsageNotSupported { + requested: vk::ImageUsageFlags, + supported: vk::ImageUsageFlags, + }, + + #[error("The surface was lost")] + SurfaceLost, + + #[error("A next swapchain image was already acquired without beeing presented.")] + SwapchainImageAlreadyAcquired, + + #[error("Vulkan Error")] + VkError(#[from] VkError), + + #[error("Allocation Error")] + AllocationError(#[from] gpu_alloc::AllocationError), + + #[error("Serialization Error")] + SerializationError(Box), + + #[error("Deserialization Error")] + DeserializationError(Box), + + #[error("unknown renderer error")] + Unknown, +} + +#[derive(Debug)] +pub struct ErrorNoExtension(pub &'static CStr); + +impl From for Error { + #[inline] + fn from(e: ErrorNoExtension) -> Self { + Self::ExtensionNotSupported(e.0) + } +} +impl From for Error { + #[inline] + fn from(e: vk::Result) -> Self { + Self::from(VkError::from(e)) + } +} +impl From<&vk::Result> for Error { + #[inline] + fn from(e: &vk::Result) -> Self { + Self::from(*e) + } +} + +pub type Result = std::result::Result; + +struct AshRendererFull { + device: Arc, + res: AshResources, + frames: Vec, + current_frame: usize, + surfaces: WindowsMirror, + graph: AshRenderGraph, +} + +impl Drop for AshRendererFull { + fn drop(&mut self) { + self.graph.cleanup(&mut self.res); + self.res.wait_idle_and_clear_all().unwrap(); + self.destroy_all_swapchains().unwrap(); + self.frames.clear(); + } +} + +bitflags! { + /// Instance initialization flags. + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] + pub struct AshRendererFlags: u32 { + /// Generate debug information in shaders and objects. + const DEBUG = 1 << 0; + } +} + +struct Frame { + // TODO: multi-threaded command recording: CommandPool per thread + command_pool: AshCommandPool, + finished_fence: vk::Fence, // signaled ad end of command-cueue, waited at beginning of frame + finished_semaphore: vk::Semaphore, // semaphore used for presenting to the swapchain +} + +impl Frame { + pub const NUM_FRAMES_IN_FLIGHT: usize = 2; +} + +impl Frame { + unsafe fn create(device: &Arc) -> Result { + unsafe { + let command_pool = device.new_command_pool(device.queues().graphics_family)?; + let finished_fence = device + .create(&vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED))?; + let finished_semaphore = device.create(&vk::SemaphoreCreateInfo::default())?; + Ok(Self { + command_pool, + finished_fence: finished_fence.take(), + finished_semaphore: finished_semaphore.take(), + }) + } + } + + unsafe fn reset(&mut self, _device: &AshDevice) -> Result<(), vk::Result> { + unsafe { + self.command_pool.reset()?; + Ok(()) + } + } +} + +impl Drop for Frame { + fn drop(&mut self) { + unsafe { + let device = self.command_pool.device(); + if self.finished_fence != vk::Fence::null() { + device.destroy_fence(self.finished_fence, None); + } + if self.finished_semaphore != vk::Semaphore::null() { + device.destroy_semaphore(self.finished_semaphore, None); + } + } + } +} + +impl AshRendererFull { + fn from_device(device: Arc) -> Result { + let res = AshResources::new(&device, Frame::NUM_FRAMES_IN_FLIGHT)?; + Ok(Self { + device, + res, + frames: Vec::with_capacity(Frame::NUM_FRAMES_IN_FLIGHT), + current_frame: 0, + surfaces: WindowsMirror::new(), + graph: AshRenderGraph::new(), + }) + } + + fn begin_frame(&mut self) -> Result { + let _span = tracing::trace_span!("BeginFrame").entered(); + + if self.frames.is_empty() { + self.frames.reserve_exact(Frame::NUM_FRAMES_IN_FLIGHT); + for _ in 0..Frame::NUM_FRAMES_IN_FLIGHT { + self.frames.push(unsafe { Frame::create(&self.device)? }); + } + } + + let frame = &mut self.frames[self.current_frame]; + unsafe { + self.device + .wait_for_fences(&[frame.finished_fence], true, !0)?; + } + + // cleanup old frame + unsafe { + frame.reset(&self.device)?; + self.res.next_frame_and_clear_garbage(); + } + + Ok(SubmissionGroup::new()) + } + + fn render_frame( + &mut self, + submission_group: &mut SubmissionGroup, + src_graph: &RenderGraph, + phases: &DrawPhases, + ) -> Result<()> { + let _span = tracing::trace_span!("RunGraph").entered(); + let frame = &mut self.frames[self.current_frame]; + + let span_update = tracing::trace_span!("Update").entered(); + self.graph.update( + src_graph, + submission_group, + &mut self.res, + &mut frame.command_pool, + &mut self.surfaces, + )?; + drop(span_update); + + let span_exec = tracing::trace_span!("Execute").entered(); + self.graph + .execute(src_graph, submission_group, &mut frame.command_pool, phases)?; + drop(span_exec); + + Ok(()) + } + + // TODO: remove this! + fn clear_unacquired_swapchain_images( + &mut self, + submission_group: &mut SubmissionGroup, + ) -> Result<()> { + let unaquired_swapchains: Vec<_> = self + .surfaces + .iter() + .filter_map(|(id, s)| if s.is_acquired() { None } else { Some(id) }) + .collect(); + if unaquired_swapchains.is_empty() { + return Ok(()); + } + + // TODO: try to clear with empty render-pass + let _span = tracing::trace_span!("ClearImages").entered(); + + let mut images = Vec::with_capacity(unaquired_swapchains.len()); + for window_id in unaquired_swapchains.iter().copied() { + let sem = self.frames[self.current_frame] + .command_pool + .request_semaphore()?; + submission_group.wait(sem, vk::PipelineStageFlags::TRANSFER); + let surface = &mut self.surfaces[window_id]; + if let Some(acquired) = surface.acquire_next_image(&mut self.res, sem)? { + images.push(( + acquired.image, + default_clear_color_value_for_format(surface.format()), + )); + } + } + + let subrange = vk::ImageSubresourceRange::default() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .layer_count(vk::REMAINING_ARRAY_LAYERS) + .level_count(vk::REMAINING_MIP_LEVELS); + + let barriers = images + .iter() + .map(|(image, _)| { + vk::ImageMemoryBarrier::default() + .src_access_mask(vk::AccessFlags::empty()) + .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE) + .old_layout(vk::ImageLayout::UNDEFINED) + .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) + .subresource_range(subrange) + .image(*image) + }) + .collect::>(); + + let frame = &mut self.frames[self.current_frame]; + let encoder = frame.command_pool.encoder()?; + encoder.begin_debug_label("ClearImages"); + + unsafe { + encoder.pipeline_barrier( + vk::PipelineStageFlags::TRANSFER | vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, + vk::PipelineStageFlags::TRANSFER, + &[], + &[], + &barriers, + ); + } + + for (image, clear_color) in &images { + unsafe { + encoder.clear_color_image( + *image, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + clear_color, + &[subrange], + ) + } + } + + let barriers = images + .iter() + .map(|(image, _)| { + vk::ImageMemoryBarrier::default() + .src_access_mask(vk::AccessFlags::TRANSFER_WRITE) + .dst_access_mask(vk::AccessFlags::empty()) + .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) + .new_layout(vk::ImageLayout::PRESENT_SRC_KHR) + .subresource_range(subrange) + .image(*image) + }) + .collect::>(); + unsafe { + encoder.pipeline_barrier( + vk::PipelineStageFlags::TRANSFER, + vk::PipelineStageFlags::TRANSFER | vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, + &[], + &[], + &barriers, + ); + } + + encoder.submit(submission_group)?; + Ok(()) + } + + fn end_frame(&mut self, mut submission_group: SubmissionGroup) -> Result<()> { + let _span = tracing::trace_span!("EndFrame").entered(); + + self.clear_unacquired_swapchain_images(&mut submission_group)?; + + let acquired_swapchains = self.get_num_acquired_swapchains(); + let frame = &self.frames[self.current_frame]; + + unsafe { + self.device + .reset_fences(&[self.frames[self.current_frame].finished_fence])?; + } + + submission_group.flush_queue(); + if acquired_swapchains == 0 { + submission_group.submit(&self.device, frame.finished_fence)?; + } else { + submission_group + .signal(frame.finished_semaphore) + .submit(&self.device, frame.finished_fence)?; + + // TODO: tranform images to PRESENT layout + + self.present_acquired_swapchain_images(&[frame.finished_semaphore])?; + } + + let next_frame = self.current_frame; + self.current_frame = next_frame + 1; + if self.current_frame >= self.frames.len() { + self.current_frame = 0; + } + Ok(()) + } + + fn run(&mut self, windows: &mut Windows, src_graph: &RenderGraph, draw_phases: &DrawPhases) { + self.reconfigure_swapchains(windows); + + let mut submission_group = self.begin_frame().unwrap(); + + self.render_frame(&mut submission_group, src_graph, draw_phases) + .unwrap(); + self.end_frame(submission_group).unwrap(); + } +} + +#[allow(clippy::large_enum_variant)] +enum AshRendererInner { + Early(Arc), + Full(AshRendererFull), +} + +pub struct AshRenderer(AshRendererInner); + +impl AshRenderer { + #[inline] + pub fn new() -> Result { + Self::with_flags(AshRendererFlags::DEBUG) + } + + #[inline] + pub fn with_flags(flags: AshRendererFlags) -> Result { + let instance = AshInstance::new(flags)?; + Ok(Self(AshRendererInner::Early(instance))) + } + + fn init(&mut self) -> Result<&mut AshRendererFull> { + if let AshRendererInner::Early(instance) = &self.0 { + let device = instance.new_device(vk::SurfaceKHR::null())?; + let renderer = AshRendererFull::from_device(device)?; + self.0 = AshRendererInner::Full(renderer); + } + let AshRendererInner::Full(renderer) = &mut self.0 else { + unreachable!() + }; + Ok(renderer) + } + + /// UNSAFE: needs to ensure, window is kept alive while surface/swapchain is alive + unsafe fn init_window( + &mut self, + window_id: WindowId, + window: &Window, + display_handle: DisplayHandle<'_>, + window_handle: WindowHandle<'_>, + ) -> Result<&mut AshRendererFull> { + if let AshRendererInner::Full(renderer) = &mut self.0 { + let device = renderer.device.clone(); + // SAVETY: window is kept alive + let surface = unsafe { + device + .instance() + .new_surface(display_handle, window_handle)? + }; + renderer.init_swapchain(window_id, window, surface)?; + } else if let AshRendererInner::Early(instance) = &self.0 { + // SAVETY: window is kept alive + let surface = unsafe { instance.new_surface(display_handle, window_handle)? }; + let device = instance.new_device(surface.raw())?; + let mut renderer = AshRendererFull::from_device(device)?; + renderer.init_swapchain(window_id, window, surface)?; + self.0 = AshRendererInner::Full(renderer); + } + let AshRendererInner::Full(renderer) = &mut self.0 else { + unreachable!() + }; + Ok(renderer) + } + + fn run(&mut self, windows: &mut Windows, src_graph: &RenderGraph, draw_phases: &DrawPhases) { + if let AshRendererInner::Full(renderer) = &mut self.0 { + renderer.run(windows, src_graph, draw_phases); + } else { + panic!("renderer uninitialized"); + } + } +} + +impl WindowSystemListener for AshRenderer { + fn on_created( + &mut self, + window_id: WindowId, + window: &Window, + display_handle: DisplayHandle<'_>, + window_handle: WindowHandle<'_>, + ) { + // SAVETY: surface/swapchain is desproyed on_suspenden/on_close + unsafe { + self.init_window(window_id, window, display_handle, window_handle) + .unwrap(); + } + } + fn on_resumed(&mut self) { + self.init().unwrap(); + } + fn on_closed(&mut self, window_id: WindowId) { + let AshRendererInner::Full(renderer) = &mut self.0 else { + return; + }; + renderer.destroy_swapchain(window_id).unwrap(); + } + fn on_suspended(&mut self) { + let AshRendererInner::Full(renderer) = &mut self.0 else { + return; + }; + renderer.destroy_all_swapchains().unwrap(); + } +} + +impl ModuleWithOutput for AshRenderer { + type Output<'l> = &'l mut Self; + + fn install_modules(&self, res: &mut Resources) { + res.install(RenderModule); + } + + fn install_resources(self, res: &mut Resources) -> &mut Self { + let resource_id = res.insert_unsend(self); + res.init_meta_id::(resource_id); + res.get_mut_id(resource_id).unwrap() + } + + fn install_systems(schedule: &mut Schedule) { + schedule + .add_system(Self::run) + .into_phase(RenderSystemPhase::Render); + } +} diff --git a/crates/render-ash/src/resources/mod.rs b/crates/render-ash/src/resources/mod.rs new file mode 100644 index 0000000..118c66b --- /dev/null +++ b/crates/render-ash/src/resources/mod.rs @@ -0,0 +1,283 @@ +use std::{ops::Index, sync::Arc}; + +use ash::vk; +use pulz_render::{ + buffer::Buffer, + pipeline::{ + BindGroupLayout, ComputePipeline, GraphicsPass, GraphicsPipeline, PipelineLayout, + RayTracingPipeline, + }, + shader::ShaderModule, + texture::Texture, + utils::hash::U64HashMap, +}; +use slotmap::SlotMap; + +use crate::{ + Result, + alloc::{AshAllocator, GpuMemoryBlock}, + device::AshDevice, + instance::AshInstance, +}; + +mod replay; +mod resource_impl; +mod traits; + +use self::{ + replay::RecordResource, + traits::{ + AshGpuResource, AshGpuResourceCollection, AshGpuResourceCreate, AshGpuResourceRemove, + }, +}; +pub struct AshResources { + pub alloc: AshAllocator, + record: Option>, + pipeline_cache: vk::PipelineCache, + graphics_passes_cache: U64HashMap, + shader_modules_cache: U64HashMap, + bind_group_layouts_cache: U64HashMap, + pipeline_layouts_cache: U64HashMap, + graphics_pipelines_cache: U64HashMap, + compute_pipelines_cache: U64HashMap, + ray_tracing_pipelines_cache: U64HashMap, + pub graphics_passes: SlotMap, + pub shader_modules: SlotMap, + pub bind_group_layouts: SlotMap, + pub pipeline_layouts: SlotMap, + pub graphics_pipelines: SlotMap, + pub compute_pipelines: SlotMap, + pub ray_tracing_pipelines: SlotMap, + pub buffers: SlotMap)>, + pub textures: SlotMap)>, + frame_garbage: Vec, + current_frame: usize, +} + +#[derive(Debug, Default)] +pub struct AshFrameGarbage { + pub texture_handles: Vec, + pub buffer_handles: Vec, + pub buffers: Vec, + pub images: Vec, + pub image_views: Vec, + pub framebuffers: Vec, + pub swapchains: Vec, + pub memory: Vec, +} + +impl AshResources { + pub fn new(device: &Arc, num_frames_in_flight: usize) -> Result { + let alloc = AshAllocator::new(device)?; + let mut frame_garbage = Vec::with_capacity(num_frames_in_flight); + frame_garbage.resize_with(num_frames_in_flight, AshFrameGarbage::default); + Ok(Self { + alloc, + record: None, + graphics_passes_cache: U64HashMap::default(), + shader_modules_cache: U64HashMap::default(), + bind_group_layouts_cache: U64HashMap::default(), + pipeline_layouts_cache: U64HashMap::default(), + graphics_pipelines_cache: U64HashMap::default(), + compute_pipelines_cache: U64HashMap::default(), + ray_tracing_pipelines_cache: U64HashMap::default(), + pipeline_cache: vk::PipelineCache::null(), + graphics_passes: SlotMap::with_key(), + shader_modules: SlotMap::with_key(), + bind_group_layouts: SlotMap::with_key(), + pipeline_layouts: SlotMap::with_key(), + graphics_pipelines: SlotMap::with_key(), + compute_pipelines: SlotMap::with_key(), + ray_tracing_pipelines: SlotMap::with_key(), + buffers: SlotMap::with_key(), + textures: SlotMap::with_key(), + frame_garbage, + current_frame: 0, + }) + } + + #[inline] + pub fn instance(&self) -> &AshInstance { + self.alloc.instance() + } + + #[inline] + pub fn device(&self) -> &AshDevice { + self.alloc.device() + } + + pub fn with_pipeline_cache(mut self, initial_data: &[u8]) -> Result { + self.set_pipeline_cache(initial_data)?; + Ok(self) + } + + pub fn set_pipeline_cache(&mut self, initial_data: &[u8]) -> Result<()> { + unsafe { + if self.pipeline_cache != vk::PipelineCache::null() { + self.alloc + .device() + .destroy_pipeline_cache(self.pipeline_cache, None); + self.pipeline_cache = vk::PipelineCache::null(); + } + self.pipeline_cache = self.alloc.device().create_pipeline_cache( + &vk::PipelineCacheCreateInfo::default().initial_data(initial_data), + None, + )?; + } + Ok(()) + } + + pub fn get_pipeline_cache_data(&self) -> Result> { + if self.pipeline_cache == vk::PipelineCache::null() { + return Ok(Vec::new()); + } + unsafe { + let data = self + .alloc + .device() + .get_pipeline_cache_data(self.pipeline_cache)?; + Ok(data) + } + } + + #[inline] + pub fn create(&mut self, descriptor: &R::Descriptor<'_>) -> Result + where + R: AshGpuResourceCreate, + { + R::create(self, descriptor) + } + + #[inline] + pub fn get_raw(&self, key: R) -> Option<&R::Raw> + where + R: AshGpuResourceCreate, + { + R::slotmap(self).get(key) + } + + #[inline] + pub fn destroy(&mut self, key: R) -> bool + where + R: AshGpuResourceRemove, + { + R::destroy(self, key) + } + + pub(crate) fn wait_idle_and_clear_garbage(&mut self) -> Result<()> { + unsafe { + self.alloc.device().device_wait_idle()?; + for frame in 0..self.frame_garbage.len() { + self.clear_garbage(frame); + } + } + Ok(()) + } + + pub(crate) fn wait_idle_and_clear_all(&mut self) -> Result<()> { + self.wait_idle_and_clear_garbage()?; + // SAFETY: clear save, because clear garbage waits until device is idle + unsafe { + self.ray_tracing_pipelines.clear_destroy(&mut self.alloc); + self.ray_tracing_pipelines_cache.clear(); + self.compute_pipelines.clear_destroy(&mut self.alloc); + self.compute_pipelines_cache.clear(); + self.graphics_pipelines.clear_destroy(&mut self.alloc); + self.graphics_pipelines_cache.clear(); + self.pipeline_layouts.clear_destroy(&mut self.alloc); + self.pipeline_layouts_cache.clear(); + self.bind_group_layouts.clear_destroy(&mut self.alloc); + self.bind_group_layouts_cache.clear(); + self.shader_modules.clear_destroy(&mut self.alloc); + self.shader_modules_cache.clear(); + self.graphics_passes.clear_destroy(&mut self.alloc); + self.graphics_passes_cache.clear(); + self.textures.clear_destroy(&mut self.alloc); + self.buffers.clear_destroy(&mut self.alloc); + } + Ok(()) + } + + #[inline] + pub(crate) fn current_frame_garbage_mut(&mut self) -> &mut AshFrameGarbage { + &mut self.frame_garbage[self.current_frame] + } + + pub(crate) unsafe fn clear_garbage(&mut self, frame: usize) { + unsafe { + let garbage = &mut self.frame_garbage[frame]; + let mut textures = std::mem::take(&mut garbage.texture_handles); + for texture in textures.drain(..) { + if let Some(raw) = self.textures.remove(texture) { + Texture::put_to_garbage(garbage, raw) + } + } + garbage.texture_handles = textures; + let mut buffers = std::mem::take(&mut garbage.buffer_handles); + for buffer in buffers.drain(..) { + if let Some(raw) = self.buffers.remove(buffer) { + Buffer::put_to_garbage(garbage, raw) + } + } + garbage.buffer_handles = buffers; + garbage.clear_frame(&mut self.alloc); + } + } + + /// # SAFETY + /// caller must ensure, that the next frame has finished + pub(crate) unsafe fn next_frame_and_clear_garbage(&mut self) { + unsafe { + self.current_frame = (self.current_frame + 1) % self.frame_garbage.len(); + self.clear_garbage(self.current_frame); + } + } +} + +impl AshFrameGarbage { + unsafe fn clear_frame(&mut self, alloc: &mut AshAllocator) { + unsafe { + let device = alloc.device(); + self.framebuffers + .drain(..) + .for_each(|r| device.destroy_framebuffer(r, None)); + self.image_views + .drain(..) + .for_each(|r| device.destroy_image_view(r, None)); + self.images + .drain(..) + .for_each(|r| device.destroy_image(r, None)); + self.buffers + .drain(..) + .for_each(|r| device.destroy_buffer(r, None)); + if let Ok(ext_swapchain) = device.ext_swapchain() { + self.swapchains + .drain(..) + .for_each(|r| ext_swapchain.destroy_swapchain(r, None)); + } + self.memory.drain(..).for_each(|r| alloc.dealloc(r)); + } + } +} + +impl Index for AshResources { + type Output = R::Raw; + #[inline] + fn index(&self, index: R) -> &Self::Output { + R::slotmap(self).get(index).expect("invalid resource") + } +} + +impl Drop for AshResources { + #[inline] + fn drop(&mut self) { + self.wait_idle_and_clear_all().unwrap(); + if self.pipeline_cache != vk::PipelineCache::null() { + unsafe { + self.alloc + .device() + .destroy_pipeline_cache(self.pipeline_cache, None); + } + } + } +} diff --git a/crates/render-ash/src/resources/replay.rs b/crates/render-ash/src/resources/replay.rs new file mode 100644 index 0000000..689dd35 --- /dev/null +++ b/crates/render-ash/src/resources/replay.rs @@ -0,0 +1,206 @@ +use std::borrow::Cow; + +use pulz_render::{ + backend::GpuResource, + pipeline::{ + BindGroupLayout, BindGroupLayoutDescriptor, ComputePipeline, ComputePipelineDescriptor, + GraphicsPass, GraphicsPassDescriptor, GraphicsPipeline, GraphicsPipelineDescriptor, + PipelineLayout, PipelineLayoutDescriptor, RayTracingPipeline, RayTracingPipelineDescriptor, + }, + shader::{ShaderModule, ShaderModuleDescriptor}, + utils::serde_slots::SlotDeserializationMapper, +}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use slotmap::Key; + +use super::AshResources; +use crate::{Error, Result}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum ReplayResourceDescr<'a, 'b> { + GraphicsPass(Cow<'a, GraphicsPassDescriptor>), + #[serde(borrow)] + ShaderModule(Cow<'a, ShaderModuleDescriptor<'b>>), + BindGroupLayout(Cow<'a, BindGroupLayoutDescriptor<'b>>), + PipelineLayout(Cow<'a, PipelineLayoutDescriptor<'b>>), + GraphicsPipeline(Cow<'a, GraphicsPipelineDescriptor<'b>>), + ComputePipeline(Cow<'a, ComputePipelineDescriptor<'b>>), + RayTracingPipeline(Cow<'a, RayTracingPipelineDescriptor<'b>>), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(super) struct ReplayResourceDescrEntry<'a, 'b>( + u64, + #[serde(borrow)] ReplayResourceDescr<'a, 'b>, +); + +pub(super) trait AsResourceRecord: GpuResource { + fn as_record<'a, 'b: 'a>( + &self, + descr: &'a Self::Descriptor<'b>, + ) -> ReplayResourceDescrEntry<'a, 'b>; +} + +macro_rules! define_replay_resources { + ($($r:ident),*) => { + + impl ReplayResourceDescr<'_, '_> { + fn create( + &self, + res: &mut AshResources, + id: u64, + mapper: &mut SlotDeserializationMapper, + ) -> Result<()> { + match self { + $( + Self::$r(d) => { + let key = res.create::<$r>(d)?; + mapper.define(id, key); + }, + )* + } + Ok(()) + } + } + + $( + impl AsResourceRecord for $r { + fn as_record<'a, 'b: 'a>(&self, descr: &'a Self::Descriptor<'b>) -> ReplayResourceDescrEntry<'a, 'b> { + ReplayResourceDescrEntry(self.data().as_ffi(), ReplayResourceDescr::$r(Cow::Borrowed(descr))) + } + } + )* + }; +} + +define_replay_resources!( + GraphicsPass, + ShaderModule, + BindGroupLayout, + PipelineLayout, + GraphicsPipeline, + ComputePipeline, + RayTracingPipeline +); + +pub(super) trait RecordResource: 'static { + fn record(&mut self, record: ReplayResourceDescrEntry<'_, '_>) -> Result<()>; + + fn end(&mut self) -> Result<()>; +} + +pub struct NoopRecorder; + +impl RecordResource for NoopRecorder { + fn record(&mut self, _record: ReplayResourceDescrEntry<'_, '_>) -> Result<()> { + Ok(()) + } + fn end(&mut self) -> Result<()> { + Ok(()) + } +} + +struct Recorder(Option); + +impl RecordResource for Recorder +where + S: 'static, +{ + fn record(&mut self, record: ReplayResourceDescrEntry<'_, '_>) -> Result<()> { + use serde::ser::SerializeSeq; + let Some(seq) = &mut self.0 else { + return Ok(()); + }; + if let Err(e) = seq.serialize_element(&record) { + Err(Error::SerializationError(Box::new(e))) + } else { + Ok(()) + } + } + fn end(&mut self) -> Result<()> { + use serde::ser::SerializeSeq; + let Some(seq) = self.0.take() else { + return Ok(()); + }; + + if let Err(e) = seq.end() { + Err(Error::SerializationError(Box::new(e))) + } else { + Ok(()) + } + } +} + +impl Drop for Recorder { + fn drop(&mut self) { + use serde::ser::SerializeSeq; + let Some(seq) = self.0.take() else { + return; + }; + seq.end() + .expect("recording not ended, and produced an error"); + } +} + +impl AshResources { + #[inline] + pub fn replay<'de, D>(&mut self, deserializer: D) -> Result<()> + where + D: Deserializer<'de>, + D::Error: 'static, + { + struct VisitResources<'l>(&'l mut AshResources, &'l mut Option); + impl<'de> serde::de::Visitor<'de> for VisitResources<'_> { + type Value = (); + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("a sequence") + } + + fn visit_seq(self, mut seq: A) -> std::result::Result + where + A: serde::de::SeqAccess<'de>, + { + SlotDeserializationMapper::with(|mapper| { + while let Some(ReplayResourceDescrEntry(id, elem)) = seq.next_element()? { + if let Err(error) = elem.create(self.0, id, mapper) { + *self.1 = Some(error); + return Err(serde::de::Error::custom("Unable to create resource")); + } + } + Ok(()) + }) + } + } + let mut error = None; + let result = deserializer.deserialize_seq(VisitResources(self, &mut error)); + if let Some(error) = error { + return Err(error); + } + if let Err(error) = result { + return Err(Error::DeserializationError(Box::new(error))); + } + Ok(()) + } + + pub fn start_recording( + &mut self, + serializer: S, + ) -> Result<(), S::Error> { + assert!( + self.record.is_none(), + "there is already an active recording session" + ); + let seq = serializer.serialize_seq(None)?; + self.record.replace(Box::new(Recorder::(Some(seq)))); + Ok(()) + } + + pub fn end_recording(&mut self) -> Result { + if let Some(mut record) = self.record.take() { + record.end()?; + Ok(true) + } else { + Ok(false) + } + } +} diff --git a/crates/render-ash/src/resources/resource_impl.rs b/crates/render-ash/src/resources/resource_impl.rs new file mode 100644 index 0000000..08bdaaf --- /dev/null +++ b/crates/render-ash/src/resources/resource_impl.rs @@ -0,0 +1,427 @@ +use ash::vk; +use pulz_render::{ + buffer::Buffer, + pipeline::{ + BindGroupLayout, ComputePipeline, GraphicsPass, GraphicsPipeline, PipelineLayout, + RayTracingPipeline, + }, + shader::ShaderModule, + texture::Texture, +}; +use slotmap::SlotMap; + +use super::{ + AshResources, U64HashMap, + traits::{AshGpuResource, AshGpuResourceCached, AshGpuResourceCreate, AshGpuResourceRemove}, +}; +use crate::{ + Result, + alloc::{AshAllocator, GpuMemoryBlock}, + convert::{CreateInfoConverter2, CreateInfoConverter6, VkInto}, + shader::compie_into_spv, +}; + +impl AshGpuResource for Buffer { + type Raw = (vk::Buffer, Option); + fn slotmap(res: &AshResources) -> &SlotMap { + &res.buffers + } + + fn slotmap_mut(res: &mut AshResources) -> &mut SlotMap { + &mut res.buffers + } + unsafe fn create_raw( + res: &mut AshResources, + descr: &Self::Descriptor<'_>, + ) -> Result { + unsafe { + let alloc = &mut res.alloc; + let device = alloc.device_arc(); + let create_info: vk::BufferCreateInfo<'static> = descr.vk_into(); + let buf = device.create(&create_info)?; + let mreq = device.get_buffer_memory_requirements(buf.raw()); + let mem = alloc.alloc(gpu_alloc::Request { + size: mreq.size, + align_mask: mreq.alignment, + usage: gpu_alloc::UsageFlags::FAST_DEVICE_ACCESS, + memory_types: mreq.memory_type_bits, + })?; + device.bind_buffer_memory(buf.raw(), *mem.memory(), mem.offset())?; + Ok((buf.take(), Some(mem.take()))) + } + } + unsafe fn destroy_raw(alloc: &mut AshAllocator, (buf, mem): Self::Raw) { + unsafe { + if buf != vk::Buffer::null() { + alloc.device().destroy_buffer(buf, None); + } + if let Some(mem) = mem { + alloc.dealloc(mem); + } + } + } +} +impl AshGpuResourceCreate for Buffer {} +impl AshGpuResourceRemove for Buffer { + fn put_to_garbage(garbage: &mut super::AshFrameGarbage, (buf, mem): Self::Raw) { + if buf != vk::Buffer::null() { + garbage.buffers.push(buf); + } + if let Some(mem) = mem { + garbage.memory.push(mem); + } + } +} + +impl AshGpuResource for Texture { + type Raw = (vk::Image, vk::ImageView, Option); + fn slotmap(res: &AshResources) -> &SlotMap { + &res.textures + } + + fn slotmap_mut(res: &mut AshResources) -> &mut SlotMap { + &mut res.textures + } + unsafe fn create_raw( + res: &mut AshResources, + descr: &Self::Descriptor<'_>, + ) -> Result { + unsafe { + let alloc = &mut res.alloc; + let device = alloc.device_arc(); + let img_create_info: vk::ImageCreateInfo<'static> = descr.vk_into(); + let img = device.create(&img_create_info)?; + let mreq = device.get_image_memory_requirements(img.raw()); + let mem = alloc.alloc(gpu_alloc::Request { + size: mreq.size, + align_mask: mreq.alignment, + usage: gpu_alloc::UsageFlags::FAST_DEVICE_ACCESS, + memory_types: mreq.memory_type_bits, + })?; + device.bind_image_memory(img.raw(), *mem.memory(), mem.offset())?; + let mut view_create_info: vk::ImageViewCreateInfo<'static> = descr.vk_into(); + view_create_info.image = img.raw(); + let view = device.create(&view_create_info)?; + Ok((img.take(), view.take(), Some(mem.take()))) + } + } + + unsafe fn destroy_raw(alloc: &mut AshAllocator, (img, view, mem): Self::Raw) { + unsafe { + if view != vk::ImageView::null() { + alloc.device().destroy(view); + } + if img != vk::Image::null() { + alloc.device().destroy(img); + } + if let Some(mem) = mem { + alloc.dealloc(mem); + } + } + } +} +impl AshGpuResourceCreate for Texture {} +impl AshGpuResourceRemove for Texture { + fn put_to_garbage(garbage: &mut super::AshFrameGarbage, (image, image_view, mem): Self::Raw) { + if image != vk::Image::null() { + garbage.images.push(image); + } + if image_view != vk::ImageView::null() { + garbage.image_views.push(image_view); + } + if let Some(mem) = mem { + garbage.memory.push(mem); + } + } +} + +impl AshGpuResource for GraphicsPass { + type Raw = vk::RenderPass; + fn slotmap(res: &AshResources) -> &SlotMap { + &res.graphics_passes + } + fn slotmap_mut(res: &mut AshResources) -> &mut SlotMap { + &mut res.graphics_passes + } + unsafe fn create_raw( + res: &mut AshResources, + descr: &Self::Descriptor<'_>, + ) -> Result { + unsafe { + let mut conv = CreateInfoConverter6::new(); + let create_info = conv.graphics_pass(descr); + let raw = res.device().create(create_info)?; + Ok(raw.take()) + } + } + unsafe fn destroy_raw(alloc: &mut AshAllocator, raw: Self::Raw) { + unsafe { + if raw != vk::RenderPass::null() { + alloc.device().destroy(raw); + } + } + } +} +impl AshGpuResourceCached for GraphicsPass { + #[inline] + fn get_hashs_mut(res: &mut AshResources) -> &mut U64HashMap { + &mut res.graphics_passes_cache + } +} +impl AshGpuResource for ShaderModule { + type Raw = vk::ShaderModule; + fn slotmap(res: &AshResources) -> &SlotMap { + &res.shader_modules + } + fn slotmap_mut(res: &mut AshResources) -> &mut SlotMap { + &mut res.shader_modules + } + unsafe fn create_raw( + res: &mut AshResources, + descr: &Self::Descriptor<'_>, + ) -> Result { + unsafe { + let code = compie_into_spv(&descr.source)?; + let create_info = vk::ShaderModuleCreateInfo::default().code(&code); + let raw = res.device().create(&create_info)?; + if let Some(label) = descr.label { + res.device().object_name(raw.raw(), label); + } + Ok(raw.take()) + } + } + unsafe fn destroy_raw(alloc: &mut AshAllocator, raw: Self::Raw) { + unsafe { + if raw != vk::ShaderModule::null() { + alloc.device().destroy(raw); + } + } + } +} +impl AshGpuResourceCached for ShaderModule { + #[inline] + fn get_hashs_mut(res: &mut AshResources) -> &mut U64HashMap { + &mut res.shader_modules_cache + } +} +impl AshGpuResource for BindGroupLayout { + type Raw = vk::DescriptorSetLayout; + fn slotmap(res: &AshResources) -> &SlotMap { + &res.bind_group_layouts + } + fn slotmap_mut(res: &mut AshResources) -> &mut SlotMap { + &mut res.bind_group_layouts + } + + unsafe fn create_raw( + res: &mut AshResources, + descr: &Self::Descriptor<'_>, + ) -> Result { + unsafe { + let mut conv = CreateInfoConverter2::new(); + let create_info = conv.bind_group_layout(descr); + let raw = res.device().create(create_info)?; + if let Some(label) = descr.label { + res.device().object_name(raw.raw(), label); + } + Ok(raw.take()) + } + } + unsafe fn destroy_raw(alloc: &mut AshAllocator, raw: Self::Raw) { + unsafe { + if raw != vk::DescriptorSetLayout::null() { + alloc.device().destroy(raw); + } + } + } +} +impl AshGpuResourceCached for BindGroupLayout { + #[inline] + fn get_hashs_mut(res: &mut AshResources) -> &mut U64HashMap { + &mut res.bind_group_layouts_cache + } +} +impl AshGpuResource for PipelineLayout { + type Raw = vk::PipelineLayout; + fn slotmap(res: &AshResources) -> &SlotMap { + &res.pipeline_layouts + } + fn slotmap_mut(res: &mut AshResources) -> &mut SlotMap { + &mut res.pipeline_layouts + } + + unsafe fn create_raw( + res: &mut AshResources, + descr: &Self::Descriptor<'_>, + ) -> Result { + unsafe { + let mut conv = CreateInfoConverter2::new(); + let create_info = conv.pipeline_layout(res, descr); + let raw = res.device().create(create_info)?; + if let Some(label) = descr.label { + res.device().object_name(raw.raw(), label); + } + Ok(raw.take()) + } + } + unsafe fn destroy_raw(alloc: &mut AshAllocator, raw: Self::Raw) { + unsafe { + if raw != vk::PipelineLayout::null() { + alloc.device().destroy(raw); + } + } + } +} +impl AshGpuResourceCached for PipelineLayout { + #[inline] + fn get_hashs_mut(res: &mut AshResources) -> &mut U64HashMap { + &mut res.pipeline_layouts_cache + } +} +impl AshGpuResource for GraphicsPipeline { + type Raw = vk::Pipeline; + fn slotmap(res: &AshResources) -> &SlotMap { + &res.graphics_pipelines + } + fn slotmap_mut(res: &mut AshResources) -> &mut SlotMap { + &mut res.graphics_pipelines + } + + unsafe fn create_raw( + res: &mut AshResources, + descr: &Self::Descriptor<'_>, + ) -> Result { + unsafe { + let mut conv = CreateInfoConverter2::new(); + let create_infos = conv.graphics_pipeline_descriptor(res, std::slice::from_ref(descr)); + match res + .device() + .create_graphics_pipelines(res.pipeline_cache, create_infos, None) + { + Ok(raw) => { + let raw = res.device().hold(raw[0]); + if let Some(label) = descr.label { + res.device().object_name(raw.raw(), label); + } + Ok(raw.take()) + } + Err((pipelines, e)) => { + res.device().destroy(pipelines); + Err(e.into()) + } + } + } + } + + unsafe fn destroy_raw(alloc: &mut AshAllocator, raw: Self::Raw) { + unsafe { + if raw != vk::Pipeline::null() { + alloc.device().destroy_pipeline(raw, None); + } + } + } +} +impl AshGpuResourceCached for GraphicsPipeline { + #[inline] + fn get_hashs_mut(res: &mut AshResources) -> &mut U64HashMap { + &mut res.graphics_pipelines_cache + } +} +impl AshGpuResource for ComputePipeline { + type Raw = vk::Pipeline; + fn slotmap(res: &AshResources) -> &SlotMap { + &res.compute_pipelines + } + fn slotmap_mut(res: &mut AshResources) -> &mut SlotMap { + &mut res.compute_pipelines + } + + unsafe fn create_raw( + res: &mut AshResources, + descr: &Self::Descriptor<'_>, + ) -> Result { + unsafe { + let mut conv = CreateInfoConverter2::new(); + let create_infos = conv.compute_pipeline_descriptor(res, std::slice::from_ref(descr)); + match res + .device() + .create_compute_pipelines(res.pipeline_cache, create_infos, None) + { + Ok(raw) => { + let raw = res.device().hold(raw[0]); + if let Some(label) = descr.label { + res.device().object_name(raw.raw(), label); + } + Ok(raw.take()) + } + Err((pipelines, e)) => { + res.device().destroy(pipelines); + Err(e.into()) + } + } + } + } + + unsafe fn destroy_raw(alloc: &mut AshAllocator, raw: Self::Raw) { + unsafe { + if raw != vk::Pipeline::null() { + alloc.device().destroy_pipeline(raw, None); + } + } + } +} +impl AshGpuResourceCached for ComputePipeline { + #[inline] + fn get_hashs_mut(res: &mut AshResources) -> &mut U64HashMap { + &mut res.compute_pipelines_cache + } +} +impl AshGpuResource for RayTracingPipeline { + type Raw = vk::Pipeline; + + fn slotmap(res: &AshResources) -> &SlotMap { + &res.ray_tracing_pipelines + } + + fn slotmap_mut(res: &mut AshResources) -> &mut SlotMap { + &mut res.ray_tracing_pipelines + } + unsafe fn create_raw( + res: &mut AshResources, + descr: &Self::Descriptor<'_>, + ) -> Result { + unsafe { + let ext = res.device().ext_raytracing_pipeline()?; + let mut conv = CreateInfoConverter2::new(); + let create_infos = + conv.ray_tracing_pipeline_descriptor(res, std::slice::from_ref(descr)); + let raw = ext + .create_ray_tracing_pipelines( + vk::DeferredOperationKHR::null(), + res.pipeline_cache, + create_infos, + None, + ) + .map_err(|(_, e)| e)?; + let raw = res.device().hold(raw[0]); + if let Some(label) = descr.label { + res.device().object_name(raw.raw(), label); + } + Ok(raw.take()) + } + } + + unsafe fn destroy_raw(res: &mut AshAllocator, raw: Self::Raw) { + unsafe { + if raw != vk::Pipeline::null() { + res.device().destroy_pipeline(raw, None); + } + } + } +} +impl AshGpuResourceCached for RayTracingPipeline { + #[inline] + fn get_hashs_mut(res: &mut AshResources) -> &mut U64HashMap { + &mut res.ray_tracing_pipelines_cache + } +} diff --git a/crates/render-ash/src/resources/traits.rs b/crates/render-ash/src/resources/traits.rs new file mode 100644 index 0000000..ec73217 --- /dev/null +++ b/crates/render-ash/src/resources/traits.rs @@ -0,0 +1,108 @@ +use std::hash::{Hash, Hasher}; + +use pulz_render::backend::GpuResource; +use slotmap::SlotMap; + +use super::{AshFrameGarbage, AshResources, U64HashMap, replay::AsResourceRecord}; +use crate::{Result, alloc::AshAllocator}; + +pub trait AshGpuResource: GpuResource + 'static { + type Raw; + unsafe fn create_raw( + alloc: &mut AshResources, + descriptor: &Self::Descriptor<'_>, + ) -> Result; + unsafe fn destroy_raw(alloc: &mut AshAllocator, raw: Self::Raw); + + fn slotmap(res: &AshResources) -> &SlotMap; + fn slotmap_mut(res: &mut AshResources) -> &mut SlotMap; +} + +fn hash_one(value: &T) -> u64 { + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + value.hash(&mut hasher); + hasher.finish() +} + +pub(super) trait AshGpuResourceCached: AshGpuResource +where + for<'l> Self::Descriptor<'l>: Hash, +{ + fn hash_descriptor(descr: &Self::Descriptor<'_>) -> u64 { + hash_one(descr) + } + + fn get_hashs_mut(res: &mut AshResources) -> &mut U64HashMap; +} + +pub trait AshGpuResourceCreate: AshGpuResource { + #[inline] + fn create(res: &mut AshResources, descr: &Self::Descriptor<'_>) -> Result { + unsafe { + let raw = Self::create_raw(res, descr)?; + let key = Self::slotmap_mut(res).insert(raw); + Ok(key) + } + } +} + +pub trait AshGpuResourceCollection { + type Resource: AshGpuResource; + unsafe fn clear_destroy(&mut self, alloc: &mut AshAllocator); + unsafe fn destroy(&mut self, key: Self::Resource, alloc: &mut AshAllocator) -> bool; +} +impl AshGpuResourceCollection for SlotMap { + type Resource = R; + unsafe fn clear_destroy(&mut self, alloc: &mut AshAllocator) { + for (_key, raw) in self.drain() { + unsafe { + R::destroy_raw(alloc, raw); + } + } + } + unsafe fn destroy(&mut self, key: Self::Resource, alloc: &mut AshAllocator) -> bool { + unsafe { + if let Some(raw) = self.remove(key) { + R::destroy_raw(alloc, raw); + true + } else { + false + } + } + } +} + +pub trait AshGpuResourceRemove: AshGpuResource { + fn put_to_garbage(garbage: &mut AshFrameGarbage, raw: Self::Raw); + fn destroy(res: &mut AshResources, key: Self) -> bool { + if let Some(raw) = Self::slotmap_mut(res).remove(key) { + let garbage = res.current_frame_garbage_mut(); + Self::put_to_garbage(garbage, raw); + true + } else { + false + } + } +} + +impl AshGpuResourceCreate for R +where + R: AshGpuResourceCached + AsResourceRecord, + for<'l> R::Descriptor<'l>: Hash, +{ + fn create(res: &mut AshResources, descr: &Self::Descriptor<'_>) -> Result { + let hash = Self::hash_descriptor(descr); + if let Some(key) = Self::get_hashs_mut(res).get(&hash) { + return Ok(*key); + } + let key = unsafe { + let raw = Self::create_raw(res, descr)?; + Self::slotmap_mut(res).insert(raw) + }; + Self::get_hashs_mut(res).insert(hash, key); + if let Some(record) = &mut res.record { + record.record(key.as_record(descr))?; + } + Ok(key) + } +} diff --git a/crates/render-ash/src/shader.rs b/crates/render-ash/src/shader.rs new file mode 100644 index 0000000..f82c696 --- /dev/null +++ b/crates/render-ash/src/shader.rs @@ -0,0 +1,15 @@ +use std::borrow::Cow; + +use pulz_render::shader::ShaderSource; + +use crate::Result; + +pub fn compie_into_spv<'a>(source: &'a ShaderSource<'a>) -> Result> { + match source { + ShaderSource::SpirV(data) => Ok(Cow::Borrowed(data)), + ShaderSource::Glsl(_data) => todo!("implement compile GLSL"), + ShaderSource::Wgsl(_data) => todo!("implement compile WGSL"), + + _ => panic!("unsupported shader source"), + } +} diff --git a/crates/render-ash/src/swapchain.rs b/crates/render-ash/src/swapchain.rs new file mode 100644 index 0000000..f3622c0 --- /dev/null +++ b/crates/render-ash/src/swapchain.rs @@ -0,0 +1,839 @@ +use ash::vk; +use pulz_render::{ + math::{USize2, uvec2}, + texture::{Texture, TextureFormat}, +}; +use pulz_window::{DisplayHandle, Size2, Window, WindowHandle, WindowId, Windows}; +use raw_window_handle::{RawDisplayHandle, RawWindowHandle}; +use tracing::{debug, info}; + +use crate::{ + AshRendererFull, Error, Result, + convert::VkInto, + device::AshDevice, + drop_guard::{Destroy, Guard}, + instance::AshInstance, + resources::AshResources, +}; + +impl Destroy for vk::SurfaceKHR { + type Context = AshInstance; + #[inline] + unsafe fn destroy(self, instance: &AshInstance) { + unsafe { + if self != Self::null() { + let ext_surface = instance.ext_surface().unwrap(); + ext_surface.destroy_surface(self, None); + } + } + } +} + +impl Destroy for vk::SwapchainKHR { + type Context = AshDevice; + #[inline] + unsafe fn destroy(self, device: &AshDevice) { + unsafe { + if self != Self::null() { + let ext_swapchain = device.ext_swapchain().unwrap(); + ext_swapchain.destroy_swapchain(self, None); + } + } + } +} + +macro_rules! check_and_get_instance_extension { + ($self:ident => $ext:ident :: $extname:ident) => {{ + if !$self.has_instance_extension(::ash::$ext::$extname::NAME) { + return Err(Error::ExtensionNotSupported(::ash::$ext::$extname::NAME)); + } + ::ash::$ext::$extname::Instance::new($self.entry(), $self) + }}; +} + +impl AshInstance { + #[cfg(all( + unix, + not(target_os = "android"), + not(target_os = "macos"), + not(target_os = "ios") + ))] + unsafe fn create_surface_xlib( + &self, + dpy: *mut vk::Display, + window: vk::Window, + ) -> Result { + unsafe { + let functions = check_and_get_instance_extension!(self => khr::xlib_surface); + let surface = functions.create_xlib_surface( + &vk::XlibSurfaceCreateInfoKHR::default() + .dpy(dpy) + .window(window), + None, + )?; + Ok(surface) + } + } + + #[cfg(all( + unix, + not(target_os = "android"), + not(target_os = "macos"), + not(target_os = "ios") + ))] + unsafe fn create_surface_xcb( + &self, + connection: *mut vk::xcb_connection_t, + window: vk::xcb_window_t, + ) -> Result { + unsafe { + let functions = check_and_get_instance_extension!(self => khr::xcb_surface); + let surface = functions.create_xcb_surface( + &vk::XcbSurfaceCreateInfoKHR::default() + .connection(connection) + .window(window), + None, + )?; + Ok(surface) + } + } + + #[cfg(all( + unix, + not(target_os = "android"), + not(target_os = "macos"), + not(target_os = "ios") + ))] + unsafe fn create_surface_wayland( + &self, + display: *mut vk::wl_display, + surface: *mut vk::wl_surface, + ) -> Result { + unsafe { + let functions = check_and_get_instance_extension!(self => khr::wayland_surface); + let surface = functions.create_wayland_surface( + &vk::WaylandSurfaceCreateInfoKHR::default() + .display(display) + .surface(surface), + None, + )?; + Ok(surface) + } + } + + #[cfg(target_os = "android")] + unsafe fn create_surface_android( + &self, + window: *mut vk::ANativeWindow, + ) -> Result { + let functions = check_and_get_instance_extension!(self => khr::android_surface); + let surface = functions.create_android_surface( + &vk::AndroidSurfaceCreateInfoKHR::default() + .window(window) + .build(), + None, + )?; + Ok(surface) + } + + #[cfg(target_os = "windows")] + unsafe fn create_surface_win32( + &self, + hinstance: vk::HINSTANCE, + hwnd: vk::HWND, + ) -> Result { + let functions = check_and_get_instance_extension!(self => khr::win32_surface); + let surface = functions.create_win32_surface( + &vk::Win32SurfaceCreateInfoKHR::default() + .hinstance(hinstance) + .hwnd(hwnd) + .build(), + None, + )?; + Ok(surface) + } + + #[cfg(any(target_os = "macos", target_os = "ios"))] + unsafe fn create_surface_metal( + &self, + layer: *const vk::CAMetalLayer, + ) -> Result { + let functions = check_and_get_extension!(self => ext::metal_surface); + let surface = functions.create_metal_surface( + &vk::MetalSurfaceCreateInfoEXT::default() + .layer(layer) + .build(), + None, + )?; + Ok(surface) + } + + /// SAFETY: display and window handles must be valid for the complete lifetime of surface + unsafe fn create_surface_raw( + &self, + raw_display_handle: RawDisplayHandle, + raw_window_handle: RawWindowHandle, + ) -> Result { + unsafe { + // check for surface-extension + self.ext_surface()?; + + match (raw_display_handle, raw_window_handle) { + #[cfg(all( + unix, + not(target_os = "android"), + not(target_os = "macos"), + not(target_os = "ios") + ))] + (RawDisplayHandle::Xlib(d), RawWindowHandle::Xlib(w)) => self.create_surface_xlib( + d.display.ok_or(Error::WindowNotAvailable)?.as_ptr().cast(), + w.window, + ), + #[cfg(all( + unix, + not(target_os = "android"), + not(target_os = "macos"), + not(target_os = "ios") + ))] + (RawDisplayHandle::Xcb(d), RawWindowHandle::Xcb(w)) => self.create_surface_xcb( + d.connection.ok_or(Error::WindowNotAvailable)?.as_ptr(), + w.window.get(), + ), + #[cfg(all( + unix, + not(target_os = "android"), + not(target_os = "macos"), + not(target_os = "ios") + ))] + (RawDisplayHandle::Wayland(d), RawWindowHandle::Wayland(w)) => { + self.create_surface_wayland(d.display.as_ptr(), w.surface.as_ptr()) + } + #[cfg(target_os = "android")] + (RawDisplayHandle::Android(_), RawWindowHandle::AndroidNdk(w)) => { + self.create_surface_android(w.a_native_window) + } + #[cfg(target_os = "windows")] + (RawDisplayHandle::Windows(_), RawWindowHandle::Windows(w)) => { + self.create_surface_win32(w.hinstance, w.hwnd)? + } + #[cfg(target_os = "macos")] + (RawDisplayHandle::AppKit(_), RawWindowHandle::AppKit(h)) => { + use raw_window_metal::{Layer, appkit}; + let layer = match appkit::metal_layer_from_handle(h) { + Layer::Existing(layer) | Layer::Allocated(layer) => layer.cast(), + Layer::None => return Err(vk::Result::ERROR_INITIALIZATION_FAILED), + }; + self.create_surface_metal(layer)? + } + #[cfg(target_os = "ios")] + (RawDisplayHandle::UiKit(_), RawWindowHandle::UiKit(w)) => { + use raw_window_metal::{Layer, uikit}; + let layer = match uikit::metal_layer_from_handle(h) { + Layer::Existing(layer) | Layer::Allocated(layer) => layer.cast(), + Layer::None => return Err(vk::Result::ERROR_INITIALIZATION_FAILED), + }; + self.create_surface_metal(layer)? + } + + _ => Err(Error::UnsupportedWindowSystem), + } + } + } + + /// SAFETY: display and window handles must be valid for the complete lifetime of surface + pub(crate) unsafe fn new_surface( + &self, + display_handle: DisplayHandle<'_>, + window_handle: WindowHandle<'_>, + ) -> Result> { + unsafe { + fn map_handle_error(e: raw_window_handle::HandleError) -> Error { + use raw_window_handle::HandleError; + match e { + HandleError::Unavailable => Error::WindowNotAvailable, + _ => Error::UnsupportedWindowSystem, + } + } + let raw_display_handle = display_handle.as_raw(); + let raw_window_handle = window_handle.as_raw(); + let surface_raw = self.create_surface_raw(raw_display_handle, raw_window_handle)?; + Ok(Guard::new(self, surface_raw)) + } + } +} + +impl AshInstance { + pub fn get_physical_device_surface_support( + &self, + physical_device: vk::PhysicalDevice, + queue_family_index: u32, + surface: vk::SurfaceKHR, + ) -> bool { + let Ok(ext_surface) = self.ext_surface() else { + return false; + }; + + unsafe { + ext_surface + .get_physical_device_surface_support(physical_device, queue_family_index, surface) + .unwrap_or(false) + } + } + + pub fn query_swapchain_support( + &self, + surface: vk::SurfaceKHR, + physical_device: vk::PhysicalDevice, + ) -> Option { + let Ok(ext_surface) = self.ext_surface() else { + return None; + }; + unsafe { + let capabilities = ext_surface + .get_physical_device_surface_capabilities(physical_device, surface) + .ok()?; + let formats = ext_surface + .get_physical_device_surface_formats(physical_device, surface) + .ok()?; + let present_modes = ext_surface + .get_physical_device_surface_present_modes(physical_device, surface) + .ok()?; + if formats.is_empty() || present_modes.is_empty() { + None + } else { + Some(SwapchainSupportDetail { + capabilities, + formats, + present_modes, + }) + } + } + } +} + +pub struct SwapchainSupportDetail { + capabilities: vk::SurfaceCapabilitiesKHR, + formats: Vec, + present_modes: Vec, +} + +impl SwapchainSupportDetail { + pub fn preferred_format(&self) -> vk::SurfaceFormatKHR { + for available_format in &self.formats { + if available_format.format == vk::Format::B8G8R8A8_SRGB + && available_format.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR + { + return *available_format; + } + } + + // return the first format from the list + self.formats.first().cloned().unwrap() + } + pub fn preferred_present_mode(&self, suggested: vk::PresentModeKHR) -> vk::PresentModeKHR { + if self.present_modes.contains(&suggested) { + return suggested; + } + if suggested == vk::PresentModeKHR::FIFO || suggested == vk::PresentModeKHR::FIFO_RELAXED { + // find any FIFO Mode + for &present_mode in self.present_modes.iter() { + if present_mode == vk::PresentModeKHR::FIFO + || present_mode == vk::PresentModeKHR::FIFO_RELAXED + { + return present_mode; + } + } + } + if suggested != vk::PresentModeKHR::IMMEDIATE { + // find any VSync Mode (not immediate) + for &present_mode in self.present_modes.iter() { + if present_mode != vk::PresentModeKHR::IMMEDIATE { + return present_mode; + } + } + } + self.present_modes.first().copied().unwrap() + } + + pub fn preferred_composite_alpha(&self) -> vk::CompositeAlphaFlagsKHR { + if self + .capabilities + .supported_composite_alpha + .contains(vk::CompositeAlphaFlagsKHR::OPAQUE) + { + return vk::CompositeAlphaFlagsKHR::OPAQUE; + } + if self + .capabilities + .supported_composite_alpha + .contains(vk::CompositeAlphaFlagsKHR::INHERIT) + { + return vk::CompositeAlphaFlagsKHR::INHERIT; + } + self.capabilities.supported_composite_alpha + } +} + +pub struct AshSurfaceSwapchain { + surface_raw: vk::SurfaceKHR, + swapchain_raw: vk::SwapchainKHR, + size: Size2, + image_count: u32, + surface_format: vk::SurfaceFormatKHR, + present_mode: vk::PresentModeKHR, + image_usage: vk::ImageUsageFlags, + images: Vec, + image_views: Vec, + textures: Vec, + acquired_image: u32, +} + +impl AshSurfaceSwapchain { + fn window_swapchain_config(window: &Window) -> (u32, vk::PresentModeKHR, USize2) { + let (image_count, present_mode) = if window.vsync { + (3, vk::PresentModeKHR::MAILBOX) + } else { + (2, vk::PresentModeKHR::IMMEDIATE) + }; + (image_count, present_mode, window.size) + } + + fn new_unconfigured(surface_raw: vk::SurfaceKHR) -> Self { + Self { + surface_raw, + swapchain_raw: vk::SwapchainKHR::null(), + size: USize2::new(0, 0), + image_count: 0, + surface_format: Default::default(), + present_mode: vk::PresentModeKHR::IMMEDIATE, + // TODO: custom usage + image_usage: vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSFER_DST, + images: Vec::new(), + image_views: Vec::new(), + textures: Vec::new(), + acquired_image: !0, + } + } + + #[inline] + pub fn size(&self) -> Size2 { + self.size + } + + #[inline] + pub fn format(&self) -> vk::Format { + self.surface_format.format + } + + #[inline] + pub fn texture_format(&self) -> TextureFormat { + self.surface_format.format.vk_into() + } + + #[inline] + pub fn present_mode(&self) -> vk::PresentModeKHR { + self.present_mode + } + + #[inline] + pub fn image_usage(&self) -> vk::ImageUsageFlags { + self.image_usage + } + + #[inline] + pub fn image_count(&self) -> usize { + self.images.len() + } + + #[inline] + pub const fn is_acquired(&self) -> bool { + self.acquired_image != !0 + } + + pub fn put_to_garbage(&mut self, res: &mut AshResources) -> vk::SwapchainKHR { + let old_swapchain = self.swapchain_raw; + if old_swapchain != vk::SwapchainKHR::null() { + self.swapchain_raw = vk::SwapchainKHR::null(); + for texture_id in self.textures.drain(..) { + res.textures.remove(texture_id); // forget texture without destroy! + } + let garbage = res.current_frame_garbage_mut(); + garbage.image_views.append(&mut self.image_views); + garbage.swapchains.push(old_swapchain); + self.images.clear(); // images owned by swapchain! + } + old_swapchain + } + + /// #SAFETY: there must not be any outstanding operations + pub unsafe fn destroy_with_surface(mut self, res: &mut AshResources) -> Result<()> { + unsafe { + let swapchain = self.swapchain_raw; + for texture_id in self.textures.drain(..) { + res.textures.remove(texture_id); // forget texture without destroy! + } + for image_view in self.image_views.drain(..) { + res.device().destroy_image_view(image_view, None); + } + self.images.clear(); // images owned by swapchain! + if swapchain != vk::SwapchainKHR::null() { + self.swapchain_raw = vk::SwapchainKHR::null(); + res.device() + .ext_swapchain()? + .destroy_swapchain(swapchain, None); + } + let surface = self.surface_raw; + if surface != vk::SurfaceKHR::null() { + self.surface_raw = vk::SurfaceKHR::null(); + if let Ok(ext_surface) = res.device().instance().ext_surface() { + ext_surface.destroy_surface(surface, None); + } + } + Ok(()) + } + } + + fn configure_with(&mut self, res: &mut AshResources, window: &Window) -> Result<()> { + let (suggested_image_count, suggested_present_mode, suggessted_size) = + Self::window_swapchain_config(window); + self.image_count = suggested_image_count; + self.size = suggessted_size; + self.present_mode = suggested_present_mode; + self.configure(res) + } + + fn configure(&mut self, res: &mut AshResources) -> Result<()> { + debug!( + "re-creating swapchain, recreate={:?}", + self.swapchain_raw != vk::SwapchainKHR::null() + ); + // check swapchain support + res.device().ext_swapchain()?; + + // TODO: also reconfigure on resize, and when presenting results in `Outdated/Lost` + // TODO: pass swapchain format to graph + + let swapchain_support_info = res + .instance() + .query_swapchain_support(self.surface_raw, res.device().physical_device()) + .ok_or(Error::NoSwapchainSupport)?; + + if !swapchain_support_info + .capabilities + .supported_usage_flags + .contains(self.image_usage) + { + return Err(Error::SwapchainUsageNotSupported { + requested: self.image_usage, + supported: swapchain_support_info.capabilities.supported_usage_flags, + }); + } + + self.surface_format = swapchain_support_info.preferred_format(); + + self.present_mode = swapchain_support_info.preferred_present_mode(self.present_mode); + + let vk::Extent2D { width, height } = swapchain_support_info.capabilities.current_extent; + if width != u32::MAX { + self.size = uvec2(width, height) + } else { + // clamp size + if self.size.x < swapchain_support_info.capabilities.min_image_extent.width { + self.size.x = swapchain_support_info.capabilities.min_image_extent.width; + } else if self.size.x > swapchain_support_info.capabilities.max_image_extent.width { + self.size.x = swapchain_support_info.capabilities.max_image_extent.width; + } + if self.size.y < swapchain_support_info.capabilities.min_image_extent.height { + self.size.y = swapchain_support_info.capabilities.min_image_extent.height; + } else if self.size.y > swapchain_support_info.capabilities.max_image_extent.height { + self.size.y = swapchain_support_info.capabilities.max_image_extent.height; + } + } + + if self.image_count < swapchain_support_info.capabilities.min_image_count { + self.image_count = swapchain_support_info.capabilities.min_image_count; + } + if swapchain_support_info.capabilities.max_image_count > 0 + && self.image_count > swapchain_support_info.capabilities.max_image_count + { + self.image_count = swapchain_support_info.capabilities.max_image_count; + } + + let shared_queue_family_indices = [ + res.device().queues().graphics_family, + res.device().queues().present_family, + ]; + // old swapchain is retired, even if `create_swapchain` fails + let old_swapchain = self.put_to_garbage(res); + self.swapchain_raw = unsafe { + res.device().ext_swapchain()?.create_swapchain( + &vk::SwapchainCreateInfoKHR::default() + .surface(self.surface_raw) + .min_image_count(self.image_count) + .image_format(self.surface_format.format) + .image_color_space(self.surface_format.color_space) + .image_extent(vk::Extent2D { + width: self.size.x, + height: self.size.y, + }) + .image_array_layers(1) + .image_usage(self.image_usage) + .image_sharing_mode(vk::SharingMode::EXCLUSIVE) + .queue_family_indices( + if shared_queue_family_indices[0] != shared_queue_family_indices[1] { + &shared_queue_family_indices + } else { + &[] + }, + ) + .pre_transform(swapchain_support_info.capabilities.current_transform) + .composite_alpha(swapchain_support_info.preferred_composite_alpha()) + .present_mode(self.present_mode) + .clipped(true) + .old_swapchain(old_swapchain) + .image_array_layers(1), + None, + )? + }; + + debug_assert_ne!(vk::SwapchainKHR::null(), self.swapchain_raw); + debug_assert_eq!(0, self.images.len()); + debug_assert_eq!(0, self.image_views.len()); + debug_assert_eq!(0, self.textures.len()); + self.images = unsafe { + res.device() + .ext_swapchain()? + .get_swapchain_images(self.swapchain_raw)? + }; + + for image in self.images.iter().copied() { + unsafe { + let image_view = res.device().create_image_view( + &vk::ImageViewCreateInfo::default() + .image(image) + .view_type(vk::ImageViewType::TYPE_2D) + .format(self.surface_format.format) + .subresource_range( + vk::ImageSubresourceRange::default() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .base_mip_level(0) + .level_count(1) + .base_array_layer(0) + .layer_count(1), + ), + None, + )?; + self.image_views.push(image_view); + let texture = res.textures.insert((image, image_view, None)); + self.textures.push(texture); + } + } + + Ok(()) + } + + pub(crate) fn acquire_next_image( + &mut self, + res: &mut AshResources, + signal_semaphore: vk::Semaphore, + ) -> Result> { + if self.is_acquired() { + return Err(Error::SwapchainImageAlreadyAcquired); + } + + let result = unsafe { + match res.device().ext_swapchain()?.acquire_next_image( + self.swapchain_raw, + !0, + signal_semaphore, + vk::Fence::null(), + ) { + Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => { + // re-configure and re-acquire + self.configure(res)?; + res.device().ext_swapchain()?.acquire_next_image( + self.swapchain_raw, + !0, + signal_semaphore, + vk::Fence::null(), + ) + } + + // TODO: handle Surface-lost + // destroy old surface & swapchain, + // re-create surface and swapchain, + // and acquire again + other => other, + } + }; + + let (index, suboptimal) = match result { + Ok(r) => r, + Err(vk::Result::NOT_READY) | Err(vk::Result::TIMEOUT) => { + return Ok(None); + } + Err(vk::Result::ERROR_SURFACE_LOST_KHR) => return Err(Error::SurfaceLost), + Err(e) => return Err(e.into()), + }; + self.acquired_image = index; + Ok(Some(AcquiredSwapchainImage { + index, + image: self.images[index as usize], + image_view: self.image_views[index as usize], + texture: self.textures[index as usize], + suboptimal, + })) + } +} + +impl Drop for AshSurfaceSwapchain { + fn drop(&mut self) { + assert_eq!(0, self.images.len()); + assert_eq!(0, self.image_views.len()); + assert_eq!(0, self.textures.len()); + assert_eq!(vk::SwapchainKHR::null(), self.swapchain_raw); + assert_eq!(vk::SurfaceKHR::null(), self.surface_raw); + } +} + +impl AshRendererFull { + pub(crate) fn init_swapchain( + &mut self, + window_id: WindowId, + window: &Window, + surface: Guard<'_, vk::SurfaceKHR>, + ) -> Result<&mut AshSurfaceSwapchain> { + assert!( + self.surfaces + .insert( + window_id, + AshSurfaceSwapchain::new_unconfigured(surface.take()) + ) + .is_none() + ); + let swapchain = self.surfaces.get_mut(window_id).unwrap(); + swapchain.configure_with(&mut self.res, window)?; + Ok(swapchain) + } + + pub(crate) fn destroy_swapchain(&mut self, window_id: WindowId) -> Result<()> { + let Some(swapchain) = self.surfaces.remove(window_id) else { + return Err(Error::WindowNotAvailable); + }; + self.res.wait_idle_and_clear_garbage()?; + unsafe { + swapchain.destroy_with_surface(&mut self.res)?; + } + Ok(()) + } + + pub(crate) fn destroy_all_swapchains(&mut self) -> Result<()> { + self.res.wait_idle_and_clear_garbage()?; + unsafe { + for (_window_id, swapchain) in self.surfaces.drain() { + swapchain.destroy_with_surface(&mut self.res)?; + } + } + Ok(()) + } + + pub(crate) fn reconfigure_swapchains(&mut self, windows: &Windows) { + self.surfaces.retain(|window_id, surface_swapchain| { + let Some(window) = windows.get(window_id) else { + surface_swapchain.put_to_garbage(&mut self.res); + return false; + }; + //TODO: re-create also the surface, when SURFACE_LOST was returned in earlier calls. + //TODO: better resize check (don't compare size, but use a 'dirty'-flag, or listener) + //TODO: sync + if window.size != surface_swapchain.size() { + info!( + "surface sized changed: {} => {}", + surface_swapchain.size(), + window.size + ); + surface_swapchain + .configure_with(&mut self.res, window) + .unwrap(); + } + true + }); + } + + pub(crate) fn get_num_acquired_swapchains(&self) -> usize { + self.surfaces + .iter() + .filter(|(_, s)| s.is_acquired()) + .count() + } + + pub(crate) fn present_acquired_swapchain_images( + &mut self, + wait_semaphores: &[vk::Semaphore], + ) -> Result<()> { + let _ = tracing::trace_span!("Present").entered(); + let ext_swapchain = self.device.ext_swapchain()?; + let mut acquired_surface_swapchains: Vec<_> = self + .surfaces + .iter_mut() + .map(|(_, s)| s) + .filter(|s| s.is_acquired()) + .collect(); + let swapchains: Vec<_> = acquired_surface_swapchains + .iter() + .map(|s| s.swapchain_raw) + .collect(); + let image_indices: Vec<_> = acquired_surface_swapchains + .iter() + .map(|s| s.acquired_image) + .collect(); + let mut results = Vec::new(); + results.resize(acquired_surface_swapchains.len(), vk::Result::SUCCESS); + let result = unsafe { + ext_swapchain.queue_present( + self.device.queues().present, + &vk::PresentInfoKHR::default() + .wait_semaphores(wait_semaphores) + .swapchains(&swapchains) + .image_indices(&image_indices) + .results(&mut results), + ) + }; + + // reset image index + for surface_swapchain in &mut acquired_surface_swapchains { + surface_swapchain.acquired_image = !0; + } + + match result { + Ok(_) + | Err(vk::Result::SUBOPTIMAL_KHR) + | Err(vk::Result::ERROR_OUT_OF_DATE_KHR) + | Err(vk::Result::ERROR_SURFACE_LOST_KHR) => (), + Err(e) => { + return Err(e.into()); + } + } + + for (result, surface_swapchain) in results + .into_iter() + .zip(acquired_surface_swapchains.into_iter()) + { + if result == vk::Result::SUBOPTIMAL_KHR || result == vk::Result::ERROR_OUT_OF_DATE_KHR { + surface_swapchain.configure(&mut self.res)?; + } else if result == vk::Result::ERROR_SURFACE_LOST_KHR { + // TODO: re-create surface and re-configure swapchain + } + } + + Ok(()) + } +} + +pub struct AcquiredSwapchainImage { + index: u32, + pub image: vk::Image, + pub image_view: vk::ImageView, + pub texture: Texture, + pub suboptimal: bool, +} diff --git a/crates/render-ash/src/utils.rs b/crates/render-ash/src/utils.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/render-ash/src/utils.rs @@ -0,0 +1 @@ + diff --git a/crates/render-pipeline-core/CHANGELOG.md b/crates/render-pipeline-core/CHANGELOG.md new file mode 100644 index 0000000..77e12af --- /dev/null +++ b/crates/render-pipeline-core/CHANGELOG.md @@ -0,0 +1,6 @@ +# `pulz-render-core-pipeline` Changelog +All notable changes to this crate will be documented in this file. + +## Unreleased (DATE) + + * Initial version diff --git a/crates/render-pipeline-core/Cargo.toml b/crates/render-pipeline-core/Cargo.toml new file mode 100644 index 0000000..faf334c --- /dev/null +++ b/crates/render-pipeline-core/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "pulz-render-pipeline-core" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +keywords = ["game", "game-engine"] +categories = ["game-engines", "game-development"] +readme = "README.md" + +[dependencies] +pulz-ecs = { path = "../ecs" } +pulz-render = { path = "../render" } + +radsort = { workspace = true } +tracing = { workspace = true } diff --git a/crates/render-pipeline-core/README.md b/crates/render-pipeline-core/README.md new file mode 100644 index 0000000..e05d7a4 --- /dev/null +++ b/crates/render-pipeline-core/README.md @@ -0,0 +1,34 @@ +# `pulz-render-core-pipeline` + + + +[![Crates.io](https://img.shields.io/crates/v/pulz-render-core-pipeline.svg?label=pulz-render-core-pipeline)](https://crates.io/crates/pulz-render-core-pipeline) +[![docs.rs](https://docs.rs/pulz-render-core-pipeline/badge.svg)](https://docs.rs/pulz-render-core-pipeline/) +[![license: MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](#license) +[![Rust CI](https://github.com/HellButcher/pulz/actions/workflows/rust.yml/badge.svg)](https://github.com/HellButcher/pulz/actions/workflows/rust.yml) + + +**TODO** + +## Example + + +**TODO** + +## License + +[license]: #license + +This project is licensed under either of + +* MIT license ([LICENSE-MIT] or ) +* Apache License, Version 2.0, ([LICENSE-APACHE] or ) + +at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +[LICENSE-MIT]: ../../LICENSE-MIT +[LICENSE-APACHE]: ../../LICENSE-APACHE diff --git a/crates/render-pipeline-core/src/common.rs b/crates/render-pipeline-core/src/common.rs new file mode 100644 index 0000000..5a01261 --- /dev/null +++ b/crates/render-pipeline-core/src/common.rs @@ -0,0 +1,64 @@ +use pulz_ecs::{Entity, prelude::Module}; +use pulz_render::{ + RenderModule, + draw::{PhaseItem, PhaseModule}, +}; + +pub struct Opaque { + distance: f32, + entity: Entity, +} + +impl PhaseItem for Opaque { + type TargetKey = Entity; + fn sort(items: &mut [E]) + where + E: std::ops::Deref, + { + // front to back + radsort::sort_by_key(items, |item| item.distance); + } +} + +pub struct OpaqueAlpha { + distance: f32, + entity: Entity, +} + +impl PhaseItem for OpaqueAlpha { + type TargetKey = Entity; + fn sort(items: &mut [E]) + where + E: std::ops::Deref, + { + // front to back + radsort::sort_by_key(items, |item| item.distance); + } +} + +pub struct Transparent { + distance: f32, + entity: Entity, +} + +impl PhaseItem for Transparent { + type TargetKey = Entity; + fn sort(items: &mut [E]) + where + E: std::ops::Deref, + { + // back to front + radsort::sort_by_key(items, |item| -item.distance); + } +} + +pub struct CorePipelineCommonModule; + +impl Module for CorePipelineCommonModule { + fn install_modules(&self, res: &mut pulz_ecs::resource::Resources) { + res.install(RenderModule); + res.install(PhaseModule::::new()); + res.install(PhaseModule::::new()); + res.install(PhaseModule::::new()); + } +} diff --git a/crates/render-pipeline-core/src/core_3d/mod.rs b/crates/render-pipeline-core/src/core_3d/mod.rs new file mode 100644 index 0000000..53dc70f --- /dev/null +++ b/crates/render-pipeline-core/src/core_3d/mod.rs @@ -0,0 +1,66 @@ +use pulz_ecs::prelude::*; +use pulz_render::{ + RenderSystemPhase, + camera::{Camera, RenderTarget}, + graph::{ + RenderGraphBuilder, + pass::{Graphics, Pass, builder::PassBuilder, run::PassExec}, + resources::WriteSlot, + }, + math::Mat4, + texture::Texture, +}; + +pub use crate::common::*; + +pub struct CoreShadingModule; + +impl CoreShadingModule { + fn build_graph_system( + builder: &mut RenderGraphBuilder, + cams_qry: Query<'_, (&Camera, &RenderTarget, Entity)>, + ) { + for (camera, render_target, entity) in cams_qry { + let output = builder.add_pass(CoreShadingPass { + view_camera: entity, + projection: camera.projection_matrix, + }); + + builder.export_texture(output.read(), render_target); + } + } +} + +impl Module for CoreShadingModule { + fn install_modules(&self, res: &mut Resources) { + res.install(CorePipelineCommonModule); + } + + fn install_systems(schedule: &mut Schedule) { + schedule + .add_system(Self::build_graph_system) + .into_phase(RenderSystemPhase::BuildGraph); + } +} +pub struct CoreShadingPass { + view_camera: Entity, + projection: Mat4, +} + +impl Pass for CoreShadingPass { + type Output = WriteSlot; + + fn build(self, mut builder: PassBuilder<'_, Graphics>) -> (Self::Output, PassExec) { + let color = builder.creates_color_attachment(); + builder.creates_depth_stencil_attachment(); + + ( + color, + PassExec::new_fn(move |mut ctx| { + ctx.draw_phase_items::(self.view_camera); + ctx.draw_phase_items::(self.view_camera); + ctx.draw_phase_items::(self.view_camera); + }), + ) + } +} diff --git a/crates/render-pipeline-core/src/deferred_3d/mod.rs b/crates/render-pipeline-core/src/deferred_3d/mod.rs new file mode 100644 index 0000000..c5462bb --- /dev/null +++ b/crates/render-pipeline-core/src/deferred_3d/mod.rs @@ -0,0 +1,146 @@ +use pulz_ecs::prelude::*; +use pulz_render::{ + RenderSystemPhase, + camera::{Camera, RenderTarget}, + graph::{ + RenderGraphBuilder, + pass::{ + Graphics, Pass, PassGroup, + builder::{PassBuilder, PassGroupBuilder}, + run::PassExec, + }, + resources::{Slot, WriteSlot}, + }, + math::Mat4, + texture::Texture, +}; + +pub use crate::common::*; +pub struct DeferredShadingModule; + +impl DeferredShadingModule { + fn build_graph_system( + builder: &mut RenderGraphBuilder, + cams_qry: Query<'_, (&Camera, &RenderTarget, Entity)>, + ) { + for (camera, render_target, entity) in cams_qry { + let output = builder.add_pass(DeferredShadingPass { + view_camera: entity, + projection: camera.projection_matrix, + }); + + builder.export_texture(output.read(), render_target); + } + } +} +impl Module for DeferredShadingModule { + fn install_modules(&self, res: &mut Resources) { + res.install(CorePipelineCommonModule); + } + + fn install_systems(schedule: &mut Schedule) { + schedule + .add_system(Self::build_graph_system) + .into_phase(RenderSystemPhase::BuildGraph); + } +} + +pub struct DeferredShadingPass { + view_camera: Entity, + projection: Mat4, +} + +impl PassGroup for DeferredShadingPass { + type Output = WriteSlot; + + fn build(self, mut build: PassGroupBuilder<'_, Graphics>) -> Self::Output { + let gbuffer = build.sub_pass(GBuffer { + view_camera: self.view_camera, + }); + let output = build.sub_pass(Composition { + albedo: gbuffer.albedo.read(), + position: gbuffer.position.read(), + normal: gbuffer.normal.read(), + }); + + build.sub_pass(Transparency { + view_camera: self.view_camera, + output, + depth: gbuffer.depth, + }) + } +} + +struct GBuffer { + view_camera: Entity, +} + +struct GBufferOutput { + albedo: WriteSlot, + position: WriteSlot, + normal: WriteSlot, + depth: WriteSlot, +} +struct Composition { + albedo: Slot, + position: Slot, + normal: Slot, +} +struct Transparency { + view_camera: Entity, + output: WriteSlot, + depth: WriteSlot, +} + +impl Pass for GBuffer { + type Output = GBufferOutput; + + fn build(self, mut build: PassBuilder<'_, Graphics>) -> (Self::Output, PassExec) { + let albedo = build.creates_color_attachment(); + let position = build.creates_color_attachment(); + let normal = build.creates_color_attachment(); + let depth = build.creates_depth_stencil_attachment(); + ( + GBufferOutput { + albedo, + position, + normal, + depth, + }, + PassExec::new_fn(move |mut ctx| { + ctx.draw_phase_items::(self.view_camera); + ctx.draw_phase_items::(self.view_camera); + }), + ) + } +} + +impl Pass for Composition { + type Output = WriteSlot; + + fn build(self, mut build: PassBuilder<'_, Graphics>) -> (Self::Output, PassExec) { + build.color_input_attachment(self.albedo); + build.color_input_attachment(self.position); + build.color_input_attachment(self.normal); + let output = build.creates_color_attachment(); + (output, PassExec::noop()) + } +} + +impl Pass for Transparency { + type Output = WriteSlot; + + fn build( + self, + mut build: PassBuilder<'_, Graphics>, + ) -> (WriteSlot, PassExec) { + build.depth_stencil_attachment(self.depth); + let output = build.color_attachment(self.output); + ( + output, + PassExec::new_fn(move |mut ctx| { + ctx.draw_phase_items::(self.view_camera); + }), + ) + } +} diff --git a/crates/render-pipeline-core/src/lib.rs b/crates/render-pipeline-core/src/lib.rs new file mode 100644 index 0000000..559afac --- /dev/null +++ b/crates/render-pipeline-core/src/lib.rs @@ -0,0 +1,30 @@ +#![warn( + // missing_docs, + // rustdoc::missing_doc_code_examples, + future_incompatible, + rust_2018_idioms, + unused, + trivial_casts, + trivial_numeric_casts, + unused_lifetimes, + unused_qualifications, + unused_crate_dependencies, + clippy::cargo, + clippy::multiple_crate_versions, + clippy::empty_line_after_outer_attr, + clippy::fallible_impl_from, + clippy::redundant_pub_crate, + clippy::use_self, + clippy::suspicious_operation_groupings, + clippy::useless_let_if_seq, + // clippy::missing_errors_doc, + // clippy::missing_panics_doc, + clippy::wildcard_imports +)] +#![doc(html_logo_url = "https://raw.githubusercontent.com/HellButcher/pulz/master/docs/logo.png")] +#![doc(html_no_source)] +#![doc = include_str!("../README.md")] + +pub mod common; +pub mod core_3d; +pub mod deferred_3d; diff --git a/crates/render-wgpu/CHANGELOG.md b/crates/render-wgpu/CHANGELOG.md new file mode 100644 index 0000000..79ef115 --- /dev/null +++ b/crates/render-wgpu/CHANGELOG.md @@ -0,0 +1,6 @@ +# `pulz-render-wgpu` Changelog +All notable changes to this crate will be documented in this file. + +## Unreleased (DATE) + + * Initial version diff --git a/crates/render-wgpu/Cargo.toml b/crates/render-wgpu/Cargo.toml new file mode 100644 index 0000000..62e937d --- /dev/null +++ b/crates/render-wgpu/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "pulz-render-wgpu" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +readme = "README.md" + +[features] +# uncomment if Uncomment once we get to https://github.com/gfx-rs/wgpu/issues/5974 is resolved +# trace = ["wgpu/trace"] + +[dependencies] +pulz-ecs = { path = "../ecs" } +pulz-window = { path = "../window" } +pulz-render = { path = "../render" } + +thiserror = { workspace = true } +tracing = { workspace = true } +slotmap = { workspace = true } +wgpu = "25.0" +raw-window-handle = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +pollster = "0.4" + +[dev-dependencies] +naga = "25.0" +pulz-window-winit = { path = "../window-winit" } +pulz-render-pipeline-core = { path = "../render-pipeline-core" } + +[target.'cfg(not(target_os = "unknown"))'.dev-dependencies] +tracing-subscriber = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wgpu = { version = "25.0" , features = ["webgl"] } +tracing-wasm = { workspace = true } +tracing-log = { workspace = true } +console_error_panic_hook = { workspace = true } +wasm-bindgen = { workspace = true } +web-sys = { workspace = true, features = ["HtmlCanvasElement"] } +wasm-bindgen-futures = { workspace = true } diff --git a/crates/render-wgpu/README.md b/crates/render-wgpu/README.md new file mode 100644 index 0000000..4d14d17 --- /dev/null +++ b/crates/render-wgpu/README.md @@ -0,0 +1,34 @@ +# `pulz-render-wgpu` + + + +[![Crates.io](https://img.shields.io/crates/v/pulz-render-wgpu.svg?label=pulz-render-wgpu)](https://crates.io/crates/pulz-render-wgpu) +[![docs.rs](https://docs.rs/pulz-render-wgpu/badge.svg)](https://docs.rs/pulz-render-wgpu/) +[![license: MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](#license) +[![Rust CI](https://github.com/HellButcher/pulz/actions/workflows/rust.yml/badge.svg)](https://github.com/HellButcher/pulz/actions/workflows/rust.yml) + + +**TODO** + +## Example + + +**TODO** + +## License + +[license]: #license + +This project is licensed under either of + +* MIT license ([LICENSE-MIT] or ) +* Apache License, Version 2.0, ([LICENSE-APACHE] or ) + +at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +[LICENSE-MIT]: ../../LICENSE-MIT +[LICENSE-APACHE]: ../../LICENSE-APACHE diff --git a/crates/render-wgpu/examples/render-wgpu-demo.rs b/crates/render-wgpu/examples/render-wgpu-demo.rs new file mode 100644 index 0000000..f0abee2 --- /dev/null +++ b/crates/render-wgpu/examples/render-wgpu-demo.rs @@ -0,0 +1,83 @@ +use std::error::Error; + +use pulz_ecs::prelude::*; +use pulz_render::camera::{Camera, RenderTarget}; +use pulz_render_pipeline_core::core_3d::CoreShadingModule; +use pulz_render_wgpu::WgpuRenderer; +use pulz_window::{WindowAttributes, WindowId, WindowModule}; +use pulz_window_winit::{Application, winit::event_loop::EventLoop}; +use tracing::*; + +async fn init() -> Resources { + info!("Initializing..."); + let mut resources = Resources::new(); + resources.install(CoreShadingModule); + + WgpuRenderer::new().await.unwrap().install(&mut resources); + + // let mut schedule = resources.remove::().unwrap(); + // schedule.init(&mut resources); + // schedule.debug_dump_if_env(None).unwrap(); + // resources.insert_again(schedule); + + let windows = resources.install(WindowModule); + let window_id = windows.create(WindowAttributes::new()); + + setup_demo_scene(&mut resources, window_id); + + resources +} + +fn setup_demo_scene(resources: &mut Resources, window: WindowId) { + let mut world = resources.world_mut(); + + world + .spawn() + .insert(Camera::new()) + .insert(RenderTarget::Window(window)); +} + +#[cfg(not(target_arch = "wasm32"))] +fn main() -> Result<(), Box> { + // todo: run blocking! + use tracing_subscriber::{EnvFilter, fmt::format::FmtSpan}; + let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + tracing_subscriber::fmt() + .with_env_filter(env_filter) + .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE) + .init(); + + pollster::block_on(async move { + let event_loop = EventLoop::new().unwrap(); + let resources = init().await; + let mut app = Application::new(resources); + event_loop.run_app(&mut app).map_err(Into::into) + }) +} + +#[cfg(target_arch = "wasm32")] +fn main() { + use wasm_bindgen::prelude::*; + use winit::platform::web::WindowExtWebSys; + + console_error_panic_hook::set_once(); + tracing_log::LogTracer::init().expect("unable to create log-tracer"); + tracing_wasm::set_as_global_default(); + + wasm_bindgen_futures::spawn_local(async move { + let event_loop = EventLoop::new().unwrap(); + let resources = init().await; + let app = Application::new(resources); + + /* + let canvas = window.canvas(); + canvas.style().set_css_text("background-color: teal;"); + web_sys::window() + .and_then(|win| win.document()) + .and_then(|doc| doc.body()) + .and_then(|body| body.append_child(&web_sys::Element::from(canvas)).ok()) + .expect("couldn't append canvas to document body"); + */ + event_loop.spawn_app(app); + }) +} diff --git a/crates/render-wgpu/src/backend.rs b/crates/render-wgpu/src/backend.rs new file mode 100644 index 0000000..d1320c4 --- /dev/null +++ b/crates/render-wgpu/src/backend.rs @@ -0,0 +1,273 @@ +use std::ops::{Deref, DerefMut}; + +use pulz_render::backend::CommandEncoder; +use pulz_window::WindowId; + +pub enum BackendTexture { + Texture { + texture: wgpu::Texture, + view: wgpu::TextureView, + }, + Surface { + window: WindowId, + view: wgpu::TextureView, + }, +} + +impl BackendTexture { + #[inline] + pub fn view(&self) -> &wgpu::TextureView { + match self { + Self::Texture { view, .. } => view, + Self::Surface { view, .. } => view, + } + } +} + +// impl RenderBackend for WgpuRendererBackend { +// type Error = ConversionError; +// fn create_buffer(&mut self, desc: &BufferDescriptor) -> Result { +// let desc = convert_buffer_descriptor(desc); +// let buffer = self.device.create_buffer(&desc); +// self.resources.buffers.insert(buffer) +// } +// fn create_texture(&mut self, desc: &TextureDescriptor) -> Result { +// let tex_desc = convert_texture_descriptor(desc); +// let view_desc = convert_texture_view_descriptor(desc); +// let texture = self.device.create_texture(&tex_desc); +// let view = texture.create_view(&view_desc); +// self.resources +// .textures +// .insert(BackendTexture::Texture { texture, view }) +// } +// fn create_shader_module(&mut self, desc: &ShaderModuleDescriptor<'_>) -> ShaderModuleId { +// debug!("creating shader module `{:?}`", desc.label); +// let desc = convert_shader_module_descriptor(desc); +// let shader_module = self.device.create_shader_module(&desc); +// self.resources.shader_modules.insert(shader_module) +// } +// fn create_bind_group_layout( +// &mut self, +// desc: &BindGroupLayoutDescriptor<'_>, +// ) -> BindGroupLayoutId { +// let mut tmp1 = Vec::new(); +// let desc = convert_bind_group_layout_descriptor(desc, &mut tmp1); +// let bind_group_layout = self.device.create_bind_group_layout(&desc); +// self.resources.bind_group_layouts.insert(bind_group_layout) +// } +// fn create_pipeline_layout(&mut self, desc: &PipelineLayoutDescriptor<'_>) -> PipelineLayoutId { +// let mut tmp1 = Vec::new(); +// let desc = convert_pipeline_layout_descriptor(self.resources(), desc, &mut tmp1); +// let pipeline_layout = self.device.create_pipeline_layout(&desc); +// self.resources.pipeline_layouts.insert(pipeline_layout) +// } +// fn create_compute_pipeline( +// &mut self, +// desc: &ComputePipelineDescriptor<'_>, +// ) -> ComputePipelineId { +// let desc = convert_compute_pipeline_descriptor(self.resources(), desc).unwrap(); +// let compute_pipeline = self.device.create_compute_pipeline(&desc); +// self.resources.compute_pipelines.insert(compute_pipeline) +// } +// fn create_graphics_pipeline( +// &mut self, +// desc: &GraphicsPipelineDescriptor<'_>, +// ) -> GraphicsPipelineId { +// let mut tmp1 = Vec::new(); +// let mut tmp2 = Vec::new(); +// let mut tmp3 = Vec::new(); +// let desc = convert_graphics_pipeline_descriptor( +// self.resources(), +// desc, +// &mut tmp1, +// &mut tmp2, +// &mut tmp3, +// ) +// .unwrap(); +// let graphics_pipeline = self.device.create_render_pipeline(&desc); +// self.resources.graphics_pipelines.insert(graphics_pipeline) +// } + +// fn write_image(&self, texture: TextureId, image: &render::texture::Image) { +// let texture = self +// .resources +// .textures +// .get(texture) +// .expect("invalid texture handle"); +// let BackendTexture::Texture { texture, .. } = texture else { +// panic!("trying to write to surface texture"); +// }; +// self.queue.write_texture( +// ImageCopyTexture { +// texture, +// mip_level: 1, +// origin: Origin3d::ZERO, +// aspect: wgpu::TextureAspect::All, +// }, +// &image.data, +// image.descriptor.wgpu_into(), +// image.descriptor.wgpu_into(), +// ); +// } + +// fn destroy_buffer(&mut self, id: BufferId) { +// self.resources.buffers.remove(id); +// } +// fn destroy_texture(&mut self, id: TextureId) { +// self.resources.textures.remove(id); +// } +// fn destroy_shader_module(&mut self, id: ShaderModuleId) { +// self.resources.shader_modules.remove(id); +// } +// } + +// impl RenderResourceBackend for WgpuRendererBackend { +// type Error = (); +// type Target = Buffer; + +// fn create( +// &mut self, +// descriptor: &render_resource::BufferDescriptor, +// ) -> Result { +// let desc = convert_buffer_descriptor(desc); +// let buffer = self.device.create_buffer(&desc); +// Ok(buffer) +// } + +// fn destroy(&mut self, buffer: Buffer) { +// self.device.destroy_buffer(buffer) +// } +// } + +// impl RenderResourceBackend for WgpuRendererBackend { +// type Error = (); +// type Target = BackendTexture; + +// fn create( +// &mut self, +// descriptor: &render_resource::TextureDescriptor, +// ) -> Result { +// let tex_desc = convert_texture_descriptor(desc); +// let view_desc = convert_texture_view_descriptor(desc); +// let texture = self.device.create_texture(&tex_desc); +// let view = texture.create_view(&view_desc); +// Ok(BackendTexture::Texture { texture, view }) +// } + +// fn destroy(&mut self, texture: BackendTexture) { +// self.device.destroy_texture(texture) +// } +// } + +// impl RenderResourceBackend for WgpuRendererBackend { +// type Error = (); +// type Target = ShaderModule; + +// fn create( +// &mut self, +// descriptor: &render_resource::ShaderModuleDescriptor, +// ) -> Result { +// debug!("creating shader module `{:?}`", desc.label); +// let desc = convert_shader_module_descriptor(desc); +// let shader_module = self.device.create_shader_module(&desc); +// Ok(shader_module) +// } + +// fn destroy(&mut self, module: ShaderModule) { +// self.device.destroy_shader_module(module) +// } +// } + +// impl RenderBackendTypes for WgpuRendererBackend { +// type Buffer = Buffer; +// type Texture = BackendTexture; +// type ShaderModule = ShaderModule; +// type BindGroupLayout = BindGroupLayout; +// type PipelineLayout = PipelineLayout; +// type GraphicsPipeline = RenderPipeline; +// type ComputePipeline = ComputePipeline; + +// #[inline] +// fn resources(&self) -> &RenderBackendResources { +// &self.resources +// } + +// #[inline] +// fn resources_mut(&mut self) -> &mut RenderBackendResources { +// &mut self.resources +// } +// } + +pub struct WgpuCommandEncoder(pub T); + +impl Deref for WgpuCommandEncoder { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + &self.0 + } +} + +impl DerefMut for WgpuCommandEncoder { + #[inline] + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl WgpuCommandEncoder { + pub fn finish(self) -> wgpu::CommandBuffer { + self.0.finish() + } +} + +impl CommandEncoder for WgpuCommandEncoder { + // fn graphics_pass( + // &mut self, + // desc: &render::pass::GraphicsPassDescriptor<'_>, + // pass_fn: &mut dyn FnMut(&mut dyn render::draw::DrawCommandEncoder), + // ) { + // let mut tmp1 = Vec::new(); + // let desc = convert_render_pass(self.1, desc, &mut tmp1).unwrap(); + // let pass = self.0.begin_render_pass(&desc); + // let mut pass_encoder = CommandEncoder(pass, self.1); + // pass_fn(&mut pass_encoder); + // } + + fn insert_debug_marker(&mut self, label: &str) { + self.0.insert_debug_marker(label) + } + + fn push_debug_group(&mut self, label: &str) { + self.0.push_debug_group(label) + } + + fn pop_debug_group(&mut self) { + self.0.pop_debug_group(); + } +} + +impl<'a> CommandEncoder for WgpuCommandEncoder> { + // fn set_pipeline(&mut self, pipeline: GraphicsPipelineId) { + // self.0.set_pipeline(&self.1.graphics_pipelines[pipeline]) + // } + // fn draw_indexed(&mut self, indices: Range, base_vertex: i32, instances: Range) { + // self.0.draw_indexed(indices, base_vertex, instances) + // } + // fn draw(&mut self, vertices: Range, instances: Range) { + // self.0.draw(vertices, instances) + // } + + fn insert_debug_marker(&mut self, label: &str) { + self.0.insert_debug_marker(label) + } + + fn push_debug_group(&mut self, label: &str) { + self.0.push_debug_group(label) + } + + fn pop_debug_group(&mut self) { + self.0.pop_debug_group(); + } +} diff --git a/crates/render-wgpu/src/convert.rs b/crates/render-wgpu/src/convert.rs new file mode 100644 index 0000000..332c2b1 --- /dev/null +++ b/crates/render-wgpu/src/convert.rs @@ -0,0 +1,750 @@ +use std::ops::Deref; + +use pulz_render::{ + buffer::{BufferDescriptor, BufferUsage}, + color::Srgba, + math::USize3, + pipeline::{ + BindGroupLayoutDescriptor, BindGroupLayoutEntry, BlendComponent, BlendFactor, + BlendOperation, BlendState, ColorTargetState, ColorWrite, CompareFunction, + ComputePipelineDescriptor, DepthStencilState, Face, FragmentState, FrontFace, + GraphicsPipelineDescriptor, IndexFormat, PipelineLayout, PipelineLayoutDescriptor, + PrimitiveState, PrimitiveTopology, StencilFaceState, StencilOperation, VertexAttribute, + VertexFormat, VertexState, + }, + shader::{ShaderModule, ShaderModuleDescriptor, ShaderSource}, + texture::{ + ImageDataLayout, Texture, TextureDescriptor, TextureDimensions, TextureFormat, TextureUsage, + }, +}; +use thiserror::Error; + +use crate::resources::WgpuResources; + +#[derive(Error, Debug)] +#[non_exhaustive] +pub enum ConversionError { + #[error("the texture format {0:?} is not supported!")] + UnsupportedTextureFormat(TextureFormat), + + #[error("the shader-module {0:?} is not available!")] + ShaderModuleNotAvailable(ShaderModule), + + #[error("the texture {0:?} is not available!")] + TextureNotAvailable(Texture), + + #[error("the pipeline layout {0:?} is not available!")] + PipelineLayoutNotAvailable(PipelineLayout), +} + +pub type Result = std::result::Result; + +#[inline] +pub fn convert_buffer_descriptor(val: &BufferDescriptor) -> wgpu::BufferDescriptor<'_> { + wgpu::BufferDescriptor { + label: None, + size: val.size as u64, + usage: convert_buffer_usage(val.usage), + mapped_at_creation: true, + } +} + +#[inline] +fn convert_buffer_usage(val: BufferUsage) -> wgpu::BufferUsages { + let mut result = wgpu::BufferUsages::empty(); + if val.intersects(BufferUsage::TRANSFER_READ) { + result |= wgpu::BufferUsages::COPY_SRC; + } + if val.intersects(BufferUsage::TRANSFER_READ) { + result |= wgpu::BufferUsages::COPY_DST; + } + if val.intersects(BufferUsage::HOST_READ) { + result |= wgpu::BufferUsages::MAP_READ; + } + if val.intersects(BufferUsage::HOST_WRITE) { + result |= wgpu::BufferUsages::MAP_WRITE; + } + if val.intersects(BufferUsage::INDEX) { + result |= wgpu::BufferUsages::INDEX; + } + if val.intersects(BufferUsage::VERTEX) { + result |= wgpu::BufferUsages::VERTEX; + } + if val.intersects(BufferUsage::INDIRECT) { + result |= wgpu::BufferUsages::INDIRECT; + } + if val.intersects(BufferUsage::UNIFORM | BufferUsage::UNIFORM_TEXEL) { + result |= wgpu::BufferUsages::UNIFORM; + } + if val.intersects(BufferUsage::STORAGE | BufferUsage::STORAGE_TEXEL) { + result |= wgpu::BufferUsages::STORAGE; + } + result +} + +#[inline] +pub fn convert_texture_descriptor(val: &TextureDescriptor) -> Result> { + Ok(wgpu::TextureDescriptor { + label: None, + size: convert_extents(val.dimensions.extents()), + mip_level_count: val.mip_level_count, + sample_count: val.sample_count as u32, + dimension: convert_texture_dimensions(&val.dimensions), + format: convert_texture_format(val.format)?, + usage: convert_texture_usages(val.usage), + view_formats: &[], + }) +} + +#[inline] +pub fn convert_texture_view_descriptor(val: &TextureDescriptor) -> wgpu::TextureViewDescriptor<'_> { + wgpu::TextureViewDescriptor { + dimension: Some(convert_texture_view_dimensions(&val.dimensions)), + ..Default::default() + } +} + +#[inline] +pub fn convert_image_data_layout(image: &ImageDataLayout) -> wgpu::ImageDataLayout { + wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(image.bytes_per_row), + rows_per_image: Some(image.rows_per_image), + } +} + +#[inline] +fn convert_texture_view_dimensions(val: &TextureDimensions) -> wgpu::TextureViewDimension { + match val { + TextureDimensions::D1(_) => wgpu::TextureViewDimension::D1, + TextureDimensions::D2 { .. } => wgpu::TextureViewDimension::D2, + TextureDimensions::D2Array { .. } => wgpu::TextureViewDimension::D2Array, + TextureDimensions::Cube { .. } => wgpu::TextureViewDimension::Cube, + TextureDimensions::CubeArray { .. } => wgpu::TextureViewDimension::CubeArray, + TextureDimensions::D3 { .. } => wgpu::TextureViewDimension::D3, + } +} + +#[inline] +fn convert_texture_dimensions(val: &TextureDimensions) -> wgpu::TextureDimension { + match val { + TextureDimensions::D1(_) => wgpu::TextureDimension::D1, + TextureDimensions::D2 { .. } + | TextureDimensions::D2Array { .. } + | TextureDimensions::Cube { .. } + | TextureDimensions::CubeArray { .. } => wgpu::TextureDimension::D2, + TextureDimensions::D3 { .. } => wgpu::TextureDimension::D3, + } +} + +#[inline] +fn convert_extents(val: USize3) -> wgpu::Extent3d { + wgpu::Extent3d { + width: val.x, + height: val.y, + depth_or_array_layers: val.z, + } +} + +#[inline] +fn convert_texture_format(val: TextureFormat) -> Result { + Ok(match val { + // 8-bit formats + TextureFormat::R8Unorm => wgpu::TextureFormat::R8Unorm, + TextureFormat::R8Snorm => wgpu::TextureFormat::R8Snorm, + TextureFormat::R8Uint => wgpu::TextureFormat::R8Uint, + TextureFormat::R8Sint => wgpu::TextureFormat::R8Sint, + + // 16-bit formats + TextureFormat::R16Uint => wgpu::TextureFormat::R16Uint, + TextureFormat::R16Sint => wgpu::TextureFormat::R16Sint, + TextureFormat::R16Float => wgpu::TextureFormat::R16Float, + TextureFormat::Rg8Unorm => wgpu::TextureFormat::Rg8Unorm, + TextureFormat::Rg8Snorm => wgpu::TextureFormat::Rg8Snorm, + TextureFormat::Rg8Uint => wgpu::TextureFormat::Rg8Uint, + TextureFormat::Rg8Sint => wgpu::TextureFormat::Rg8Sint, + + // 32-bit formats + TextureFormat::R32Uint => wgpu::TextureFormat::R32Uint, + TextureFormat::R32Sint => wgpu::TextureFormat::R32Sint, + TextureFormat::R32Float => wgpu::TextureFormat::R32Float, + TextureFormat::Rg16Uint => wgpu::TextureFormat::Rg16Uint, + TextureFormat::Rg16Sint => wgpu::TextureFormat::Rg16Sint, + TextureFormat::Rg16Float => wgpu::TextureFormat::Rg16Float, + TextureFormat::Rgba8Unorm => wgpu::TextureFormat::Rgba8Unorm, + TextureFormat::Rgba8UnormSrgb => wgpu::TextureFormat::Rgba8UnormSrgb, + TextureFormat::Rgba8Snorm => wgpu::TextureFormat::Rgba8Snorm, + TextureFormat::Rgba8Uint => wgpu::TextureFormat::Rgba8Uint, + TextureFormat::Rgba8Sint => wgpu::TextureFormat::Rgba8Sint, + TextureFormat::Bgra8Unorm => wgpu::TextureFormat::Bgra8Unorm, + TextureFormat::Bgra8UnormSrgb => wgpu::TextureFormat::Bgra8UnormSrgb, + + // Packed 32-bit formats + TextureFormat::Rgb9E5Ufloat => wgpu::TextureFormat::Rgb9e5Ufloat, + TextureFormat::Rgb10A2Unorm => wgpu::TextureFormat::Rgb10a2Unorm, + TextureFormat::Rg11B10Float => wgpu::TextureFormat::Rg11b10Ufloat, + + // 64-bit formats + TextureFormat::Rg32Uint => wgpu::TextureFormat::Rg32Uint, + TextureFormat::Rg32Sint => wgpu::TextureFormat::Rg32Sint, + TextureFormat::Rg32Float => wgpu::TextureFormat::Rg32Float, + TextureFormat::Rgba16Uint => wgpu::TextureFormat::Rgba16Uint, + TextureFormat::Rgba16Sint => wgpu::TextureFormat::Rgba16Sint, + TextureFormat::Rgba16Float => wgpu::TextureFormat::Rgba16Float, + + // 128-bit formats + TextureFormat::Rgba32Uint => wgpu::TextureFormat::Rgba32Uint, + TextureFormat::Rgba32Sint => wgpu::TextureFormat::Rgba32Sint, + TextureFormat::Rgba32Float => wgpu::TextureFormat::Rgba32Float, + + // Depth and stencil formats + // TextureFormat::Stencil8 => wgpu::TextureFormat::Stencil8, + // TextureFormat::Depth16Unorm => wgpu::TextureFormat::Depth16Unorm, + TextureFormat::Depth24Plus => wgpu::TextureFormat::Depth24Plus, + TextureFormat::Depth24PlusStencil8 => wgpu::TextureFormat::Depth24PlusStencil8, + TextureFormat::Depth32Float => wgpu::TextureFormat::Depth32Float, + + _ => return Err(ConversionError::UnsupportedTextureFormat(val)), + }) +} + +#[inline] +fn convert_vertex_format(val: VertexFormat) -> Result { + Ok(match val { + VertexFormat::Uint8x2 => wgpu::VertexFormat::Uint8x2, + VertexFormat::Uint8x4 => wgpu::VertexFormat::Uint8x4, + VertexFormat::Sint8x2 => wgpu::VertexFormat::Sint8x2, + VertexFormat::Sint8x4 => wgpu::VertexFormat::Sint8x4, + VertexFormat::Unorm8x2 => wgpu::VertexFormat::Unorm8x2, + VertexFormat::Unorm8x4 => wgpu::VertexFormat::Unorm8x4, + VertexFormat::Snorm8x2 => wgpu::VertexFormat::Snorm8x2, + VertexFormat::Snorm8x4 => wgpu::VertexFormat::Snorm8x4, + VertexFormat::Uint16x2 => wgpu::VertexFormat::Uint16x2, + VertexFormat::Uint16x4 => wgpu::VertexFormat::Uint16x4, + VertexFormat::Sint16x2 => wgpu::VertexFormat::Sint16x2, + VertexFormat::Sint16x4 => wgpu::VertexFormat::Sint16x4, + VertexFormat::Unorm16x2 => wgpu::VertexFormat::Unorm16x2, + VertexFormat::Unorm16x4 => wgpu::VertexFormat::Unorm16x4, + VertexFormat::Snorm16x2 => wgpu::VertexFormat::Snorm16x2, + VertexFormat::Snorm16x4 => wgpu::VertexFormat::Snorm16x4, + VertexFormat::Uint32 => wgpu::VertexFormat::Uint32, + VertexFormat::Uint32x2 => wgpu::VertexFormat::Uint32x2, + VertexFormat::Uint32x3 => wgpu::VertexFormat::Uint32x3, + VertexFormat::Uint32x4 => wgpu::VertexFormat::Uint32x4, + VertexFormat::Sint32 => wgpu::VertexFormat::Sint32, + VertexFormat::Sint32x2 => wgpu::VertexFormat::Sint32x2, + VertexFormat::Sint32x3 => wgpu::VertexFormat::Sint32x3, + VertexFormat::Sint32x4 => wgpu::VertexFormat::Sint32x4, + //VertexFormat::Float16 => wgpu::VertexFormat::Float16, + VertexFormat::Float16x2 => wgpu::VertexFormat::Float16x2, + VertexFormat::Float16x4 => wgpu::VertexFormat::Float16x4, + VertexFormat::Float32 => wgpu::VertexFormat::Float32, + VertexFormat::Float32x2 => wgpu::VertexFormat::Float32x2, + VertexFormat::Float32x3 => wgpu::VertexFormat::Float32x3, + VertexFormat::Float32x4 => wgpu::VertexFormat::Float32x4, + VertexFormat::Float64 => wgpu::VertexFormat::Float64, + VertexFormat::Float64x2 => wgpu::VertexFormat::Float64x2, + VertexFormat::Float64x3 => wgpu::VertexFormat::Float64x3, + VertexFormat::Float64x4 => wgpu::VertexFormat::Float64x4, + + _ => panic!("unsupported vertex format: {val:?}"), + }) +} + +#[inline] +fn convert_texture_usages(val: TextureUsage) -> wgpu::TextureUsages { + let mut result = wgpu::TextureUsages::empty(); + if val.intersects(TextureUsage::TRANSFER_READ) { + result |= wgpu::TextureUsages::COPY_SRC; + } + if val.intersects(TextureUsage::TRANSFER_WRITE) { + result |= wgpu::TextureUsages::COPY_DST; + } + if val.intersects(TextureUsage::SAMPLED) { + result |= wgpu::TextureUsages::TEXTURE_BINDING; + } + if val.intersects(TextureUsage::STORAGE) { + result |= wgpu::TextureUsages::STORAGE_BINDING; + } + if val.intersects( + TextureUsage::COLOR_ATTACHMENT + | TextureUsage::DEPTH_STENCIL_ATTACHMENT + | TextureUsage::INPUT_ATTACHMENT, + ) { + result |= wgpu::TextureUsages::RENDER_ATTACHMENT; + } + result +} + +fn convert_bind_group_layout_entry(_val: BindGroupLayoutEntry) -> wgpu::BindGroupLayoutEntry { + todo!() // TODO +} + +fn convert_vertex_attribute(index: usize, attr: VertexAttribute) -> Result { + Ok(wgpu::VertexAttribute { + format: convert_vertex_format(attr.format)?, + offset: attr.offset as u64, + shader_location: index as u32, + }) +} + +pub fn convert_shader_module_descriptor<'a>( + val: &'a ShaderModuleDescriptor<'a>, +) -> wgpu::ShaderModuleDescriptor<'a> { + let source = match &val.source { + ShaderSource::Wgsl(s) => wgpu::ShaderSource::Wgsl(s.deref().into()), + // ShaderSource::Glsl(s) => wgpu::ShaderSource::Glsl(s.deref().into()), + // ShaderSource::SpirV(s) => wgpu::ShaderSource::SpirV(s.deref().into()), + #[allow(unreachable_patterns)] + _ => panic!("unsupported shader type in shader {:?}", val.label), + }; + wgpu::ShaderModuleDescriptor { + label: val.label, + source, + } +} + +pub fn convert_bind_group_layout_descriptor<'l>( + desc: &BindGroupLayoutDescriptor<'l>, + entries_tmp: &'l mut Vec, +) -> wgpu::BindGroupLayoutDescriptor<'l> { + entries_tmp.reserve_exact(desc.entries.len()); + for entry in desc.entries.iter().copied() { + entries_tmp.push(convert_bind_group_layout_entry(entry)); + } + wgpu::BindGroupLayoutDescriptor { + label: desc.label, + entries: entries_tmp, + } +} + +pub fn convert_pipeline_layout_descriptor<'l>( + _res: &WgpuResources, + desc: &PipelineLayoutDescriptor<'l>, + layouts_tmp: &'l mut Vec<&'l wgpu::BindGroupLayout>, +) -> wgpu::PipelineLayoutDescriptor<'l> { + wgpu::PipelineLayoutDescriptor { + label: desc.label, + bind_group_layouts: layouts_tmp, + push_constant_ranges: &[], // TODO + } +} + +pub fn convert_compute_pipeline_descriptor<'l, 'r: 'l>( + res: &'r WgpuResources, + desc: &ComputePipelineDescriptor<'l>, +) -> Result> { + let layout = if let Some(layout) = desc.layout { + Some( + res.pipeline_layouts + .get(layout) + .ok_or(ConversionError::PipelineLayoutNotAvailable(layout))?, + ) + } else { + None + }; + + let module = res + .shader_modules + .get(desc.module) + .ok_or(ConversionError::ShaderModuleNotAvailable(desc.module))?; + + Ok(wgpu::ComputePipelineDescriptor::<'l> { + label: desc.label, + layout, + module, + entry_point: Some(desc.entry_point), + compilation_options: Default::default(), + cache: None, + }) +} + +pub fn convert_graphics_pipeline_descriptor<'l, 'r: 'l>( + res: &'r WgpuResources, + desc: &'r GraphicsPipelineDescriptor<'_>, + buffers_tmp: &'l mut Vec>, + attribs_tmp: &'l mut Vec, + targets_tmp: &'l mut Vec>, +) -> Result> { + let layout = if let Some(layout) = desc.layout { + Some( + res.pipeline_layouts + .get(layout) + .ok_or(ConversionError::PipelineLayoutNotAvailable(layout))?, + ) + } else { + None + }; + + let vertex = convert_vertex_state(res, &desc.vertex, buffers_tmp, attribs_tmp)?; + + let depth_stencil = if let Some(ref state) = desc.depth_stencil { + Some(convert_depth_stencil_state(state)?) + } else { + None + }; + + let fragment = if let Some(ref fragment) = desc.fragment { + Some(convert_fragment_state(res, fragment, targets_tmp)?) + } else { + None + }; + + Ok(wgpu::RenderPipelineDescriptor::<'l> { + label: desc.label, + layout, + vertex, + primitive: convert_primitive_state(&desc.primitive), + depth_stencil, + multisample: wgpu::MultisampleState { + count: desc.samples, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment, + multiview: None, + cache: None, + }) +} + +fn convert_vertex_state<'l, 'r: 'l>( + res: &'r WgpuResources, + state: &'r VertexState<'_>, + buffers_tmp: &'l mut Vec>, + attributes_tmp: &'l mut Vec, +) -> Result> { + let module = res + .shader_modules + .get(state.module) + .ok_or(ConversionError::ShaderModuleNotAvailable(state.module))?; + + attributes_tmp.reserve_exact(state.buffers.iter().map(|l| l.attributes.len()).sum()); + for (i, attr) in state + .buffers + .iter() + .flat_map(|l| l.attributes.as_ref()) + .copied() + .enumerate() + { + attributes_tmp.push(convert_vertex_attribute(i, attr)?); + } + + buffers_tmp.reserve_exact(state.buffers.len()); + let mut offset = 0; + for layout in state.buffers.as_ref() { + let next_offset = offset + layout.attributes.len(); + buffers_tmp.push(wgpu::VertexBufferLayout { + array_stride: layout.array_stride as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &attributes_tmp[offset..next_offset], + }); + offset = next_offset; + } + + Ok(wgpu::VertexState::<'l> { + module, + entry_point: Some(state.entry_point), + buffers: buffers_tmp, + compilation_options: Default::default(), + }) +} + +fn convert_fragment_state<'l, 'r: 'l>( + res: &'r WgpuResources, + state: &FragmentState<'l>, + targets_tmp: &'l mut Vec>, +) -> Result> { + let module = res + .shader_modules + .get(state.module) + .ok_or(ConversionError::ShaderModuleNotAvailable(state.module))?; + + targets_tmp.reserve_exact(state.targets.len()); + for target in state.targets.as_ref() { + targets_tmp.push(convert_color_target_state(target)?); + } + + Ok(wgpu::FragmentState { + module, + entry_point: Some(state.entry_point), + targets: targets_tmp, + compilation_options: Default::default(), + }) +} + +#[inline] +fn convert_color_target_state(val: &ColorTargetState) -> Result> { + Ok(Some(wgpu::ColorTargetState { + format: convert_texture_format(val.format)?, + blend: val.blend.map(convert_blent_state), + write_mask: convert_color_write(val.write_mask), + })) +} + +#[inline] +fn convert_blent_state(val: BlendState) -> wgpu::BlendState { + wgpu::BlendState { + color: convert_blent_component(val.color), + alpha: convert_blent_component(val.alpha), + } +} + +#[inline] +fn convert_blent_component(val: BlendComponent) -> wgpu::BlendComponent { + wgpu::BlendComponent { + operation: convert_blend_operation(val.operation), + src_factor: convert_blend_factor(val.src_factor), + dst_factor: convert_blend_factor(val.dst_factor), + } +} + +#[inline] +fn convert_primitive_state(val: &PrimitiveState) -> wgpu::PrimitiveState { + wgpu::PrimitiveState { + topology: convert_primitive_topology(val.topology), + strip_index_format: None, + front_face: convert_front_face(val.front_face), + cull_mode: val.cull_mode.map(convert_face), + + polygon_mode: wgpu::PolygonMode::Fill, // TODO: + unclipped_depth: false, // TODO, + conservative: false, // TODO + } +} + +#[inline] +fn convert_depth_stencil_state(val: &DepthStencilState) -> Result { + Ok(wgpu::DepthStencilState { + format: convert_texture_format(val.format)?, + depth_write_enabled: val.depth.write_enabled, + depth_compare: convert_compare_function(val.depth.compare), + stencil: wgpu::StencilState { + front: convert_stencil_face_state(&val.stencil.front), + back: convert_stencil_face_state(&val.stencil.back), + read_mask: val.stencil.read_mask, + write_mask: val.stencil.write_mask, + }, + bias: wgpu::DepthBiasState { + constant: val.depth.bias, + slope_scale: val.depth.bias_slope_scale, + clamp: val.depth.bias_clamp, + }, + }) +} + +#[inline] +fn convert_stencil_face_state(val: &StencilFaceState) -> wgpu::StencilFaceState { + wgpu::StencilFaceState { + compare: convert_compare_function(val.compare), + fail_op: convert_stencil_operation(val.fail_op), + depth_fail_op: convert_stencil_operation(val.depth_fail_op), + pass_op: convert_stencil_operation(val.pass_op), + } +} + +#[inline] +fn convert_color_write(val: ColorWrite) -> wgpu::ColorWrites { + let mut result = wgpu::ColorWrites::empty(); + if val.contains(ColorWrite::RED) { + result |= wgpu::ColorWrites::RED; + } + if val.contains(ColorWrite::GREEN) { + result |= wgpu::ColorWrites::GREEN; + } + if val.contains(ColorWrite::BLUE) { + result |= wgpu::ColorWrites::BLUE; + } + if val.contains(ColorWrite::ALPHA) { + result |= wgpu::ColorWrites::ALPHA; + } + result +} + +#[inline] +fn convert_primitive_topology(val: PrimitiveTopology) -> wgpu::PrimitiveTopology { + match val { + PrimitiveTopology::PointList => wgpu::PrimitiveTopology::PointList, + PrimitiveTopology::LineList => wgpu::PrimitiveTopology::LineList, + PrimitiveTopology::LineStrip => wgpu::PrimitiveTopology::LineStrip, + PrimitiveTopology::TriangleList => wgpu::PrimitiveTopology::TriangleList, + PrimitiveTopology::TriangleStrip => wgpu::PrimitiveTopology::TriangleStrip, + } +} + +#[inline] +fn convert_front_face(val: FrontFace) -> wgpu::FrontFace { + match val { + FrontFace::CounterClockwise => wgpu::FrontFace::Ccw, + FrontFace::Clockwise => wgpu::FrontFace::Cw, + } +} + +#[inline] +fn convert_face(val: Face) -> wgpu::Face { + match val { + Face::Front => wgpu::Face::Front, + Face::Back => wgpu::Face::Back, + } +} + +#[inline] +fn convert_blend_operation(val: BlendOperation) -> wgpu::BlendOperation { + match val { + BlendOperation::Add => wgpu::BlendOperation::Add, + BlendOperation::Subtract => wgpu::BlendOperation::Subtract, + BlendOperation::ReverseSubtract => wgpu::BlendOperation::ReverseSubtract, + BlendOperation::Min => wgpu::BlendOperation::Min, + BlendOperation::Max => wgpu::BlendOperation::Max, + } +} + +#[inline] +fn convert_blend_factor(val: BlendFactor) -> wgpu::BlendFactor { + match val { + BlendFactor::Zero => wgpu::BlendFactor::Zero, + BlendFactor::One => wgpu::BlendFactor::One, + BlendFactor::Src => wgpu::BlendFactor::Src, + BlendFactor::OneMinusSrc => wgpu::BlendFactor::OneMinusSrc, + BlendFactor::SrcAlpha => wgpu::BlendFactor::SrcAlpha, + BlendFactor::OneMinusSrcAlpha => wgpu::BlendFactor::OneMinusSrcAlpha, + BlendFactor::Dst => wgpu::BlendFactor::Dst, + BlendFactor::OneMinusDst => wgpu::BlendFactor::OneMinusDst, + BlendFactor::DstAlpha => wgpu::BlendFactor::DstAlpha, + BlendFactor::OneMinusDstAlpha => wgpu::BlendFactor::OneMinusDstAlpha, + BlendFactor::SrcAlphaSaturated => wgpu::BlendFactor::SrcAlphaSaturated, + BlendFactor::Constant => wgpu::BlendFactor::Constant, + BlendFactor::OneMinusConstant => wgpu::BlendFactor::OneMinusConstant, + } +} + +#[inline] +fn convert_index_format(val: IndexFormat) -> wgpu::IndexFormat { + match val { + IndexFormat::Uint16 => wgpu::IndexFormat::Uint16, + IndexFormat::Uint32 => wgpu::IndexFormat::Uint32, + } +} + +#[inline] +fn convert_compare_function(val: CompareFunction) -> wgpu::CompareFunction { + match val { + CompareFunction::Never => wgpu::CompareFunction::Never, + CompareFunction::Less => wgpu::CompareFunction::Less, + CompareFunction::Equal => wgpu::CompareFunction::Equal, + CompareFunction::LessEqual => wgpu::CompareFunction::LessEqual, + CompareFunction::Greater => wgpu::CompareFunction::Greater, + CompareFunction::NotEqual => wgpu::CompareFunction::NotEqual, + CompareFunction::GreaterEqual => wgpu::CompareFunction::GreaterEqual, + CompareFunction::Always => wgpu::CompareFunction::Always, + } +} + +#[inline] +fn convert_stencil_operation(val: StencilOperation) -> wgpu::StencilOperation { + match val { + StencilOperation::Keep => wgpu::StencilOperation::Keep, + StencilOperation::Zero => wgpu::StencilOperation::Zero, + StencilOperation::Replace => wgpu::StencilOperation::Replace, + StencilOperation::Invert => wgpu::StencilOperation::Invert, + StencilOperation::IncrementClamp => wgpu::StencilOperation::IncrementClamp, + StencilOperation::DecrementClamp => wgpu::StencilOperation::DecrementClamp, + StencilOperation::IncrementWrap => wgpu::StencilOperation::IncrementWrap, + StencilOperation::DecrementWrap => wgpu::StencilOperation::DecrementWrap, + } +} + +// #[inline] +// fn convert_color_operations(val: Operations) -> wgpu::Operations { +// wgpu::Operations { +// load: match val.load { +// LoadOp::Clear(clear) => wgpu::LoadOp::Clear(convert_color(clear)), +// LoadOp::Load => wgpu::LoadOp::Load, +// }, +// store: val.store, +// } +// } + +// #[inline] +// fn convert_operations(val: Operations) -> wgpu::Operations { +// wgpu::Operations { +// load: match val.load { +// LoadOp::Clear(clear) => wgpu::LoadOp::Clear(clear), +// LoadOp::Load => wgpu::LoadOp::Load, +// }, +// store: val.store, +// } +// } + +#[inline] +fn convert_color(color: Srgba) -> wgpu::Color { + wgpu::Color { + r: color.red as f64, + g: color.green as f64, + b: color.blue as f64, + a: color.alpha as f64, + } +} + +// pub fn convert_render_pass<'l, 'r: 'l>( +// res: &'r RenderBackendResources, +// desc: &GraphicsPassDescriptor<'l>, +// tmp_color: &'l mut Vec>, +// ) -> Result> { +// tmp_color.reserve_exact(desc.color_attachments.len()); +// for a in desc.color_attachments { +// tmp_color.push(convert_color_attachment(res, a)?); +// } + +// let depth_stencil_attachment = if let Some(a) = &desc.depth_stencil_attachment { +// Some(convert_depth_stencil_attachment(res, a)?) +// } else { +// None +// }; +// Ok(wgpu::RenderPassDescriptor { +// label: desc.label, +// color_attachments: tmp_color, +// depth_stencil_attachment, +// }) +// } + +// pub fn convert_color_attachment<'r>( +// res: &'r RenderBackendResources, +// desc: &ColorAttachment, +// ) -> Result> { +// let view = res +// .textures +// .get(desc.texture) +// .ok_or(ConversionError::TextureNotAvailable(desc.texture))? +// .view(); +// let resolve_target = if let Some(resolve) = desc.resolve_target { +// Some( +// res.textures +// .get(resolve) +// .ok_or(ConversionError::TextureNotAvailable(resolve))? +// .view(), +// ) +// } else { +// None +// }; +// Ok(wgpu::RenderPassColorAttachment { +// view, +// resolve_target, +// ops: convert_color_operations(desc.ops), +// }) +// } + +// pub fn convert_depth_stencil_attachment<'r>( +// res: &'r RenderBackendResources, +// desc: &DepthStencilAttachment, +// ) -> Result> { +// let view = res +// .textures +// .get(desc.texture) +// .ok_or(ConversionError::TextureNotAvailable(desc.texture))? +// .view(); +// Ok(wgpu::RenderPassDepthStencilAttachment { +// view, +// depth_ops: desc.depth_ops.map(convert_operations), +// stencil_ops: desc.stencil_ops.map(convert_operations), +// }) +// } diff --git a/crates/render-wgpu/src/graph.rs b/crates/render-wgpu/src/graph.rs new file mode 100644 index 0000000..9b9dc13 --- /dev/null +++ b/crates/render-wgpu/src/graph.rs @@ -0,0 +1,26 @@ +use pulz_render::graph::RenderGraph; + +use crate::backend::WgpuCommandEncoder; + +pub struct WgpuRenderGraph; + +impl WgpuRenderGraph { + pub fn new() -> Self { + Self + } + + pub fn update(&mut self, _src_graph: &RenderGraph) { + todo!() + } + + pub fn execute( + &self, + _src_graph: &RenderGraph, + encoder: wgpu::CommandEncoder, + ) -> [wgpu::CommandBuffer; 1] { + let _encoder = WgpuCommandEncoder(encoder); + todo!(); + // TODO + [encoder.finish()] + } +} diff --git a/crates/render-wgpu/src/lib.rs b/crates/render-wgpu/src/lib.rs new file mode 100644 index 0000000..ba4bcf8 --- /dev/null +++ b/crates/render-wgpu/src/lib.rs @@ -0,0 +1,363 @@ +#![warn( + // missing_docs, + // rustdoc::missing_doc_code_examples, + future_incompatible, + rust_2018_idioms, + unused, + trivial_casts, + trivial_numeric_casts, + unused_lifetimes, + unused_qualifications, + unused_crate_dependencies, + clippy::cargo, + clippy::multiple_crate_versions, + clippy::empty_line_after_outer_attr, + clippy::fallible_impl_from, + clippy::redundant_pub_crate, + clippy::use_self, + clippy::suspicious_operation_groupings, + clippy::useless_let_if_seq, + // clippy::missing_errors_doc, + // clippy::missing_panics_doc, + clippy::wildcard_imports +)] +#![doc(html_logo_url = "https://raw.githubusercontent.com/HellButcher/pulz/master/docs/logo.png")] +#![doc(html_no_source)] +#![doc = include_str!("../README.md")] + +use convert::ConversionError; +use graph::WgpuRenderGraph; +use pulz_ecs::prelude::*; +use pulz_render::{RenderModule, RenderSystemPhase, draw::DrawPhases, graph::RenderGraph}; +use pulz_window::{ + DisplayHandle, Window, WindowHandle, WindowId, Windows, WindowsMirror, + listener::WindowSystemListener, +}; +use resources::WgpuResources; +use surface::Surface; +use thiserror::Error; +use tracing::info; +use wgpu::MemoryHints; + +mod backend; +mod convert; +mod graph; +mod resources; +mod surface; + +#[derive(Error, Debug)] +#[non_exhaustive] +pub enum Error { + #[error("Unable to request a suitable device!")] + NoDevice, + + #[error("The window is not available, or it has no raw-window-handle")] + WindowNotAvailable, + + #[error("The used Window-System is not supported")] + UnsupportedWindowSystem, + + #[error("Unable to request an Adapter: {0}")] + RequestAdapterError(#[from] wgpu::RequestAdapterError), + + #[error("Unable to create surface: {0}")] + CreateSurfaceError(#[from] wgpu::CreateSurfaceError), + + #[error("Unable to convert objects")] + ConversionError(#[from] ConversionError), + + #[error("unknown renderer error")] + Unknown, +} + +impl From for Error { + fn from(_: wgpu::RequestDeviceError) -> Self { + Self::NoDevice + } +} + +pub type Result = std::result::Result; + +struct WgpuRendererFull { + instance: wgpu::Instance, + adapter: wgpu::Adapter, + device: wgpu::Device, + queue: wgpu::Queue, + resources: WgpuResources, + surfaces: WindowsMirror, + graph: WgpuRenderGraph, + tmp_surface_textures: Vec, +} + +impl WgpuRendererFull { + async fn for_adapter(instance: wgpu::Instance, adapter: wgpu::Adapter) -> Result { + #[cfg(feature = "trace")] + let trace = if let Some(path) = std::env::var_os("WGPU_TRACE").map(std::path::PathBuf::from) + { + wgpu::Trace::Dictionary(path) + } else { + wgpu::Trace::Off + }; + + #[cfg(not(feature = "trace"))] + let trace = wgpu::Trace::Off; + + let (device, queue) = adapter + .request_device(&wgpu::DeviceDescriptor { + label: None, + required_features: wgpu::Features::empty(), + // Make sure we use the texture resolution limits from the adapter, so we can support images the size of the swapchain. + required_limits: wgpu::Limits::downlevel_defaults() + .using_resolution(adapter.limits()) + .using_alignment(adapter.limits()), + memory_hints: MemoryHints::Performance, + trace, + }) + .await?; + + Ok(Self { + instance, + adapter, + device, + queue, + resources: WgpuResources::new(), + surfaces: WindowsMirror::new(), + tmp_surface_textures: Vec::new(), + graph: WgpuRenderGraph::new(), + }) + } + + fn reconfigure_surfaces(&mut self, windows: &Windows) { + for (window_id, surface) in self.surfaces.iter_mut() { + if let Some(window) = windows.get(window_id) { + if surface.update(window) { + surface.configure(&self.adapter, &self.device); + } + } + } + } + + fn aquire_swapchain_images(&mut self) { + let _ = tracing::trace_span!("AquireImages").entered(); + assert_eq!(0, self.tmp_surface_textures.len()); + + self.tmp_surface_textures + .reserve_exact(self.surfaces.capacity()); + + for (_window, surface) in self.surfaces.iter_mut() { + // TODO: only affected/updated surfaces/windows + let tex = match surface.get_current_texture() { + Ok(t) => t, + Err(wgpu::SurfaceError::Outdated | wgpu::SurfaceError::Lost) => { + info!("reconfigure surface (outdated)"); + surface.configure(&self.adapter, &self.device); + surface + .get_current_texture() + .expect("Failed to acquire next surface texture!") + } + Err(e) => panic!("unable to aquire next frame: {e}"), // TODO: better error handling + }; + self.tmp_surface_textures.push(tex); + } + } + + fn present_swapchain_images(&mut self) { + let _ = tracing::trace_span!("Present").entered(); + + for surface_texture in self.tmp_surface_textures.drain(..) { + surface_texture.present(); + } + } + + fn run_graph(&mut self, src_graph: &RenderGraph) { + if self.tmp_surface_textures.is_empty() { + // skip + return; + } + let _ = tracing::trace_span!("RunGraph").entered(); + let encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + let cmds = self.graph.execute(src_graph, encoder); + self.queue.submit(cmds); + } + + fn run(&mut self, windows: &Windows, src_graph: &RenderGraph, _draw_phases: &DrawPhases) { + self.reconfigure_surfaces(windows); + self.graph.update(src_graph); + self.aquire_swapchain_images(); + self.run_graph(src_graph); + self.present_swapchain_images(); + } +} + +#[allow(clippy::large_enum_variant)] +enum WgpuRendererInner { + #[cfg(not(target_arch = "wasm32"))] + // for delaying full initialization until first surface is created. + Early { + instance: wgpu::Instance, + }, + #[cfg(not(target_arch = "wasm32"))] + Tmp, + Full(WgpuRendererFull), +} + +pub struct WgpuRenderer(WgpuRendererInner); + +impl WgpuRenderer { + pub async fn new() -> Result { + let instance = wgpu::Instance::new( + &wgpu::InstanceDescriptor { + backends: wgpu::Backends::PRIMARY, + ..Default::default() + } + .with_env(), + ); + #[cfg(target_arch = "wasm32")] + { + // full-initialization on wasm32: + // Adapter-initialization is async, and we can not block on wasm. + // Delay of initialisation is not needed on wasm, because we don't + // depend on the surface being created for getting a compatible adapter. + let adapter = Self::default_adapter(&instance, None).await?; + let renderer = Self::for_adapter(instance, adapter).await?; + return Ok(Self(WgpuRendererInner::Full(renderer))); + } + #[cfg(not(target_arch = "wasm32"))] + // Delay full initialisation until first surface is created. + Ok(Self(WgpuRendererInner::Early { instance })) + } + + /// UNSAFE: needs to ensure window is alive while surface is alive + unsafe fn init_window( + &mut self, + window_id: WindowId, + window_descriptor: &Window, + display_handle: DisplayHandle<'_>, + window_handle: WindowHandle<'_>, + ) -> Result<&mut WgpuRendererFull> { + unsafe { + if let WgpuRendererInner::Full(renderer) = &mut self.0 { + renderer.surfaces.remove(window_id); // replaces old surface + let surface = Surface::create( + &renderer.instance, + window_descriptor, + display_handle, + window_handle, + )?; + renderer.surfaces.insert(window_id, surface); + } else { + // Delayed initialization + #[cfg(not(target_arch = "wasm32"))] + { + let WgpuRendererInner::Early { instance } = + std::mem::replace(&mut self.0, WgpuRendererInner::Tmp) + else { + panic!("unexpected state"); + }; + let surface = Surface::create( + &instance, + window_descriptor, + display_handle, + window_handle, + )?; + let mut renderer = pollster::block_on(async { + let adapter = wgpu::util::initialize_adapter_from_env_or_default( + &instance, + Some(&surface), + ) + .await?; + WgpuRendererFull::for_adapter(instance, adapter).await + })?; + renderer.surfaces.insert(window_id, surface); + self.0 = WgpuRendererInner::Full(renderer); + } + } + let WgpuRendererInner::Full(renderer) = &mut self.0 else { + unreachable!() + }; + Ok(renderer) + } + } + + fn init(&mut self) -> Result<&mut WgpuRendererFull> { + #[cfg(not(target_arch = "wasm32"))] + if !matches!(self.0, WgpuRendererInner::Full { .. }) { + let WgpuRendererInner::Early { instance } = + std::mem::replace(&mut self.0, WgpuRendererInner::Tmp) + else { + panic!("unexpected state"); + }; + let renderer = pollster::block_on(async { + let adapter = + wgpu::util::initialize_adapter_from_env_or_default(&instance, None).await?; + WgpuRendererFull::for_adapter(instance, adapter).await + })?; + self.0 = WgpuRendererInner::Full(renderer); + } + let WgpuRendererInner::Full(renderer) = &mut self.0 else { + unreachable!() + }; + Ok(renderer) + } + + fn run(&mut self, windows: &mut Windows, src_graph: &RenderGraph, draw_phases: &DrawPhases) { + if let WgpuRendererInner::Full(renderer) = &mut self.0 { + renderer.run(windows, src_graph, draw_phases); + } else { + panic!("renderer uninitialized"); + } + } +} + +impl WindowSystemListener for WgpuRenderer { + fn on_created( + &mut self, + window_id: WindowId, + window_desc: &Window, + display_handle: DisplayHandle<'_>, + window_handle: WindowHandle<'_>, + ) { + // SAFETY: surface destroyed on_closed/on_suspended + unsafe { + self.init_window(window_id, window_desc, display_handle, window_handle) + .unwrap(); + } + } + fn on_resumed(&mut self) { + self.init().unwrap(); + } + fn on_closed(&mut self, window_id: WindowId) { + let WgpuRendererInner::Full(renderer) = &mut self.0 else { + return; + }; + renderer.surfaces.remove(window_id); + } + fn on_suspended(&mut self) { + let WgpuRendererInner::Full(renderer) = &mut self.0 else { + return; + }; + renderer.surfaces.clear(); + } +} + +impl ModuleWithOutput for WgpuRenderer { + type Output<'l> = &'l mut Self; + + fn install_modules(&self, res: &mut Resources) { + res.install(RenderModule); + } + + fn install_resources(self, res: &mut Resources) -> &mut Self { + let resource_id = res.insert_unsend(self); + res.init_meta_id::(resource_id); + res.get_mut_id(resource_id).unwrap() + } + + fn install_systems(schedule: &mut Schedule) { + schedule + .add_system(Self::run) + .into_phase(RenderSystemPhase::Render); + } +} diff --git a/crates/render-wgpu/src/resources.rs b/crates/render-wgpu/src/resources.rs new file mode 100644 index 0000000..7407383 --- /dev/null +++ b/crates/render-wgpu/src/resources.rs @@ -0,0 +1,192 @@ +use pulz_render::{ + backend::GpuResource, + buffer::Buffer, + pipeline::{BindGroupLayout, ComputePipeline, GraphicsPipeline, PipelineLayout}, + shader::ShaderModule, + texture::Texture, +}; +use slotmap::SlotMap; + +use crate::{Result, convert as c}; + +pub trait WgpuResource: GpuResource + 'static { + type Wgpu: 'static; + + fn create( + device: &wgpu::Device, + res: &WgpuResources, + descriptor: &Self::Descriptor<'_>, + ) -> Result; +} + +impl WgpuResource for Buffer { + type Wgpu = wgpu::Buffer; + + fn create( + device: &wgpu::Device, + _res: &WgpuResources, + descr: &Self::Descriptor<'_>, + ) -> Result { + let descr = c::convert_buffer_descriptor(descr); + let raw = device.create_buffer(&descr); + Ok(raw) + } +} + +impl WgpuResource for Texture { + type Wgpu = (wgpu::Texture, wgpu::TextureView); + + fn create( + device: &wgpu::Device, + _res: &WgpuResources, + descr: &Self::Descriptor<'_>, + ) -> Result { + let tex_descr = c::convert_texture_descriptor(descr)?; + let tex = device.create_texture(&tex_descr); + let view_descr = c::convert_texture_view_descriptor(descr); + let view = tex.create_view(&view_descr); + Ok((tex, view)) + } +} + +impl WgpuResource for ShaderModule { + type Wgpu = wgpu::ShaderModule; + + fn create( + device: &wgpu::Device, + _res: &WgpuResources, + descr: &Self::Descriptor<'_>, + ) -> Result { + let descr = c::convert_shader_module_descriptor(descr); + let raw = device.create_shader_module(descr); + Ok(raw) + } +} + +impl WgpuResource for BindGroupLayout { + type Wgpu = wgpu::BindGroupLayout; + + fn create( + device: &wgpu::Device, + _res: &WgpuResources, + descr: &Self::Descriptor<'_>, + ) -> Result { + let mut tmp = Vec::new(); + let descr = c::convert_bind_group_layout_descriptor(descr, &mut tmp); + let raw = device.create_bind_group_layout(&descr); + Ok(raw) + } +} + +impl WgpuResource for PipelineLayout { + type Wgpu = wgpu::PipelineLayout; + + fn create( + device: &wgpu::Device, + res: &WgpuResources, + descr: &Self::Descriptor<'_>, + ) -> Result { + let mut tmp = Vec::new(); + let descr = c::convert_pipeline_layout_descriptor(res, descr, &mut tmp); + let raw = device.create_pipeline_layout(&descr); + Ok(raw) + } +} + +impl WgpuResource for GraphicsPipeline { + type Wgpu = wgpu::RenderPipeline; + + fn create( + device: &wgpu::Device, + res: &WgpuResources, + descr: &Self::Descriptor<'_>, + ) -> Result { + let mut tmp1 = Vec::new(); + let mut tmp2 = Vec::new(); + let mut tmp3 = Vec::new(); + let descr = + c::convert_graphics_pipeline_descriptor(res, descr, &mut tmp1, &mut tmp2, &mut tmp3)?; + let raw = device.create_render_pipeline(&descr); + Ok(raw) + } +} + +impl WgpuResource for ComputePipeline { + type Wgpu = wgpu::ComputePipeline; + + fn create( + device: &wgpu::Device, + res: &WgpuResources, + descr: &Self::Descriptor<'_>, + ) -> Result { + let descr = c::convert_compute_pipeline_descriptor(res, descr)?; + let raw = device.create_compute_pipeline(&descr); + Ok(raw) + } +} + +macro_rules! define_resources { + ( + $v:vis struct $name:ident { + $( + $vfield:vis $namefield:ident<$keytype:ty, $ashtype:ty> + ),* + $(,)? + } + ) => { + $v struct $name { + $( + $vfield $namefield: ::slotmap::basic::SlotMap<$keytype, $ashtype> + ),* + } + + impl $name { + pub fn new() -> Self { + Self { + $( + $namefield: ::slotmap::basic::SlotMap::with_key(), + )* + } + } + + } + + $( + impl AsRef<::slotmap::basic::SlotMap<$keytype,$ashtype>> for $name { + fn as_ref(&self) -> &::slotmap::basic::SlotMap<$keytype,$ashtype> { + &self.$namefield + } + } + + impl AsMut<::slotmap::basic::SlotMap<$keytype,$ashtype>> for $name { + fn as_mut(&mut self) -> &mut ::slotmap::basic::SlotMap<$keytype,$ashtype> { + &mut self.$namefield + } + } + )* + }; +} + +define_resources! { + pub struct WgpuResources { + pub buffers, + pub textures, + pub shader_modules, + pub bind_group_layouts, + pub pipeline_layouts, + pub render_pipelines, + pub compute_pipelines, + } +} + +impl WgpuResources { + pub fn create(&mut self, device: &wgpu::Device, descriptor: &R::Descriptor<'_>) -> Result + where + R: WgpuResource, + Self: AsMut>, + { + let raw = R::create(device, self, descriptor)?; + let key = self.as_mut().insert(raw); + Ok(key) + } +} diff --git a/crates/render-wgpu/src/surface.rs b/crates/render-wgpu/src/surface.rs new file mode 100644 index 0000000..697dae5 --- /dev/null +++ b/crates/render-wgpu/src/surface.rs @@ -0,0 +1,95 @@ +use std::ops::{Deref, DerefMut}; + +use pulz_window::{DisplayHandle, Size2, Window, WindowHandle}; +use tracing::info; + +use crate::{Error, Result}; + +pub struct Surface { + surface: wgpu::Surface<'static>, + size: Size2, + vsync: bool, + format: wgpu::TextureFormat, +} + +impl Surface { + /// UNSAFE: needs to ensure, whndow is alive while surface is alive + pub unsafe fn create( + instance: &wgpu::Instance, + window: &Window, + display_handle: DisplayHandle<'_>, + window_handle: WindowHandle<'_>, + ) -> Result { + fn map_handle_error(e: raw_window_handle::HandleError) -> Error { + use raw_window_handle::HandleError; + match e { + HandleError::Unavailable => Error::WindowNotAvailable, + _ => Error::UnsupportedWindowSystem, + } + } + let raw_display_handle = display_handle.as_raw(); + let raw_window_handle = window_handle.as_raw(); + let surface = unsafe { + instance.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle { + raw_display_handle, + raw_window_handle, + })? + }; + Ok(Self { + surface, + size: window.size, + vsync: window.vsync, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + }) + } + + #[inline] + pub fn size(&self) -> Size2 { + self.size + } + + #[inline] + pub fn format(&self) -> wgpu::TextureFormat { + self.format + } + + pub fn update(&mut self, window: &Window) -> bool { + let mut changed = false; + if self.vsync != window.vsync { + info!("window vsync changed: {} => {}", self.vsync, window.vsync); + self.vsync = window.vsync; + changed = true; + } + if self.size != window.size { + info!("window size changed: {} => {}", self.size, window.size); + self.size = window.size; + changed = true; + } + changed + } + + pub fn configure(&mut self, adapter: &wgpu::Adapter, device: &wgpu::Device) { + // TODO: also reconfigure on resize, and when presenting results in `Outdated/Lost` + let surface_config = self + .surface + .get_default_config(adapter, self.size.x, self.size.y) + .expect("surface not supported by adapter"); + + self.surface.configure(device, &surface_config); + } +} + +impl Deref for Surface { + type Target = wgpu::Surface<'static>; + #[inline] + fn deref(&self) -> &Self::Target { + &self.surface + } +} + +impl DerefMut for Surface { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.surface + } +} diff --git a/crates/render/CHANGELOG.md b/crates/render/CHANGELOG.md new file mode 100644 index 0000000..6b5a9a2 --- /dev/null +++ b/crates/render/CHANGELOG.md @@ -0,0 +1,6 @@ +# `pulz-render` Changelog +All notable changes to this crate will be documented in this file. + +## Unreleased (DATE) + + * Initial version diff --git a/crates/render/Cargo.toml b/crates/render/Cargo.toml new file mode 100644 index 0000000..069ad6c --- /dev/null +++ b/crates/render/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "pulz-render" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +keywords = ["game", "game-engine"] +categories = ["game-engines", "game-development"] +readme = "README.md" + +[dependencies] +pulz-bitset = { path = "../bitset" } +pulz-ecs = { path = "../ecs" } +pulz-assets = { path = "../assets" } +pulz-transform = { path = "../transform" } +pulz-window = { path = "../window" } +pulz-render-macros = { path = "macros" } +pulz-functional-utils = { version = "0.1.0-alpha", path = "../functional-utils" } + +atomic_refcell = { workspace = true } +serde = { workspace = true } +dynsequence = { workspace = true } +palette = { workspace = true, features = ["std"], default-features = false } +image = { workspace = true, default-features = false } +slotmap = { workspace = true } +bytemuck = { workspace = true } +fnv = { workspace = true } +bitflags = { workspace = true, features = ["serde"] } +thiserror = { workspace = true } +tracing = { workspace = true } +crossbeam-queue = { workspace = true } +encase = { workspace = true } + diff --git a/crates/render/README.md b/crates/render/README.md new file mode 100644 index 0000000..3dbfb73 --- /dev/null +++ b/crates/render/README.md @@ -0,0 +1,34 @@ +# `pulz-render` + + + +[![Crates.io](https://img.shields.io/crates/v/pulz-render.svg?label=pulz-render)](https://crates.io/crates/pulz-render) +[![docs.rs](https://docs.rs/pulz-render/badge.svg)](https://docs.rs/pulz-render/) +[![license: MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](#license) +[![Rust CI](https://github.com/HellButcher/pulz/actions/workflows/rust.yml/badge.svg)](https://github.com/HellButcher/pulz/actions/workflows/rust.yml) + + +**TODO** + +## Example + + +**TODO** + +## License + +[license]: #license + +This project is licensed under either of + +* MIT license ([LICENSE-MIT] or ) +* Apache License, Version 2.0, ([LICENSE-APACHE] or ) + +at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +[LICENSE-MIT]: ../../LICENSE-MIT +[LICENSE-APACHE]: ../../LICENSE-APACHE diff --git a/crates/render/macros/Cargo.toml b/crates/render/macros/Cargo.toml new file mode 100644 index 0000000..d1b76d5 --- /dev/null +++ b/crates/render/macros/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "pulz-render-macros" +description = "Proc-Macros for pulz-render" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +keywords = ["ecs", "game"] +categories = ["data-structures", "game-engines", "game-development"] +readme = "../README.md" + +[features] +unstable = [] + +[lib] +proc-macro = true + +[dependencies] +darling = { workspace = true } +proc-macro2 = { workspace = true } +syn = { workspace = true } +quote = { workspace = true } +proc-macro-crate = { workspace = true } +encase_derive_impl = { workspace = true } diff --git a/crates/render/macros/src/binding_layout.rs b/crates/render/macros/src/binding_layout.rs new file mode 100644 index 0000000..73118a8 --- /dev/null +++ b/crates/render/macros/src/binding_layout.rs @@ -0,0 +1,294 @@ +use darling::{Error, FromMeta, Result, ToTokens, export::NestedMeta}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{TokenStreamExt, quote}; +use syn::{Attribute, Data, DeriveInput, Expr, Fields, Lit, LitInt, Meta}; + +use crate::utils::resolve_render_crate; + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum BindingType { + Uniform, + Texture, + Sampler, +} + +impl BindingType { + fn from_ident(ident: &Ident) -> Option { + if ident == "uniform" { + Some(Self::Uniform) + } else if ident == "texture" { + Some(Self::Texture) + } else if ident == "sampler" { + Some(Self::Sampler) + } else { + None + } + } +} + +impl ToTokens for BindingType { + fn to_tokens(&self, tokens: &mut TokenStream) { + let name = match self { + Self::Uniform => "uniform", + Self::Texture => "texture", + Self::Sampler => "sampler", + }; + tokens.append(Ident::new(name, Span::call_site())) + } +} + +pub fn derive_as_binding_layout(input: DeriveInput) -> Result { + let ident = input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let crate_render = resolve_render_crate()?; + + let mut layouts = Vec::new(); + + let input_struct = match input.data { + Data::Struct(input_struct) => input_struct, + Data::Enum(_) => return Err(Error::unsupported_shape_with_expected("enum", &"struct")), + Data::Union(_) => return Err(Error::unsupported_shape_with_expected("union", &"struct")), + }; + match input_struct.fields { + Fields::Unit => {} + Fields::Named(_fields) => { + // TODO: named fields + } + Fields::Unnamed(fields) => { + if fields.unnamed.len() == 1 { + // TODO: newtype / wrapper + } else { + return Err(Error::unsupported_shape_with_expected( + "tuple", + &"named struct", + )); + } + } + } + + if let Some(BindingLayoutArgs { + binding_type, + binding_index, + options: _, + }) = BindingLayoutArgs::from_attributes(&input.attrs)? + { + if binding_type != BindingType::Uniform { + return Err(Error::custom("only uniform is allowed on struct").with_span(&ident)); + } + + layouts.push(quote! { + #crate_render::pipeline::BindGroupLayoutEntry { + binding: #binding_index, + binding_type: #binding_type, + stages: #crate_render::pipeline::ShaderStages::ALL, // TODO, + } + }); + } + + Ok(quote! { + impl #impl_generics #crate_render::pipeline::AsBindingLayout for #ident #ty_generics #where_clause { + + } + }) +} + +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct BindingLayoutArgs { + binding_type: BindingType, + binding_index: LitInt, + options: BindingLayoutOptions, +} + +#[derive(FromMeta, Default, Clone, Eq, PartialEq, Debug)] +pub struct BindingLayoutOptions {} + +impl BindingLayoutArgs { + fn from_attributes(attrs: &[Attribute]) -> Result> { + let mut errors = Error::accumulator(); + let mut binding_type = None; + let mut binding_index = None; + let mut meta_list = Vec::new(); + for attr in attrs { + if let Some(ident) = attr.path().get_ident() { + if let Some(t) = BindingType::from_ident(ident) { + if binding_type.is_none() { + binding_type = Some(t); + } else if binding_type != Some(t) { + errors.push( + Error::custom("only a single attribute is allowed").with_span(ident), + ); + } + match &attr.meta { + Meta::List(meta) => { + let mut items = darling::export::NestedMeta::parse_meta_list( + meta.tokens.to_token_stream(), + )?; + if let Some(NestedMeta::Lit(Lit::Int(index))) = items.first() { + if binding_index.is_none() { + binding_index = Some(index.clone()); + } else { + errors.push( + Error::duplicate_field("binding_index").with_span(index), + ); + } + meta_list.extend_from_slice(&items[1..]); + } else { + meta_list.append(&mut items); + } + } + Meta::NameValue(meta) => { + if binding_index.is_none() { + if let Some(index) = errors.handle(parse_index(&meta.value)) { + binding_index = Some(index); + } + } else { + errors.push( + Error::duplicate_field("binding_index").with_span(&meta.value), + ); + } + } + Meta::Path(_path) => {} + } + } + } + } + errors.finish()?; + let Some(binding_type) = binding_type else { + return Ok(None); + }; + let Some(binding_index) = binding_index else { + return Err(Error::missing_field("binding_index")); + }; + let options = BindingLayoutOptions::from_list(&meta_list)?; + Ok(Some(Self { + binding_type, + binding_index, + options, + })) + } +} + +fn parse_index(expr: &Expr) -> Result { + if let Expr::Lit(lit) = expr { + if let Lit::Int(index) = &lit.lit { + Ok(index.clone()) + } else { + Err(Error::unexpected_lit_type(&lit.lit)) + } + } else { + Err(Error::unexpected_expr_type(expr)) + } +} + +#[cfg(test)] +mod tests { + use syn::{DeriveInput, FieldsNamed, LitInt}; + + use super::*; + + #[test] + fn test_empty() { + // with specified MyAttr: + let derive_input = syn::parse_str( + r#" + #[derive(AsBindingLayout)] + struct Foo; + "#, + ) + .unwrap(); + let _result = derive_as_binding_layout(derive_input).unwrap(); + // TODO: assert + } + + #[test] + fn test_newtype() { + let derive_input = syn::parse_str( + r#" + #[derive(AsBindingLayout)] + struct Foo(Bar); + "#, + ) + .unwrap(); + let _result = derive_as_binding_layout(derive_input).unwrap(); + // TODO: assert + } + + #[test] + fn test_fail_tuple() { + let derive_input = syn::parse_str( + r#" + #[derive(AsBindingLayout)] + struct Foo(Alice,Bob); + "#, + ) + .unwrap(); + let result = derive_as_binding_layout(derive_input); + match result { + Ok(tokens) => { + panic!("Expected error, got: {}", tokens); + } + Err(_err) => { + // TODO: assert + } + } + } + + #[test] + fn test_fail_enum() { + let derive_input = syn::parse_str( + r#" + #[derive(AsBindingLayout)] + enum Foo{Bar} + "#, + ) + .unwrap(); + let result = derive_as_binding_layout(derive_input); + match result { + Ok(tokens) => { + panic!("Expected error, got: {}", tokens); + } + Err(_err) => { + // TODO: assert + } + } + } + + #[test] + fn test_struct() { + // with no MyAttr: + let derive_input: DeriveInput = syn::parse_str( + r#" + #[derive(AsBindingLayout)] + #[myderive()] + struct Foo{ + a: Bar, + b: Blub, + } + "#, + ) + .unwrap(); + let _result = derive_as_binding_layout(derive_input).unwrap(); + // TODO: assert + } + + #[test] + fn test_parse_args() { + let fields: FieldsNamed = syn::parse_str( + r#"{ + #[uniform(2)] + pub foo: u32, + }"#, + ) + .unwrap(); + let args = BindingLayoutArgs::from_attributes(&fields.named[0].attrs).unwrap(); + assert_eq!( + args, + Some(BindingLayoutArgs { + binding_type: BindingType::Uniform, + binding_index: LitInt::new("2", Span::call_site()), + options: BindingLayoutOptions {}, + }) + ); + } +} diff --git a/crates/render/macros/src/compile_shader.rs b/crates/render/macros/src/compile_shader.rs new file mode 100644 index 0000000..e902e4a --- /dev/null +++ b/crates/render/macros/src/compile_shader.rs @@ -0,0 +1,26 @@ +use darling::FromMeta; +use proc_macro2::TokenStream; +use syn::{LitStr, Result}; + +#[derive(FromMeta, PartialEq, Eq, Debug)] +pub enum TargetFormat { + Wgsl, + SpirV, +} + +#[derive(FromMeta, PartialEq, Eq, Debug)] +pub struct CompileShaderArgs { + pub target_format: TargetFormat, + pub source: LitStr, +} + +impl CompileShaderArgs { + pub fn parse(input: TokenStream) -> Result { + let meta_list = darling::ast::NestedMeta::parse_meta_list(input)?; + let args = Self::from_list(&meta_list)?; + Ok(args) + } + pub fn compile(&self) -> Result { + panic!("TODO: implement: {:#?}", self); + } +} diff --git a/crates/render/macros/src/lib.rs b/crates/render/macros/src/lib.rs new file mode 100644 index 0000000..1e18890 --- /dev/null +++ b/crates/render/macros/src/lib.rs @@ -0,0 +1,35 @@ +#![cfg_attr(feature = "unstable", feature(proc_macro_span))] + +use proc_macro::TokenStream; +use syn::{DeriveInput, parse_macro_input}; +use utils::resolve_render_crate; + +#[macro_use] +mod utils; +mod binding_layout; + +#[cfg(feature = "unstable")] +mod compile_shader; + +#[proc_macro_derive(AsBindingLayout, attributes(uniform, texture, sampler))] +pub fn derive_as_binding_layout(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + binding_layout::derive_as_binding_layout(input) + .unwrap_or_else(|err| err.write_errors()) + .into() +} + +encase_derive_impl::implement! {{ + let crate_render = resolve_render_crate().unwrap(); + encase_derive_impl::syn::parse_quote!(#crate_render::shader) +}} + +/// requires #![feature(proc_macro_span)] +#[cfg(feature = "unstable")] +#[proc_macro] +pub fn compile_shader_int(input: TokenStream) -> TokenStream { + compile_shader::CompileShaderArgs::parse(input.into()) + .and_then(|args| args.compile()) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/crates/render/macros/src/utils.rs b/crates/render/macros/src/utils.rs new file mode 100644 index 0000000..9a5e927 --- /dev/null +++ b/crates/render/macros/src/utils.rs @@ -0,0 +1,24 @@ +use proc_macro_crate::FoundCrate; +use proc_macro2::{Ident, Span}; +use syn::{Error, Path, Result, Token}; + +#[cfg(test)] +pub fn resolve_render_crate() -> Result { + Ok(Path::from(Ident::new("pulz_render", Span::call_site()))) +} + +#[cfg(not(test))] +pub fn resolve_render_crate() -> Result { + resolve_crate("pulz-render") +} + +pub fn resolve_crate(name: &str) -> Result { + match proc_macro_crate::crate_name(name).map_err(|e| Error::new(Span::call_site(), e))? { + FoundCrate::Itself => Ok(Path::from(Ident::new("crate", Span::call_site()))), + FoundCrate::Name(name) => { + let mut path: Path = Ident::new(&name, Span::call_site()).into(); + path.leading_colon = Some(Token![::](Span::call_site())); + Ok(path) + } + } +} diff --git a/crates/render/src/backend.rs b/crates/render/src/backend.rs new file mode 100644 index 0000000..0d26548 --- /dev/null +++ b/crates/render/src/backend.rs @@ -0,0 +1,48 @@ +use pulz_assets::Handle; + +use crate::{ + buffer::Buffer, + camera::RenderTarget, + graph::{access::Access, resources::PhysicalResource}, + texture::{Texture, TextureDimensions, TextureFormat}, +}; + +pub trait GpuResource: slotmap::Key { + type Descriptor<'l>; +} + +macro_rules! define_gpu_resource { + ($type_name:ident, $descriptor_type:ident $(<$life:tt>)?) => { + ::slotmap::new_key_type!{ + pub struct $type_name; + } + + impl $crate::backend::GpuResource for $type_name { + type Descriptor<'l> = $descriptor_type $(<$life>)?; + } + }; +} + +// export macro to crate +pub(crate) use define_gpu_resource; + +pub trait CommandEncoder { + fn insert_debug_marker(&mut self, label: &str); + fn push_debug_group(&mut self, label: &str); + fn pop_debug_group(&mut self); +} + +pub trait PhysicalResourceResolver { + fn resolve_render_target( + &mut self, + render_target: &RenderTarget, + ) -> Option>; + fn resolve_buffer(&mut self, handle: &Handle) -> Option>; + fn create_transient_texture( + &mut self, + format: TextureFormat, + size: TextureDimensions, + access: Access, + ) -> Option; + fn create_transient_buffer(&mut self, size: usize, access: Access) -> Option; +} diff --git a/crates/render/src/buffer.rs b/crates/render/src/buffer.rs new file mode 100644 index 0000000..05f607b --- /dev/null +++ b/crates/render/src/buffer.rs @@ -0,0 +1,96 @@ +use bitflags::bitflags; +use serde::{Deserialize, Serialize}; + +use crate::graph::access::Access; + +crate::backend::define_gpu_resource!(Buffer, BufferDescriptor); + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct BufferDescriptor { + pub size: usize, + pub usage: BufferUsage, +} + +impl BufferDescriptor { + pub const fn new() -> Self { + Self { + size: 0, + usage: BufferUsage::empty(), + } + } +} + +impl Default for BufferDescriptor { + fn default() -> Self { + Self::new() + } +} +bitflags! { + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] + pub struct BufferUsage: u32 { + const TRANSFER_READ = 0x0001; + const TRANSFER_WRITE = 0x0002; + const HOST_READ = 0x0004; + const HOST_WRITE = 0x0008; + const INDEX = 0x0010; + const VERTEX = 0x0020; + const INDIRECT = 0x0040; + const UNIFORM = 0x0080; + const STORAGE = 0x0100; + const UNIFORM_TEXEL = 0x0200; + const STORAGE_TEXEL = 0x0400; + const ACCELERATION_STRUCTURE_BUILD_INPUT = 0x0800; + const ACCELERATION_STRUCTURE_STORAGE = 0x1000; + const SHADER_BINDING_TABLE = 0x2000; + const NONE = 0; + } +} + +impl Access { + pub fn as_buffer_usage(self) -> BufferUsage { + let mut result = BufferUsage::NONE; + if self.intersects(Self::INDIRECT_COMMAND_READ) { + result |= BufferUsage::INDIRECT; + } + if self.intersects(Self::INDEX_READ) { + result |= BufferUsage::INDEX; + } + if self.intersects(Self::VERTEX_ATTRIBUTE_READ) { + result |= BufferUsage::VERTEX; + } + + if self.intersects(Self::SHADER_READ | Self::SHADER_WRITE) { + result |= BufferUsage::STORAGE; + } + if self.intersects(Self::UNIFORM_READ) { + result |= BufferUsage::UNIFORM; + } + + if self.intersects(Self::TRANSFER_READ) { + result |= BufferUsage::TRANSFER_READ; + } + if self.intersects(Self::TRANSFER_WRITE) { + result |= BufferUsage::TRANSFER_WRITE; + } + if self.intersects(Self::HOST_READ) { + result |= BufferUsage::HOST_READ; + } + if self.intersects(Self::HOST_WRITE) { + result |= BufferUsage::HOST_WRITE; + } + // TODO: check this + if self.intersects( + Self::ACCELERATION_STRUCTURE_BUILD_READ | Self::ACCELERATION_STRUCTURE_BUILD_WRITE, + ) { + result |= BufferUsage::ACCELERATION_STRUCTURE_STORAGE; + } + result + } +} + +impl From for BufferUsage { + #[inline] + fn from(access: Access) -> Self { + access.as_buffer_usage() + } +} diff --git a/crates/render/src/camera.rs b/crates/render/src/camera.rs new file mode 100644 index 0000000..eeaa776 --- /dev/null +++ b/crates/render/src/camera.rs @@ -0,0 +1,267 @@ +use pulz_assets::{Assets, Handle}; +use pulz_ecs::prelude::*; +use pulz_transform::math::{Mat4, Size2, size2}; +use pulz_window::WindowId; + +use crate::{ + surface::{Surface, WindowSurfaces}, + texture::Image, +}; + +trait AsProjectionMatrix { + fn as_projection_matrix(&self) -> Mat4; + fn update_viewport(&mut self, size: Size2) -> bool; + fn far(&self) -> f32; + fn zorder_optimization(&self) -> bool; +} + +#[derive(Component)] +pub enum Projection { + Perspective(PerspectiveProjection), + Orthographic(OrthographicProjection), +} + +impl AsProjectionMatrix for Projection { + #[inline] + fn as_projection_matrix(&self) -> Mat4 { + match self { + Self::Perspective(p) => p.as_projection_matrix(), + Self::Orthographic(p) => p.as_projection_matrix(), + } + } + #[inline] + fn update_viewport(&mut self, size: Size2) -> bool { + match self { + Self::Perspective(p) => p.update_viewport(size), + Self::Orthographic(p) => p.update_viewport(size), + } + } + #[inline] + fn far(&self) -> f32 { + match self { + Self::Perspective(p) => p.far(), + Self::Orthographic(p) => p.far(), + } + } + #[inline] + fn zorder_optimization(&self) -> bool { + match self { + Self::Perspective(p) => p.zorder_optimization(), + Self::Orthographic(p) => p.zorder_optimization(), + } + } +} + +impl Default for Projection { + #[inline] + fn default() -> Self { + Self::Perspective(Default::default()) + } +} + +pub struct PerspectiveProjection { + pub fov: f32, + pub aspect_ratio: f32, + pub near: f32, + pub far: f32, +} + +impl AsProjectionMatrix for PerspectiveProjection { + #[inline] + fn as_projection_matrix(&self) -> Mat4 { + Mat4::perspective_rh(self.fov, self.aspect_ratio, self.near, self.far) + } + #[inline] + fn update_viewport(&mut self, size: Size2) -> bool { + let new_aspect_ratio = size.x / size.y; + if self.aspect_ratio != new_aspect_ratio { + self.aspect_ratio = new_aspect_ratio; + true + } else { + false + } + } + #[inline] + fn far(&self) -> f32 { + self.far + } + fn zorder_optimization(&self) -> bool { + false + } +} + +impl Default for PerspectiveProjection { + fn default() -> Self { + Self { + fov: std::f32::consts::PI / 4.0, + near: 0.1, + far: 1000.0, + aspect_ratio: 1.0, + } + } +} + +pub enum OrthographicOrigin { + Center, + BottomLeft, +} + +pub enum OrthographicScalingMode { + // use manually specified values of left/right/bottom/top as they are. + // the image will stretch wit the window! + Manual, + // use the window size + WindowSize, + // fits the given rect inside the window while keeping the aspect-ratio of the window. + AutoFit(Size2), +} + +pub struct OrthographicProjection { + pub left: f32, + pub right: f32, + pub bottom: f32, + pub top: f32, + pub near: f32, + pub far: f32, + pub scaling_mode: OrthographicScalingMode, + pub origin: OrthographicOrigin, +} + +impl AsProjectionMatrix for OrthographicProjection { + fn as_projection_matrix(&self) -> Mat4 { + Mat4::orthographic_rh( + self.left, + self.right, + self.bottom, + self.top, + self.near, + self.far, + ) + } + fn update_viewport(&mut self, size: Size2) -> bool { + let adjusted_size = match self.scaling_mode { + OrthographicScalingMode::Manual => return false, + OrthographicScalingMode::WindowSize => size, + OrthographicScalingMode::AutoFit(min) => { + if size.x * min.y > min.x * size.y { + size2(size.x * min.y / size.y, min.y) + } else { + size2(min.x, size.y * min.x / size.x) + } + } + }; + match self.origin { + OrthographicOrigin::Center => { + let half = adjusted_size / 2.0; + self.left = -half.x; + self.right = half.x; + self.bottom = -half.y; + self.top = half.y; + if let OrthographicScalingMode::WindowSize = self.scaling_mode { + self.left = self.left.floor(); + self.right = self.right.floor(); + self.bottom = self.bottom.floor(); + self.top = self.top.floor(); + } + } + OrthographicOrigin::BottomLeft => { + self.left = 0.0; + self.right = adjusted_size.x; + self.bottom = 0.0; + self.top = adjusted_size.y; + } + } + true + } + fn far(&self) -> f32 { + self.far + } + fn zorder_optimization(&self) -> bool { + true + } +} + +impl Default for OrthographicProjection { + #[inline] + fn default() -> Self { + Self { + left: -1.0, + right: 1.0, + bottom: -1.0, + top: 1.0, + near: 0.0, + far: 1000.0, + scaling_mode: OrthographicScalingMode::WindowSize, + origin: OrthographicOrigin::Center, + } + } +} + +impl From for Projection { + fn from(p: PerspectiveProjection) -> Self { + Self::Perspective(p) + } +} + +impl From for Projection { + fn from(p: OrthographicProjection) -> Self { + Self::Orthographic(p) + } +} + +#[derive(Component)] +pub struct Camera { + pub order: isize, + zorder_optimization: bool, + pub projection_matrix: Mat4, +} + +impl Default for Camera { + fn default() -> Self { + Self::new() + } +} + +impl Camera { + pub const fn new() -> Self { + Self { + order: 0, + zorder_optimization: false, + projection_matrix: Mat4::IDENTITY, + } + } +} + +#[derive(Component, Copy, Clone, Debug)] +pub enum RenderTarget { + Window(WindowId), + Image(Handle), +} + +impl RenderTarget { + pub fn resolve(&self, surfaces: &WindowSurfaces, _images: &Assets) -> Option { + match self { + Self::Window(window_id) => surfaces.get(*window_id).copied(), + Self::Image(_image_handle) => todo!("surface from image asset"), + } + } +} + +pub fn update_projections_from_render_targets( + window_surfaces: &'_ WindowSurfaces, + images: &'_ Assets, + mut projections: Query<'_, (&'_ mut Projection, &'_ RenderTarget)>, +) { + for (projection, render_target) in projections.iter() { + if let Some(surface) = render_target.resolve(window_surfaces, images) { + projection.update_viewport(surface.logical_size()); + } + } +} + +pub fn update_cameras_from_projections(mut cameras: Query<'_, (&'_ mut Camera, &'_ Projection)>) { + for (camera, projection) in cameras.iter() { + camera.zorder_optimization = projection.zorder_optimization(); + camera.projection_matrix = projection.as_projection_matrix(); + } +} diff --git a/crates/render/src/draw.rs b/crates/render/src/draw.rs new file mode 100644 index 0000000..2d83175 --- /dev/null +++ b/crates/render/src/draw.rs @@ -0,0 +1,339 @@ +use std::{ + any::{Any, TypeId}, + hash::Hash, + marker::PhantomData, + ops::Deref, +}; + +use atomic_refcell::AtomicRefCell; +use dynsequence::{DynSequence, dyn_sequence}; +use fnv::FnvHashMap as HashMap; +use pulz_ecs::{prelude::*, resource::ResState, system::data::SystemData}; + +use crate::{RenderSystemPhase, backend::CommandEncoder, utils::hash::TypeIdHashMap}; + +pub type DrawContext<'a> = &'a mut (dyn CommandEncoder + 'a); + +pub trait Drawable { + fn draw(&self, cmds: DrawContext<'_>); +} +impl Drawable for &D { + #[inline] + fn draw(&self, cmds: DrawContext<'_>) { + D::draw(self, cmds) + } +} +impl Drawable for &mut D { + #[inline] + fn draw(&self, cmds: DrawContext<'_>) { + D::draw(self, cmds) + } +} +impl Drawable for [D] { + #[inline] + fn draw(&self, cmds: DrawContext<'_>) { + for d in self { + D::draw(d, cmds) + } + } +} +impl Drawable for Box { + #[inline] + fn draw(&self, cmds: DrawContext<'_>) { + D::draw(self.as_ref(), cmds) + } +} +impl Drawable for Vec { + #[inline] + fn draw(&self, cmds: DrawContext<'_>) { + <[D]>::draw(self.as_slice(), cmds) + } +} + +impl Drawable for DynSequence { + #[inline] + fn draw(&self, cmds: DrawContext<'_>) { + <[&D]>::draw(self.as_slice(), cmds) + } +} + +pub type DynDrawables = DynSequence; + +pub trait PhaseItem: Send + Sync + Sized + 'static { + type TargetKey: Copy + Clone + Hash + Ord + Eq + Send + Sync; + fn sort(items: &mut [E]) + where + E: Deref; +} + +#[doc(hidden)] +pub struct DrawQueue(crossbeam_queue::SegQueue<(I::TargetKey, PhaseData)>); + +struct PhaseDataMap(AtomicRefCell>>); + +impl PhaseDataMap { + fn new() -> Self { + Self(AtomicRefCell::new(HashMap::default())) + } + + fn new_any() -> Box { + Box::new(Self::new()) + } + + fn get(&self, target_key: I::TargetKey) -> Option>> { + atomic_refcell::AtomicRef::filter_map(self.0.borrow(), |v| v.get(&target_key)) + } +} +pub struct PhaseData { + drawables: DynDrawables, + items: Vec>, +} + +struct PhaseDataItem { + item: I, + draw_offset: usize, + draw_count: usize, +} + +impl Deref for PhaseDataItem { + type Target = I; + #[inline] + fn deref(&self) -> &Self::Target { + &self.item + } +} + +impl PhaseData { + #[inline] + const fn new() -> Self { + Self { + drawables: DynDrawables::new(), + items: Vec::new(), + } + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } + + #[inline] + pub fn len(&self) -> usize { + self.items.len() + } + + pub fn push(&mut self, item: I) -> PhaseDraw<'_> { + let draw_offset = self.drawables.len(); + let index = self.items.len(); + self.items.push(PhaseDataItem { + draw_offset, + draw_count: 0, + item, + }); + let item = &mut self.items[index]; + PhaseDraw { + drawables: &mut self.drawables, + count: &mut item.draw_count, + } + } + + fn clear(&mut self) { + self.drawables.clear(); + self.items.clear(); + } + + fn extend(&mut self, mut other: Self) { + let drawables_offset = self.drawables.len(); + self.drawables + .extend_dynsequence(std::mem::take(&mut other.drawables)); + let mut other_items = std::mem::take(&mut other.items); + for other_item in &mut other_items { + other_item.draw_offset += drawables_offset; + } + self.items.extend(other_items); + } + + fn sort(&mut self) { + I::sort(self.items.as_mut_slice()); + } + + pub(crate) fn draw(&self, cmds: DrawContext<'_>) { + for item in &self.items { + for draw in + &self.drawables.as_slice()[item.draw_offset..item.draw_offset + item.draw_count] + { + draw.draw(cmds); + } + } + } +} + +pub struct PhaseDraw<'l> { + drawables: &'l mut DynDrawables, + count: &'l mut usize, +} + +impl PhaseDraw<'_> { + pub fn draw(&mut self, draw: D) + where + D: Drawable + Send + Sync + 'static, + { + dyn_sequence![dyn Drawable + Send + Sync + 'static | &mut self.drawables => { + push(draw); + }]; + *self.count += 1; + } +} + +pub struct DrawPhases(TypeIdHashMap>); + +impl DrawPhases { + pub fn new() -> Self { + Self(TypeIdHashMap::default()) + } +} + +impl Default for DrawPhases { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl DrawPhases { + fn get_map(&self) -> Option<&PhaseDataMap> { + self.0 + .get(&TypeId::of::>())? + .downcast_ref::>() + } + + pub fn get( + &self, + target_key: I::TargetKey, + ) -> Option>> { + self.get_map::()?.get(target_key) + } + + fn register(&mut self) { + self.0 + .entry(TypeId::of::>()) + .or_insert_with(PhaseDataMap::::new_any); + } +} + +pub struct Draw<'l, I: PhaseItem> { + destination: &'l DrawQueue, +} + +impl Draw<'_, I> { + #[inline] + pub fn draw(&mut self, target_key: I::TargetKey) -> DrawTarget<'_, I> { + DrawTarget { + draw: self, + data: PhaseData::new(), + target_key, + } + } +} + +pub struct DrawTarget<'l, I: PhaseItem> { + draw: &'l Draw<'l, I>, + data: PhaseData, + target_key: I::TargetKey, +} + +impl Default for DrawQueue { + #[inline] + fn default() -> Self { + Self(crossbeam_queue::SegQueue::new()) + } +} + +fn collect_and_sort_draws_system(queue: &mut DrawQueue, phases: &DrawPhases) { + let mut phase_map = phases.get_map::().unwrap().0.borrow_mut(); + + // clear sequences + for phase_data in phase_map.values_mut() { + phase_data.clear(); + } + + // TODO: optimize with a variant of merge-sort with pre-sorted chunks. + // pre-sort chunks inside Draw::flush, where it could utilize other threads. + for (target_key, chunk) in std::mem::take(&mut queue.0) { + phase_map + .entry(target_key) + .or_insert_with(PhaseData::new) + .extend(chunk); + } + + // remove empty sequences + phase_map.retain(|_, v| !v.items.is_empty()); + + // sort remaining sequences + for phase_data in phase_map.values_mut() { + phase_data.sort(); + } +} + +impl DrawTarget<'_, I> { + pub fn flush(&mut self) { + if !self.data.is_empty() { + // move commands into queue + self.draw.destination.0.push(( + self.target_key, + std::mem::replace(&mut self.data, PhaseData::new()), + )); + } + } + pub fn push(&mut self, item: I) -> PhaseDraw<'_> { + if self.data.len() >= 64 { + self.flush(); + } + self.data.push(item) + } +} + +impl Drop for DrawTarget<'_, I> { + fn drop(&mut self) { + self.flush(); + } +} + +impl SystemData for Draw<'_, I> { + type State = ResState>; + type Fetch<'r> = Res<'r, DrawQueue>; + type Item<'a> = Draw<'a, I>; + + fn get<'a>(fetch: &'a mut Self::Fetch<'_>) -> Self::Item<'a> { + Draw { destination: fetch } + } +} + +pub struct PhaseModule(PhantomData); + +impl Default for PhaseModule { + fn default() -> Self { + Self::new() + } +} + +impl PhaseModule { + #[inline] + pub const fn new() -> Self { + Self(PhantomData) + } +} + +impl Module for PhaseModule { + fn install_once(&self, res: &mut Resources) { + let phases = res.init::(); + res.init::>(); + res.get_mut_id(phases).unwrap().register::(); + } + + fn install_systems(schedule: &mut Schedule) { + schedule + .add_system(collect_and_sort_draws_system::) + .into_phase(RenderSystemPhase::Sorting); + } +} diff --git a/crates/render/src/graph/access.rs b/crates/render/src/graph/access.rs new file mode 100644 index 0000000..df88eb5 --- /dev/null +++ b/crates/render/src/graph/access.rs @@ -0,0 +1,313 @@ +use std::{fmt::Debug, hash::Hash}; + +use bitflags::bitflags; +use pulz_assets::Handle; +use serde::{Deserialize, Serialize}; + +use crate::{ + buffer::Buffer, + camera::RenderTarget, + texture::{Texture, TextureDimensions, TextureFormat}, +}; + +pub trait ResourceAccess: Copy + Eq + Default + Hash { + type Format: PartialEq + Debug + Copy + Hash; + type Size: PartialEq + Copy + Debug; + type ExternHandle: Debug; + + fn default_format(access: Access) -> Self::Format; + fn merge_size_max(a: Self::Size, b: Self::Size) -> Option; +} + +impl ResourceAccess for Texture { + type Format = TextureFormat; + type Size = TextureDimensions; + type ExternHandle = RenderTarget; + + #[inline] + fn default_format(access: Access) -> Self::Format { + if access.intersects( + Access::DEPTH_STENCIL_ATTACHMENT_READ + | Access::DEPTH_STENCIL_ATTACHMENT_STENCIL_WRITE + | Access::DEPTH_STENCIL_INPUT_ATTACHMENT_READ, + ) { + TextureFormat::Depth24PlusStencil8 + } else { + TextureFormat::Rgba8UnormSrgb + } + } + + #[inline] + fn merge_size_max(a: Self::Size, b: Self::Size) -> Option { + use TextureDimensions::*; + match (a, b) { + (D1(a), D1(b)) => Some(D1(a.max(b))), + (D2(a), D2(b)) => Some(D2(a.max(b))), + ( + D2Array { + size: a1, + array_len: a2, + }, + D2Array { + size: b1, + array_len: b2, + }, + ) => Some(D2Array { + size: a1.max(b1), + array_len: a2.max(b2), + }), + (Cube(a), Cube(b)) => Some(Cube(a.max(b))), + ( + CubeArray { + size: a1, + array_len: a2, + }, + CubeArray { + size: b1, + array_len: b2, + }, + ) => Some(CubeArray { + size: a1.max(b1), + array_len: a2.max(b2), + }), + (D3(a), D3(b)) => Some(D3(a.max(b))), + + _ => None, + } + } +} + +impl ResourceAccess for Buffer { + type Format = (); + type Size = usize; + type ExternHandle = Handle; + + #[inline] + fn default_format(_access: Access) -> Self::Format {} + + #[inline] + fn merge_size_max(a: usize, b: usize) -> Option { + Some(a.max(b)) + } +} + +bitflags! { + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] + pub struct Access: u32 { + const INDIRECT_COMMAND_READ = 0x00000001; + const INDEX_READ = 0x00000002; + const VERTEX_ATTRIBUTE_READ = 0x00000004; + const COLOR_INPUT_ATTACHMENT_READ = 0x00000008; + const DEPTH_STENCIL_INPUT_ATTACHMENT_READ = 0x00000010; + + // combined with shader stage: 0x??000000 + const UNIFORM_READ = 0x00000020; + const SHADER_READ = 0x00000040; + const SAMPLED_READ = 0x00000080; + + const COLOR_ATTACHMENT_READ = 0x00000100; + const DEPTH_STENCIL_ATTACHMENT_READ = 0x00000200; + const TRANSFER_READ = 0x00000400; + const HOST_READ = 0x00000800; + const ACCELERATION_STRUCTURE_READ = 0x00001000; + const ACCELERATION_STRUCTURE_BUILD_READ = 0x00002000; + const PRESENT = 0x00004000; + + // combined with shader stage: 0x??000000 + const SHADER_WRITE = 0x00010000; + + const COLOR_ATTACHMENT_WRITE = 0x00020000; + const DEPTH_STENCIL_ATTACHMENT_DEPTH_WRITE = 0x00040000; + const DEPTH_STENCIL_ATTACHMENT_STENCIL_WRITE = 0x00080000; + const DEPTH_STENCIL_ATTACHMENT_WRITE = Self::DEPTH_STENCIL_ATTACHMENT_DEPTH_WRITE.bits() | Self::DEPTH_STENCIL_ATTACHMENT_STENCIL_WRITE.bits(); + const TRANSFER_WRITE = 0x00100000; + const HOST_WRITE = 0x00200000; + const ACCELERATION_STRUCTURE_BUILD_WRITE = 0x00400000; + + const VERTEX_SHADER = 0x01000000; + const TESSELLATION_CONTROL_SHADER = 0x02000000; + const TESSELLATION_EVALUATION_SHADER = 0x04000000; + const GEOMETRY_SHADER = 0x08000000; + const FRAGMENT_SHADER = 0x10000000; + const COMPUTE_SHADER = 0x20000000; + const RAY_TRACING_SHADER = 0x40000000; + + const NONE = 0; + const ANY_READ = 0x0000FFFF; + const ANY_WRITE = 0x00FF0000; + const ANY_SHADER_STAGE = 0xFF000000; + const GRAPICS_ATTACHMENTS = Self::COLOR_INPUT_ATTACHMENT_READ.bits() | Self::DEPTH_STENCIL_INPUT_ATTACHMENT_READ.bits() | Self::COLOR_ATTACHMENT_READ.bits() | Self::COLOR_ATTACHMENT_WRITE.bits() | Self::DEPTH_STENCIL_ATTACHMENT_READ.bits() | Self::DEPTH_STENCIL_ATTACHMENT_WRITE.bits(); + const MEMORY_READ = Self::ANY_SHADER_STAGE.bits() | Self::ANY_READ.bits(); + const MEMORY_WRITE = Self::ANY_SHADER_STAGE.bits() | Self::ANY_WRITE.bits(); + const GENERAL = 0xFFFFFFFF; + } +} + +bitflags! { + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] + pub struct Stage: u32 { + // const TOP_OF_PIPE = 0x00000001; + const DRAW_INDIRECT = 0x00000002; + const VERTEX_INPUT = 0x00000004; + const VERTEX_SHADER = 0x00000008; + const TESSELLATION_CONTROL_SHADER = 0x00000010; + const TESSELLATION_EVALUATION_SHADER = 0x00000020; + const GEOMETRY_SHADER = 0x00000040; + const FRAGMENT_SHADER = 0x00000080; + const EARLY_FRAGMENT_TESTS = 0x00000100; + const LATE_FRAGMENT_TESTS = 0x00000200; + const FRAGMENT_TESTS = 0x00000300; // EARLY_FRAGMENT_TESTS | LATE_FRAGMENT_TESTS + + const COLOR_ATTACHMENT_OUTPUT = 0x00000400; + const COMPUTE_SHADER = 0x00000800; + const TRANSFER = 0x00001000; + // const BOTTOM_OF_PIPE = 0x00002000; + const HOST = 0x00004000; + // const ALL_GRAPHICS = 0x00008000; + + const ACCELERATION_STRUCTURE_BUILD = 0x02000000; + const RAY_TRACING_SHADER = 0x00200000; + + const NONE = 0; + } +} + +bitflags! { + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] + pub struct ShaderStage: u32 { + // SUBSET of Stage + const VERTEX = 0x00000008; + const TESSELLATION_CONTROL = 0x00000010; + const TESSELLATION_EVALUATION = 0x00000020; + const GEOMETRY = 0x00000040; + const FRAGMENT = 0x00000080; + const COMPUTE = 0x00000800; + const RAY_TRACING = 0x00200000; + + const NONE = 0; + } +} + +impl Access { + #[inline] + pub fn as_stage(self) -> Stage { + let mut result = Stage::NONE; + if self.intersects(Self::INDIRECT_COMMAND_READ) { + result |= Stage::DRAW_INDIRECT; + } + if self.intersects(Self::INDEX_READ | Self::VERTEX_ATTRIBUTE_READ) { + result |= Stage::VERTEX_INPUT; + } + if self.intersects( + Self::COLOR_INPUT_ATTACHMENT_READ | Self::DEPTH_STENCIL_INPUT_ATTACHMENT_READ, + ) { + result |= Stage::FRAGMENT_SHADER; + } + + if self.intersects(Self::VERTEX_SHADER) { + result |= Stage::VERTEX_SHADER; + } + if self.intersects(Self::TESSELLATION_CONTROL_SHADER) { + result |= Stage::TESSELLATION_CONTROL_SHADER; + } + if self.intersects(Self::TESSELLATION_EVALUATION_SHADER) { + result |= Stage::TESSELLATION_EVALUATION_SHADER; + } + if self.intersects(Self::GEOMETRY_SHADER) { + result |= Stage::GEOMETRY_SHADER; + } + if self.intersects(Self::FRAGMENT_SHADER) { + result |= Stage::FRAGMENT_SHADER; + } + if self.intersects(Self::COMPUTE_SHADER) { + result |= Stage::COMPUTE_SHADER; + } + if self.intersects(Self::RAY_TRACING_SHADER) { + result |= Stage::RAY_TRACING_SHADER; + } + + if self.intersects(Self::COLOR_ATTACHMENT_READ | Self::COLOR_ATTACHMENT_WRITE) { + result |= Stage::COLOR_ATTACHMENT_OUTPUT; + } + if self + .intersects(Self::DEPTH_STENCIL_ATTACHMENT_READ | Self::DEPTH_STENCIL_ATTACHMENT_WRITE) + { + result |= Stage::FRAGMENT_TESTS; + } + if self.intersects(Self::TRANSFER_READ | Self::TRANSFER_WRITE) { + result |= Stage::TRANSFER; + } + if self.intersects(Self::HOST_READ | Self::HOST_WRITE) { + result |= Stage::HOST; + } + if self.intersects( + Self::ACCELERATION_STRUCTURE_BUILD_READ | Self::ACCELERATION_STRUCTURE_BUILD_WRITE, + ) { + result |= Stage::ACCELERATION_STRUCTURE_BUILD; + } + + result + } + + #[inline] + pub const fn is_read(self) -> bool { + self.intersects(Self::ANY_READ) + } + #[inline] + pub const fn is_write(self) -> bool { + self.intersects(Self::ANY_WRITE) + } + + #[inline] + pub const fn is_graphics_attachment(self) -> bool { + self.intersects(Self::GRAPICS_ATTACHMENTS) + } +} + +impl ShaderStage { + #[inline] + pub const fn as_stage(self) -> Stage { + Stage::from_bits_truncate(self.bits()) + } + + #[inline] + pub fn as_access(self) -> Access { + let mut result = Access::NONE; + if self.contains(Self::VERTEX) { + result |= Access::VERTEX_SHADER; + } + if self.contains(Self::TESSELLATION_CONTROL) { + result |= Access::TESSELLATION_CONTROL_SHADER; + } + if self.contains(Self::TESSELLATION_EVALUATION) { + result |= Access::TESSELLATION_EVALUATION_SHADER; + } + if self.contains(Self::GEOMETRY) { + result |= Access::GEOMETRY_SHADER; + } + if self.contains(Self::FRAGMENT) { + result |= Access::FRAGMENT_SHADER; + } + if self.contains(Self::COMPUTE) { + result |= Access::COMPUTE_SHADER; + } + if self.contains(Self::RAY_TRACING) { + result |= Access::RAY_TRACING_SHADER; + } + result + } +} + +impl From for Stage { + #[inline] + fn from(shader_state: ShaderStage) -> Self { + shader_state.as_stage() + } +} + +impl From for Access { + #[inline] + fn from(shader_state: ShaderStage) -> Self { + shader_state.as_access() + } +} diff --git a/crates/render/src/graph/builder.rs b/crates/render/src/graph/builder.rs new file mode 100644 index 0000000..f1f4266 --- /dev/null +++ b/crates/render/src/graph/builder.rs @@ -0,0 +1,198 @@ +use std::{ + collections::{VecDeque, hash_map::DefaultHasher}, + hash::{Hash, Hasher}, +}; + +use pulz_bitset::BitSet; +use tracing::{debug, trace}; + +use super::{ + RenderGraph, RenderGraphBuilder, + access::ResourceAccess, + deps::DependencyMatrix, + resources::{ExtendedResourceData, Slot}, +}; +use crate::{buffer::Buffer, texture::Texture}; + +pub trait GraphImport { + fn import(&self) -> R::ExternHandle; +} + +pub trait GraphExport { + fn export(&self) -> R::ExternHandle; +} + +impl RenderGraphBuilder { + pub fn import_texture(&mut self, import_from: &I) -> Slot + where + I: GraphImport, + { + let f = import_from.import(); + self.textures.import(f) + } + + pub fn import_buffer(&mut self, import_from: &I) -> Slot + where + I: GraphImport, + { + let f = import_from.import(); + self.buffers.import(f) + } + + pub fn export_texture(&mut self, slot: Slot, export_to: &E) + where + E: GraphExport, + { + let f = export_to.export(); + let last_written_by_pass = slot.last_written_by.0; + self.passes[last_written_by_pass as usize].active = true; + self.textures.export(slot, f) + } + + pub fn export_buffer(&mut self, slot: Slot, export_to: &E) + where + E: GraphExport, + { + let f = export_to.export(); + let last_written_by_pass = slot.last_written_by.0; + self.passes[last_written_by_pass as usize].active = true; + self.buffers.export(slot, f) + } + + fn hash(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + Hash::hash(&self.textures, &mut hasher); + Hash::hash(&self.buffers, &mut hasher); + Hash::hash(&self.subpasses, &mut hasher); + Hash::hash(&self.passes, &mut hasher); + hasher.finish() + } + + pub(crate) fn reset(&mut self) { + debug_assert!(!self.is_reset); + self.is_reset = true; + self.textures.reset(); + self.buffers.reset(); + self.subpasses.clear(); + self.subpasses_run.clear(); + self.passes.clear(); + } +} + +impl RenderGraph { + fn reset(&mut self) { + self.init = false; + self.was_updated = true; + self.textures.reset(); + self.textures_ext.clear(); + self.buffers.reset(); + self.buffers_ext.clear(); + self.subpasses.clear(); + self.subpasses_exec.clear(); + self.passes.clear(); + self.pass_topo_group.clear(); + self.passes_topo_order.clear(); + } + + pub(crate) fn build_from_builder(&mut self, builder: &mut RenderGraphBuilder) { + debug_assert!(builder.is_reset); + builder.is_reset = false; + + let builder_hash = builder.hash(); + if self.init + && builder_hash == self.hash + && builder.textures.len() == self.textures.len() + && builder.buffers.len() == self.buffers.len() + && builder.subpasses.len() == self.subpasses.len() + && builder.subpasses_run.len() == self.subpasses_exec.len() + && builder.passes.len() == self.passes.len() + { + // graph not changed: swap data from builder (rest stays the same) + self.was_updated = false; + swap_graph_data(builder, self); + trace!("RenderGraph not changed"); + return; + } + + debug!( + "Updating RenderGraph with {} passes...", + builder.subpasses.len() + ); + + self.reset(); + swap_graph_data(builder, self); + self.hash = builder_hash; + + let active = self.build_active_set(); + + let mut m = self.build_dependency_matrix(&active); + m.remove_self_references(); + + self.passes_topo_order = m.into_topological_order(); + self.pass_topo_group.resize(self.passes.len(), !0); + for (i, g) in self.passes_topo_order.iter().enumerate() { + for p in g { + self.pass_topo_group[*p] = i; + } + } + + debug!("Topological order: {:?}", self.passes_topo_order); + + self.update_resource_topo_group_range(); + + self.init = true; + } + + fn build_active_set(&mut self) -> BitSet { + let mut todo = VecDeque::new(); + let mut active = BitSet::with_capacity_for(self.passes.len()); + for p in &self.passes { + if p.active { + todo.push_back(p.index); + active.insert(p.index as usize); + } + } + while let Some(next) = todo.pop_front() { + let p = &self.passes[next as usize]; + p.textures.mark_deps(&mut active, &mut todo); + p.buffers.mark_deps(&mut active, &mut todo); + } + active + } + + fn build_dependency_matrix(&self, active: &BitSet) -> DependencyMatrix { + let mut m = DependencyMatrix::new(self.passes.len()); + // TODO: only mark used nodes + for p in &self.passes { + if active.contains(p.index as usize) { + p.textures.mark_pass_dependency_matrix(&mut m, p.index); + p.buffers.mark_pass_dependency_matrix(&mut m, p.index); + } + } + m + } + + fn update_resource_topo_group_range(&mut self) { + self.textures_ext + .resize_with(self.textures.len(), ExtendedResourceData::new); + self.buffers_ext + .resize_with(self.buffers.len(), ExtendedResourceData::new); + for (i, group) in self.passes_topo_order.iter().enumerate() { + for p in group.iter().copied() { + let p = &self.passes[p]; + p.textures + .update_resource_topo_group_range(&mut self.textures_ext, i as u16); + p.buffers + .update_resource_topo_group_range(&mut self.buffers_ext, i as u16); + } + } + } +} + +fn swap_graph_data(builder: &mut RenderGraphBuilder, dest: &mut RenderGraph) { + std::mem::swap(&mut builder.textures, &mut dest.textures); + std::mem::swap(&mut builder.buffers, &mut dest.buffers); + std::mem::swap(&mut builder.subpasses, &mut dest.subpasses); + std::mem::swap(&mut builder.subpasses_run, &mut dest.subpasses_exec); + std::mem::swap(&mut builder.passes, &mut dest.passes); +} diff --git a/crates/render/src/graph/deps.rs b/crates/render/src/graph/deps.rs new file mode 100644 index 0000000..d57026b --- /dev/null +++ b/crates/render/src/graph/deps.rs @@ -0,0 +1,128 @@ +use pulz_bitset::{BitSet, BitSetIter}; + +pub struct DependencyMatrix { + num_dependencies: Vec, + num_dependents: Vec, + matrix: BitSet, +} + +impl DependencyMatrix { + pub fn new(num_nodes: usize) -> Self { + let mut num_dependencies = Vec::new(); + let mut num_dependents = Vec::new(); + num_dependencies.resize(num_nodes, 0); + num_dependents.resize(num_nodes, 0); + let deps = BitSet::with_capacity_for(num_nodes * num_nodes); + Self { + num_dependencies, + num_dependents, + matrix: deps, + } + } + + #[inline] + pub fn num_nodes(&self) -> usize { + self.num_dependencies.len() + } + + #[inline] + pub fn num_dependencies(&self, index: usize) -> usize { + self.num_dependencies.get(index).copied().unwrap_or(0) + } + + #[inline] + pub fn num_dependents(&self, index: usize) -> usize { + self.num_dependencies.get(index).copied().unwrap_or(0) + } + + pub fn dependents(&self, from: usize) -> BitSetIter<'_> { + let len = self.num_nodes(); + let start_index = len * from; + self.matrix.iter_range(start_index..start_index + len) + } + + #[inline] + fn index(&self, from: usize, to: usize) -> usize { + self.num_nodes() * from + to + } + + pub fn insert(&mut self, from: usize, to: usize) -> bool { + if self.matrix.insert(self.index(from, to)) { + self.num_dependents[from] += 1; + self.num_dependencies[to] += 1; + true + } else { + false + } + } + + pub fn contains(&self, from: usize, to: usize) -> bool { + self.matrix.contains(self.index(from, to)) + } + + pub fn remove(&mut self, from: usize, to: usize) -> bool { + if self.matrix.remove(self.index(from, to)) { + self.num_dependents[from] -= 1; + self.num_dependencies[to] -= 1; + true + } else { + false + } + } + + pub fn remove_self_references(&mut self) { + for i in 0..self.num_nodes() { + self.remove(i, i); + } + } + + pub fn remove_from_dependents(&mut self, from: usize) { + for _i in 0..self.num_nodes() { + let len = self.num_nodes(); + let start_index = len * from; + for d in self.matrix.drain(start_index..start_index + len) { + self.num_dependents[d] -= 1; + } + self.num_dependents[from] = 0; + } + } + + pub fn clear(&mut self) { + self.matrix.clear(); + for e in &mut self.num_dependencies { + *e = 0; + } + for e in &mut self.num_dependents { + *e = 0; + } + } + + pub fn into_topological_order(mut self) -> Vec> { + let mut todo = BitSet::from_range(0..self.num_nodes()); + let mut result = Vec::new(); + loop { + let mut new_nodes = Vec::new(); + let mut total_deps = 0; + for i in &todo { + let num_deps = self.num_dependencies[i]; + total_deps += num_deps; + if num_deps == 0 { + new_nodes.push(i); + } + } + + if new_nodes.is_empty() { + assert_eq!(0, total_deps, "cycle detected"); + break; + } + + for i in new_nodes.iter().copied() { + todo.remove(i); + self.remove_from_dependents(i); + } + + result.push(new_nodes); + } + result + } +} diff --git a/crates/render/src/graph/mod.rs b/crates/render/src/graph/mod.rs new file mode 100644 index 0000000..8fac172 --- /dev/null +++ b/crates/render/src/graph/mod.rs @@ -0,0 +1,260 @@ +use core::fmt; + +use self::{ + access::Access, + pass::{PipelineBindPoint, run::PassExec}, + resources::{ExtendedResourceData, Resource, ResourceDeps, ResourceSet}, +}; +use crate::{ + buffer::Buffer, + draw::{DrawContext, DrawPhases}, + texture::Texture, +}; + +pub mod access; +#[macro_use] +pub mod resources; +pub mod builder; +pub mod deps; +pub mod pass; + +pub type ResourceIndex = u16; +pub type PassIndex = u16; +type SubPassIndex = (u16, u16); + +const PASS_UNDEFINED: PassIndex = !0; +const SUBPASS_UNDEFINED: SubPassIndex = (!0, !0); + +#[derive(Hash, Debug)] +pub struct SubPassDescription { + pass_index: PassIndex, + name: &'static str, + color_attachments: Vec<(ResourceIndex, Access)>, + depth_stencil_attachment: Option<(ResourceIndex, Access)>, + input_attachments: Vec<(ResourceIndex, Access)>, +} + +#[derive(Hash, Debug)] +pub struct PassDescription { + index: PassIndex, + name: &'static str, + bind_point: PipelineBindPoint, + textures: ResourceDeps, + buffers: ResourceDeps, + begin_subpasses: usize, + end_subpasses: usize, // exclusive! + active: bool, +} + +pub struct RenderGraph { + init: bool, + hash: u64, + was_updated: bool, + textures: ResourceSet, + textures_ext: Vec, + buffers: ResourceSet, + buffers_ext: Vec, + subpasses: Vec, + subpasses_exec: Vec>, + passes: Vec, + pass_topo_group: Vec, + passes_topo_order: Vec>, +} + +pub struct RenderGraphBuilder { + is_reset: bool, + textures: ResourceSet, + buffers: ResourceSet, + subpasses: Vec, + subpasses_run: Vec>, + passes: Vec, +} + +impl RenderGraph { + #[inline] + const fn new() -> Self { + Self { + init: false, + hash: 0, + was_updated: false, + textures: ResourceSet::new(), + textures_ext: Vec::new(), + buffers: ResourceSet::new(), + buffers_ext: Vec::new(), + subpasses: Vec::new(), + subpasses_exec: Vec::new(), + passes: Vec::new(), + pass_topo_group: Vec::new(), + passes_topo_order: Vec::new(), + } + } + + #[inline] + pub const fn was_updated(&self) -> bool { + self.was_updated + } + + #[inline] + pub const fn hash(&self) -> u64 { + self.hash + } + + #[inline] + pub fn get_num_topological_groups(&self) -> usize { + self.passes_topo_order.len() + } + + #[inline] + pub fn get_num_textures(&self) -> usize { + self.textures.len() + } + + #[inline] + pub fn get_num_buffers(&self) -> usize { + self.buffers.len() + } + + #[inline] + pub(crate) fn get_texture_info(&self, index: ResourceIndex) -> Option<&Resource> { + self.textures.get(index as usize) + } + + #[inline] + pub(crate) fn get_buffer_info(&self, index: ResourceIndex) -> Option<&Resource> { + self.buffers.get(index as usize) + } + + pub fn get_topological_group( + &self, + group: usize, + ) -> impl Iterator + '_ { + self.passes_topo_order + .get(group) + .into_iter() + .flatten() + .map(|g| &self.passes[*g]) + } + + pub fn get_subpass(&self, sub_pass_index: usize) -> Option<&SubPassDescription> { + self.subpasses.get(sub_pass_index) + } + + pub fn get_pass(&self, pass_index: PassIndex) -> Option<&PassDescription> { + self.passes.get(pass_index as usize) + } + + pub fn get_topo_group_for_pass(&self, pass_index: PassIndex) -> Option { + let g = *self.pass_topo_group.get(pass_index as usize)?; + if g == !0 { None } else { Some(g) } + } + + pub fn execute_sub_pass( + &self, + sub_pass_index: usize, + draw_context: DrawContext<'_>, + draw_phases: &DrawPhases, + ) { + self.subpasses_exec[sub_pass_index].execute(draw_context, draw_phases) + } +} + +impl RenderGraphBuilder { + #[inline] + pub const fn new() -> Self { + Self { + is_reset: false, + textures: ResourceSet::new(), + buffers: ResourceSet::new(), + subpasses: Vec::new(), + subpasses_run: Vec::new(), + passes: Vec::new(), + } + } +} + +impl Default for RenderGraphBuilder { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl Default for RenderGraph { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl fmt::Debug for RenderGraph { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RenderGraph") + .field("init", &self.init) + .field("hash", &self.hash) + .field("was_updated", &self.was_updated) + .field("textures", &self.textures) + .field("textures_ext", &self.textures_ext) + .field("buffers", &self.buffers) + .field("buffers_ext", &self.buffers_ext) + .field("subpasses", &self.subpasses) + .field("passes", &self.passes) + .field("passes_topo_order", &self.passes_topo_order) + .finish() + } +} + +impl SubPassDescription { + #[inline] + pub const fn pass_index(&self) -> PassIndex { + self.pass_index + } + + #[inline] + pub const fn name(&self) -> &'static str { + self.name + } + + #[inline] + pub fn color_attachments(&self) -> &[(ResourceIndex, Access)] { + &self.color_attachments + } + #[inline] + pub fn input_attachments(&self) -> &[(ResourceIndex, Access)] { + &self.input_attachments + } + #[inline] + pub fn depth_stencil_attachment(&self) -> Option<(ResourceIndex, Access)> { + self.depth_stencil_attachment + } +} + +impl PassDescription { + #[inline] + pub const fn index(&self) -> PassIndex { + self.index + } + #[inline] + pub const fn name(&self) -> &'static str { + self.name + } + + #[inline] + pub const fn bind_point(&self) -> PipelineBindPoint { + self.bind_point + } + + #[inline] + pub const fn textures(&self) -> &ResourceDeps { + &self.textures + } + + #[inline] + pub const fn buffers(&self) -> &ResourceDeps { + &self.buffers + } + + #[inline] + pub fn sub_pass_range(&self) -> std::ops::Range { + self.begin_subpasses..self.end_subpasses + } +} diff --git a/crates/render/src/graph/pass/builder.rs b/crates/render/src/graph/pass/builder.rs new file mode 100644 index 0000000..e63906f --- /dev/null +++ b/crates/render/src/graph/pass/builder.rs @@ -0,0 +1,288 @@ +use std::marker::PhantomData; + +use super::{Graphics, Pass, PassGroup, PipelineType, SubPassDescription}; +use crate::{ + buffer::Buffer, + graph::{ + PassDescription, PassIndex, RenderGraphBuilder, SubPassIndex, + access::{Access, ShaderStage}, + resources::{ResourceDeps, Slot, SlotAccess, WriteSlot}, + }, + texture::{Texture, TextureDimensions, TextureFormat}, +}; + +impl RenderGraphBuilder { + pub fn add_pass(&mut self, pass: P) -> P::Output + where + Q: PipelineType, + P: PassGroup, + { + debug_assert!(self.is_reset); + let begin_subpasses = self.subpasses.len(); + let index = self.passes.len() as PassIndex; + let mut descr = PassDescription { + index, + name: pass.type_name(), + bind_point: Q::BIND_POINT, + textures: ResourceDeps::new(), + buffers: ResourceDeps::new(), + begin_subpasses, + end_subpasses: begin_subpasses, + active: false, + }; + + let output = pass.build(PassGroupBuilder( + PassBuilderIntern { + graph: self, + pass: &mut descr, + current_subpass: 0, + }, + PhantomData, + )); + + // update end marker + let end_subpasses = self.subpasses.len(); + // only add pass, if not empty + if begin_subpasses < end_subpasses { + descr.end_subpasses = end_subpasses; + self.passes.push(descr); + } + output + } +} + +struct PassBuilderIntern<'a> { + graph: &'a mut RenderGraphBuilder, + pass: &'a mut PassDescription, + current_subpass: u16, +} + +impl PassBuilderIntern<'_> { + #[inline] + fn current_subpass(&self) -> SubPassIndex { + (self.pass.index, self.current_subpass) + } + + fn writes_texture_intern( + &mut self, + slot: WriteSlot, + access: Access, + ) -> WriteSlot { + let current_subpass = self.current_subpass(); + assert_ne!( + slot.last_written_by, current_subpass, + "trying to write to a texture multiple times in the same sub-pass" + ); + self.pass.textures.access(&slot, access); + self.graph.textures.writes(slot, current_subpass, access) + } + + fn reads_texture_intern(&mut self, slot: Slot, access: Access) { + assert_ne!( + slot.last_written_by, + self.current_subpass(), + "trying to read and write a texture in the same sub-pass" + ); + self.pass.textures.access(&slot, access); + self.graph.textures.reads(slot, access); + } + + fn writes_buffer_intern( + &mut self, + slot: WriteSlot, + access: Access, + ) -> WriteSlot { + let current_subpass = self.current_subpass(); + assert_ne!( + slot.last_written_by, current_subpass, + "trying to write to a buffer multiple times in the same sub-pass" + ); + self.pass.buffers.access(&slot, access); + self.graph.buffers.writes(slot, current_subpass, access) + } + + fn reads_buffer_intern(&mut self, slot: Slot, access: Access) { + assert_ne!( + slot.last_written_by, + self.current_subpass(), + "trying to read and write a buffer in the same sub-pass" + ); + self.pass.buffers.access(&slot, access); + self.graph.buffers.reads(slot, access); + } +} + +pub struct PassGroupBuilder<'a, Q>(PassBuilderIntern<'a>, PhantomData); + +impl PassGroupBuilder<'_, Q> { + #[inline] + pub fn set_active(&mut self) { + self.0.pass.active = true; + } + + #[inline] + pub(super) fn push_sub_pass>(&mut self, sub_pass: P) -> P::Output { + let mut descr = SubPassDescription::new(self.0.pass.index, sub_pass.type_name()); + let (output, run) = sub_pass.build(PassBuilder { + base: PassBuilderIntern { + graph: self.0.graph, + pass: self.0.pass, + current_subpass: self.0.current_subpass, + }, + subpass: &mut descr, + _pipeline_type: PhantomData, + }); + self.0.current_subpass += 1; + self.0.graph.subpasses.push(descr); + self.0.graph.subpasses_run.push(run.erased()); + output + } +} + +impl PassGroupBuilder<'_, Graphics> { + #[inline] + pub fn sub_pass>(&mut self, sub_pass: P) -> P::Output { + self.push_sub_pass(sub_pass) + } +} + +pub struct PassBuilder<'a, Q> { + base: PassBuilderIntern<'a>, + subpass: &'a mut SubPassDescription, + _pipeline_type: PhantomData, +} + +impl PassBuilder<'_, Q> { + #[inline] + pub fn set_texture_format(&mut self, slot: &Slot, format: TextureFormat) { + self.base.graph.textures.set_format(slot, format); + } + + #[inline] + pub fn set_texture_size(&mut self, slot: &Slot, size: TextureDimensions) { + self.base.graph.textures.set_size(slot, size); + } + + #[inline] + pub fn set_buffer_size(&mut self, slot: &Slot, size: usize) { + self.base.graph.buffers.set_size(slot, size); + } + + #[inline] + pub fn reads_texture(&mut self, texture: Slot, stages: ShaderStage) { + self.base + .reads_texture_intern(texture, stages.as_access() | Access::SAMPLED_READ) + } + + #[inline] + pub fn reads_texture_storage(&mut self, texture: Slot, stages: ShaderStage) { + self.base + .reads_texture_intern(texture, stages.as_access() | Access::SHADER_READ) + } + + #[inline] + pub fn writes_texture_storage( + &mut self, + texture: WriteSlot, + stages: ShaderStage, + ) -> WriteSlot { + self.base + .writes_texture_intern(texture, stages.as_access() | Access::SHADER_WRITE) + } + + #[inline] + pub fn reads_uniform_buffer(&mut self, buffer: Slot, stages: ShaderStage) { + self.base + .reads_buffer_intern(buffer, stages.as_access() | Access::UNIFORM_READ) + } + + #[inline] + pub fn reads_buffer(&mut self, buffer: Slot, stages: ShaderStage) { + self.base + .reads_buffer_intern(buffer, stages.as_access() | Access::SHADER_READ) + } + + #[inline] + pub fn writes_buffer( + &mut self, + buffer: WriteSlot, + stages: ShaderStage, + ) -> WriteSlot { + self.base + .writes_buffer_intern(buffer, stages.as_access() | Access::SHADER_WRITE) + } +} + +impl PassBuilder<'_, Graphics> { + #[inline] + pub fn creates_color_attachment(&mut self) -> WriteSlot { + let slot = self.base.graph.textures.create(); + self.color_attachment(slot) + } + pub fn color_attachment(&mut self, texture: WriteSlot) -> WriteSlot { + self.subpass + .color_attachments + .push((texture.index(), Access::COLOR_ATTACHMENT_WRITE)); + self.base + .writes_texture_intern(texture, Access::COLOR_ATTACHMENT_WRITE) + } + + #[inline] + pub fn creates_depth_stencil_attachment(&mut self) -> WriteSlot { + let slot = self.base.graph.textures.create(); + self.depth_stencil_attachment(slot) + } + #[inline] + pub fn depth_stencil_attachment(&mut self, texture: WriteSlot) -> WriteSlot { + self.write_depth_stencil_attachment_intern(texture, Access::DEPTH_STENCIL_ATTACHMENT_WRITE) + } + + fn write_depth_stencil_attachment_intern( + &mut self, + texture: WriteSlot, + access: Access, + ) -> WriteSlot { + let old = self + .subpass + .depth_stencil_attachment + .replace((texture.index(), access)); + assert!(old.is_none(), "only one depth stencil attachment allowed"); + // TODO: support early & late fragment tests + // TODO: support readonly, write depth only, write stencil only + self.base.writes_texture_intern(texture, access) + } + + #[inline] + pub fn color_input_attachment(&mut self, texture: Slot) { + self.input_attachment_intern(texture, Access::COLOR_INPUT_ATTACHMENT_READ) + } + + #[inline] + pub fn depth_stencil_input_attachment(&mut self, texture: Slot) { + self.input_attachment_intern(texture, Access::DEPTH_STENCIL_INPUT_ATTACHMENT_READ) + } + + fn input_attachment_intern(&mut self, texture: Slot, access: Access) { + self.subpass + .input_attachments + .push((texture.index(), access)); + self.base.reads_texture_intern(texture, access) + } + + #[inline] + pub fn vertex_buffer(&mut self, buffer: Slot) { + self.base + .reads_buffer_intern(buffer, Access::VERTEX_ATTRIBUTE_READ) + } + + #[inline] + pub fn index_buffer(&mut self, buffer: Slot) { + self.base.reads_buffer_intern(buffer, Access::INDEX_READ) + } + + #[inline] + pub fn indirect_command_buffer(&mut self, buffer: Slot) { + self.base + .reads_buffer_intern(buffer, Access::INDIRECT_COMMAND_READ) + } +} diff --git a/crates/render/src/graph/pass/mod.rs b/crates/render/src/graph/pass/mod.rs new file mode 100644 index 0000000..5103f33 --- /dev/null +++ b/crates/render/src/graph/pass/mod.rs @@ -0,0 +1,90 @@ +use self::{ + builder::{PassBuilder, PassGroupBuilder}, + run::PassExec, +}; +use super::{PassIndex, SubPassDescription}; + +pub mod builder; +pub mod run; + +impl SubPassDescription { + const fn new(pass_index: PassIndex, name: &'static str) -> Self { + Self { + pass_index, + name, + color_attachments: Vec::new(), + depth_stencil_attachment: None, + input_attachments: Vec::new(), + } + } +} + +pub trait PipelineType: 'static { + const BIND_POINT: PipelineBindPoint; +} +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub enum PipelineBindPoint { + Graphics, + Compute, + RayTracing, +} + +// use the types Graphics, Compute and RayTracing also as a PipelineBindPoint Value +pub use self::PipelineBindPoint::*; + +pub enum Graphics {} +impl PipelineType for Graphics { + const BIND_POINT: PipelineBindPoint = Graphics; +} + +pub enum Compute {} +impl PipelineType for Compute { + const BIND_POINT: PipelineBindPoint = Compute; +} + +pub enum RayTracing {} +impl PipelineType for RayTracing { + const BIND_POINT: PipelineBindPoint = RayTracing; +} + +pub trait Pass { + type Output: 'static; + fn build(self, builder: PassBuilder<'_, Q>) -> (Self::Output, PassExec); + + fn type_name(&self) -> &'static str { + std::any::type_name::() + } +} + +pub trait PassGroup { + type Output; + fn build(self, builder: PassGroupBuilder<'_, Q>) -> Self::Output; + + fn type_name(&self) -> &'static str { + std::any::type_name::() + } +} + +impl> PassGroup for P { + type Output = P::Output; + #[inline] + fn build(self, mut builder: PassGroupBuilder<'_, Q>) -> Self::Output { + builder.push_sub_pass(self) + } + + fn type_name(&self) -> &'static str { + Pass::type_name(self) + } +} + +impl Pass for F +where + F: FnOnce(PassBuilder<'_, Q>) -> (O, PassExec), + O: PipelineType, +{ + type Output = O; + #[inline] + fn build(self, builder: PassBuilder<'_, Q>) -> (Self::Output, PassExec) { + self(builder) + } +} diff --git a/crates/render/src/graph/pass/run.rs b/crates/render/src/graph/pass/run.rs new file mode 100644 index 0000000..7bd49ac --- /dev/null +++ b/crates/render/src/graph/pass/run.rs @@ -0,0 +1,134 @@ +use std::{ + marker::PhantomData, + ops::{Deref, DerefMut}, +}; + +use super::{Graphics, PipelineType}; +use crate::{ + backend::CommandEncoder, + draw::{DrawContext, DrawPhases, PhaseItem}, +}; + +pub trait PassRun: Send + Sync + 'static { + fn run(&self, _ctx: PassContext<'_, Q>); +} + +trait PassRunAny: Send + Sync + 'static { + #[inline] + fn import(&self) {} + fn run(&self, draw_context: DrawContext<'_>, draw_phases: &DrawPhases); +} + +struct PassRunAnyNoop; +struct PassRunAnyWrapper(R, PhantomData); + +impl PassRunAny for PassRunAnyNoop { + #[inline] + fn run(&self, _draw_context: DrawContext<'_>, _draw_phases: &DrawPhases) {} +} + +impl PassRunAny for PassRunAnyWrapper +where + R: PassRun, + Q: PipelineType, +{ + #[inline] + fn run(&self, draw_context: DrawContext<'_>, draw_phases: &DrawPhases) { + let ctx = PassContext::<'_, Q> { + draw_context, + draw_phases, + _pipeline_type: PhantomData, + }; + PassRun::::run(&self.0, ctx); + } +} + +pub struct PassExec { + run: Box, + _phantom: PhantomData, +} + +impl PassExec { + pub fn noop() -> Self { + Self { + run: Box::new(PassRunAnyNoop), + _phantom: PhantomData, + } + } + + #[inline] + pub(crate) fn execute(&self, draw_context: DrawContext<'_>, draw_phases: &DrawPhases) { + self.run.run(draw_context, draw_phases) + } +} + +impl PassExec { + #[inline] + pub fn new_fn(run: F) -> Self + where + F: Fn(PassContext<'_, Q>) + Send + Sync + 'static, + { + Self::new(run) + } + + pub fn new(run: R) -> Self + where + R: PassRun, + { + let boxed = Box::new(PassRunAnyWrapper::(run, PhantomData)); + Self { + run: boxed, + _phantom: PhantomData, + } + } + + #[inline] + pub(crate) fn erased(self) -> PassExec<()> { + PassExec { + run: self.run, + _phantom: PhantomData, + } + } +} + +impl PassRun for F +where + F: Fn(PassContext<'_, Q>) + Send + Sync + 'static, +{ + #[inline] + fn run(&self, ctx: PassContext<'_, Q>) { + self(ctx) + } +} + +pub struct PassContext<'a, Q = Graphics> { + draw_context: DrawContext<'a>, + draw_phases: &'a DrawPhases, + _pipeline_type: PhantomData, +} + +impl PassContext<'_, Graphics> { + pub fn draw_phase_items(&mut self, target_key: I::TargetKey) + where + I: PhaseItem, + { + if let Some(phase) = self.draw_phases.get::(target_key) { + phase.draw(self.draw_context); + } + } +} + +// TODO: ambassador +impl<'a, Q> Deref for PassContext<'a, Q> { + type Target = dyn CommandEncoder + 'a; + #[inline] + fn deref(&self) -> &Self::Target { + self.draw_context + } +} +impl DerefMut for PassContext<'_, Q> { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + self.draw_context + } +} diff --git a/crates/render/src/graph/resources.rs b/crates/render/src/graph/resources.rs new file mode 100644 index 0000000..bebea1f --- /dev/null +++ b/crates/render/src/graph/resources.rs @@ -0,0 +1,973 @@ +use std::{ + collections::VecDeque, + hash::{Hash, Hasher}, + marker::PhantomData, + ops::Deref, + usize, +}; + +use pulz_assets::Handle; +use pulz_bitset::BitSet; +use pulz_window::WindowId; + +use super::{ + PASS_UNDEFINED, PassDescription, PassIndex, RenderGraph, ResourceIndex, SUBPASS_UNDEFINED, + SubPassIndex, + access::{Access, ResourceAccess, Stage}, + builder::{GraphExport, GraphImport}, + deps::DependencyMatrix, +}; +use crate::{ + backend::PhysicalResourceResolver, + buffer::Buffer, + camera::RenderTarget, + texture::{Image, Texture, TextureDimensions, TextureFormat}, +}; + +#[derive(Copy, Clone)] +pub struct SlotRaw { + pub(crate) index: ResourceIndex, + pub(crate) last_written_by: SubPassIndex, +} + +#[derive(Copy, Clone)] +pub struct Slot { + raw: SlotRaw, + _phantom: PhantomData R>, +} + +impl std::fmt::Debug for Slot { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let typename = std::any::type_name::(); + f.debug_tuple(&format!("Slot<{typename}>")) + .field(&self.raw.index) + .finish() + } +} + +impl Deref for Slot { + type Target = SlotRaw; + #[inline] + fn deref(&self) -> &SlotRaw { + &self.raw + } +} + +// Not Copy by intention! +pub struct WriteSlot(Slot); + +impl std::fmt::Debug for WriteSlot { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let typename = std::any::type_name::(); + f.debug_tuple(&format!("WriteSlot<{typename}>")) + .field(&self.raw.index) + .finish() + } +} + +impl Deref for WriteSlot { + type Target = Slot; + #[inline] + fn deref(&self) -> &Slot { + &self.0 + } +} + +pub trait SlotAccess { + const WRITE: bool; + fn index(&self) -> ResourceIndex; +} + +impl SlotAccess for Slot { + const WRITE: bool = false; + #[inline] + fn index(&self) -> ResourceIndex { + self.raw.index + } +} + +impl SlotAccess for WriteSlot { + const WRITE: bool = true; + #[inline] + fn index(&self) -> ResourceIndex { + self.raw.index + } +} + +impl Slot { + const fn new(index: ResourceIndex, last_written_by: SubPassIndex) -> Self { + Self { + raw: SlotRaw { + index, + last_written_by, + }, + _phantom: PhantomData, + } + } +} + +impl WriteSlot { + #[inline] + const fn new(index: ResourceIndex, last_written_by: SubPassIndex) -> Self { + Self(Slot::new(index, last_written_by)) + } + #[inline] + pub const fn read(self) -> Slot { + self.0 + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ResourceVariant { + Transient, + Import, + Export, +} + +#[derive(Debug)] +pub(crate) struct Resource { + first_written: SubPassIndex, + last_written: SubPassIndex, + is_read_after_last_written: bool, + access: Access, + format: Option, + size: Option, + variant: ResourceVariant, + extern_assignment: Option, +} + +#[derive(Debug)] +pub(super) struct ExtendedResourceData { + pub first_topo_group: u16, + pub last_topo_group: u16, +} + +#[derive(Debug)] +pub(super) struct ResourceSet { + resources: Vec>, +} + +impl Resource { + #[inline] + fn format_or_default(&self) -> R::Format { + if let Some(f) = self.format { + f + } else { + R::default_format(self.access) + } + } + + #[inline] + pub fn access(&self) -> Access { + self.access + } + + #[inline] + pub fn size(&self) -> Option { + self.size + } + + #[inline] + pub fn variant(&self) -> ResourceVariant { + self.variant + } +} + +impl ExtendedResourceData { + #[inline] + pub const fn new() -> Self { + Self { + first_topo_group: !0, + last_topo_group: 0, + } + } + + #[inline] + fn is_active(&self) -> bool { + self.first_topo_group <= self.last_topo_group + } +} + +impl Hash for ResourceSet { + fn hash(&self, state: &mut H) { + self.resources.hash(state); + // ignore transients + } +} + +impl Hash for Resource { + fn hash(&self, state: &mut H) { + self.first_written.hash(state); + self.last_written.hash(state); + self.access.hash(state); + self.variant.hash(state); + self.format.hash(state); + // ignore size and extern assignment! + } +} + +#[derive(Debug)] +pub struct ResourceDeps(Vec); + +impl Hash for ResourceDeps { + fn hash(&self, state: &mut H) { + Hash::hash_slice(&self.0, state); + } +} + +#[derive(Hash, Debug)] +pub struct ResourceDep { + index: ResourceIndex, + last_written_by_pass: PassIndex, + access: Access, +} + +impl ResourceSet { + #[inline] + pub(super) const fn new() -> Self { + Self { + resources: Vec::new(), + } + } + + pub fn len(&self) -> usize { + self.resources.len() + } + + pub(super) fn reset(&mut self) { + self.resources.clear(); + } + + pub(super) fn create(&mut self) -> WriteSlot { + let index = self.resources.len() as ResourceIndex; + self.resources.push(Resource { + first_written: SUBPASS_UNDEFINED, + last_written: SUBPASS_UNDEFINED, + is_read_after_last_written: false, + access: Access::empty(), + format: None, + size: None, + variant: ResourceVariant::Transient, + extern_assignment: None, + }); + WriteSlot::new(index, SUBPASS_UNDEFINED) + } + + pub(super) fn get(&self, index: usize) -> Option<&Resource> { + self.resources.get(index) + } + + pub(super) fn set_format(&mut self, slot: &Slot, format: R::Format) { + let slot = &mut self.resources[slot.index as usize]; + if let Some(old_format) = &slot.format { + assert_eq!(old_format, &format, "incompatible format"); + } + slot.format = Some(format); + } + + pub(super) fn set_size(&mut self, slot: &Slot, size: R::Size) { + let slot = &mut self.resources[slot.index as usize]; + slot.size = Some(size); + } + + pub(super) fn writes( + &mut self, + slot: WriteSlot, + new_pass: SubPassIndex, + access: Access, + ) -> WriteSlot { + assert!(access.is_empty() || access.is_write()); + let r = &mut self.resources[slot.0.index as usize]; + let last_written_by_pass = r.last_written; + assert_eq!( + last_written_by_pass, slot.0.last_written_by, + "resource also written by an other pass (slot out of sync)" + ); + r.access |= access; + if new_pass != last_written_by_pass { + r.is_read_after_last_written = false; + r.last_written = new_pass; + if r.first_written.0 == PASS_UNDEFINED { + r.first_written = new_pass + } + } + WriteSlot::new(slot.0.index, new_pass) + } + + pub(super) fn reads(&mut self, slot: Slot, access: Access) { + assert!(access.is_empty() || access.is_read()); + assert_ne!( + slot.last_written_by.0, PASS_UNDEFINED, + "resource was not yet written!" + ); + let r = &mut self.resources[slot.index as usize]; + let last_written_by_pass = r.last_written; + // TODO: allow usage of older slots for reading (Write>Read>Write) + assert_eq!( + last_written_by_pass, slot.last_written_by, + "resource also written by an other pass (slot out of sync)" + ); + r.is_read_after_last_written = true; + r.access |= access; + } + + pub(super) fn import(&mut self, extern_resource: R::ExternHandle) -> Slot { + let slot = self.create(); + let r = &mut self.resources[slot.index as usize]; + r.variant = ResourceVariant::Import; + r.extern_assignment = Some(extern_resource); + slot.read() + } + + pub(super) fn export(&mut self, slot: Slot, extern_resource: R::ExternHandle) { + let r = &mut self.resources[slot.index as usize]; + assert_eq!( + ResourceVariant::Transient, + r.variant, + "resource can be exported only once" + ); + assert_eq!( + None, r.format, + "format of slot must be undefined for exports. Export target format will be used." + ); + assert_eq!( + None, r.size, + "size of slot must be undefined for exports. Export target size will be used." + ); + // TODO: allow multiple exports by copying resource? + r.variant = ResourceVariant::Export; + r.extern_assignment = Some(extern_resource); + } +} + +impl ResourceDeps { + #[inline] + pub fn deps(&self) -> &[ResourceDep] { + &self.0 + } + + pub fn find_by_resource_index(&self, resource_index: ResourceIndex) -> Option<&ResourceDep> { + if let Ok(i) = self.0.binary_search_by_key(&resource_index, |d| d.index) { + Some(&self.0[i]) + } else { + None + } + } + + #[inline] + pub(super) const fn new() -> Self { + Self(Vec::new()) + } + + pub(super) fn mark_deps(&self, marks: &mut BitSet, todo: &mut VecDeque) { + for dep in &self.0 { + let pass_index = dep.src_pass(); + if pass_index != !0 && marks.insert(pass_index as usize) { + todo.push_back(pass_index); + } + } + } + + pub(super) fn mark_pass_dependency_matrix(&self, m: &mut DependencyMatrix, to_pass: PassIndex) { + for dep in &self.0 { + let pass_index = dep.src_pass(); + if pass_index != !0 { + m.insert(pass_index as usize, to_pass as usize); + } + } + } + + pub(super) fn update_resource_topo_group_range( + &self, + res: &mut [ExtendedResourceData], + group_index: u16, + ) { + for dep in &self.0 { + let d = &mut res[dep.resource_index() as usize]; + if d.first_topo_group > group_index { + d.first_topo_group = group_index; + } + if d.last_topo_group < group_index { + d.last_topo_group = group_index; + } + } + } + + pub(super) fn access(&mut self, slot: &SlotRaw, access: Access) -> bool { + match self.0.binary_search_by_key(&slot.index, |e| e.index) { + Ok(i) => { + let entry = &mut self.0[i]; + assert_eq!(entry.last_written_by_pass, slot.last_written_by.0); + entry.access |= access; + if access.is_write() { + //TODO: R::check_usage_is_pass_compatible(entry.usage); + } + false + } + Err(i) => { + self.0.insert( + i, + ResourceDep { + index: slot.index, + last_written_by_pass: slot.last_written_by.0, + access, + }, + ); + true + } + } + } +} + +impl Deref for ResourceDeps { + type Target = [ResourceDep]; + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ResourceDep { + #[inline] + pub fn resource_index(&self) -> ResourceIndex { + self.index + } + + #[inline] + pub fn src_pass(&self) -> PassIndex { + self.last_written_by_pass + } + + #[inline] + pub fn stages(&self) -> Stage { + self.access.as_stage() + } + + #[inline] + pub fn access(&self) -> Access { + self.access + } + + #[inline] + pub fn is_read(&self) -> bool { + self.access.is_read() + } + + #[inline] + pub fn is_write(&self) -> bool { + self.access.is_write() + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct PhysicalResource { + pub resource: R, + pub format: R::Format, + pub size: R::Size, + pub access: Access, +} + +impl Hash for PhysicalResource { + fn hash(&self, state: &mut H) { + self.format.hash(state); + self.access.hash(state); + // ignore resource and size + } +} + +#[derive(Hash, Debug)] +struct TransientResource { + physical: PhysicalResource, + first_topo_group: u16, + last_topo_group: u16, +} + +#[derive(Copy, Clone, Hash, Debug)] +enum ExternalOrTransient { + None, + External(u16), + Transient(u16), +} + +#[derive(Hash, Debug)] +struct PhysicalResourceSet { + assignments: Vec, + assignment_sizes: Vec>, + externals: Vec>, + transients: Vec>, +} + +trait ResolveExtern { + fn resolve_extern(&mut self, handle: &R::ExternHandle) -> Option>; +} +trait CreateTransient { + fn create_transient(&mut self, format: R::Format, size: R::Size, access: Access) -> Option; +} + +impl PhysicalResourceSet { + #[inline] + pub(super) const fn new() -> Self { + Self { + assignments: Vec::new(), + assignment_sizes: Vec::new(), + externals: Vec::new(), + transients: Vec::new(), + } + } + + pub fn len(&self) -> usize { + self.assignments.len() + } + + fn reset(&mut self, resources: &ResourceSet) { + self.assignments.clear(); + self.assignment_sizes.clear(); + self.externals.clear(); + self.transients.clear(); + self.assignments + .resize_with(resources.len(), || ExternalOrTransient::None); + for res in resources.resources.iter() { + self.assignment_sizes.push(res.size); + } + } + + fn get_physical(&self, idx: ResourceIndex) -> Option<&PhysicalResource> { + match self.assignments.get(idx as usize).copied()? { + ExternalOrTransient::None => None, + ExternalOrTransient::External(e) => self.externals.get(e as usize), + ExternalOrTransient::Transient(t) => { + self.transients.get(t as usize).map(|t| &t.physical) + } + } + } + + fn get_or_create_transient( + transients: &mut Vec>, + format: R::Format, + size: R::Size, + access: Access, + first_topo_group: u16, + last_topo_group: u16, + ) -> u16 { + for (j, p) in transients.iter_mut().enumerate() { + if format == p.physical.format && p.last_topo_group < first_topo_group { + if let Some(s) = R::merge_size_max(p.physical.size, size) { + p.physical.size = s; + p.physical.access |= access; + p.last_topo_group = last_topo_group; + return j as u16; + } + } + } + let index = transients.len(); + transients.push(TransientResource { + physical: PhysicalResource { + resource: R::default(), + format, + size, + access, + }, + first_topo_group, + last_topo_group, + }); + index as u16 + } + + fn assign_externals>( + &mut self, + resources: &ResourceSet, + resources_data: &[ExtendedResourceData], + backend: &mut B, + ) { + // assign externals + for (i, r) in resources.resources.iter().enumerate() { + if !resources_data[i].is_active() { + continue; + } + if let Some(extern_handle) = &r.extern_assignment { + if let Some(ext) = backend.resolve_extern(extern_handle) { + // TODO: check usage compatible? + let external_index = self.externals.len() as u16; + self.assignments[i] = ExternalOrTransient::External(external_index); + self.assignment_sizes[i] = Some(ext.size); + self.externals.push(ext); + } else { + panic!( + "unable to resolve external resource {:?}, first_written={:?}", + i, r.first_written + ); + } + } + } + } + + fn assign_transient>( + &mut self, + resources: &ResourceSet, + resources_data: &[ExtendedResourceData], + backend: &mut B, + ) { + let mut res_sorted: Vec<_> = (0..resources.len()).collect(); + res_sorted.sort_by_key(|&r| resources_data[r].first_topo_group); + + // pre-assign transients + for &i in res_sorted.iter() { + let r = &resources.resources[i]; + let d = &resources_data[i]; + if d.is_active() && matches!(self.assignments[i], ExternalOrTransient::None) { + if r.access.is_empty() { + panic!( + "transient usage is empty, {:?}, {:?}, {}, {}, {:?}, {:?}", + r.size, + r.format, + d.first_topo_group, + d.last_topo_group, + r.first_written, + r.access + ); + } + let transient_index = Self::get_or_create_transient( + &mut self.transients, + r.format_or_default(), + self.assignment_sizes[i].expect("missing size"), + r.access, + d.first_topo_group, + d.last_topo_group, + ); + self.assignments[i] = ExternalOrTransient::Transient(transient_index); + } + } + + for trans in self.transients.iter_mut() { + trans.physical.resource = backend + .create_transient( + trans.physical.format, + trans.physical.size, + trans.physical.access, + ) + .expect("unable to create transient"); // TODO: error + } + } +} + +impl PhysicalResourceSet { + fn derive_framebuffer_sizes(&mut self, passes: &[PassDescription]) { + for pass in passes { + if pass.active { + self.derive_framebuffer_size_for_pass(pass); + } + } + } + + fn derive_framebuffer_size_for_pass(&mut self, pass: &PassDescription) { + let mut pass_size = None; + let mut empty = true; + for r in pass.textures().iter() { + if r.access().is_graphics_attachment() { + empty = false; + if let Some(s) = self.assignment_sizes[r.index as usize] { + if let Some(s2) = pass_size { + assert_eq!(s2, s, "pass attachments have to be the same size"); + } else { + pass_size = Some(s); + } + } + } + } + if empty { + return; + } + if pass_size.is_none() { + panic!( + "unable to derive framebuffer size for pass {:?}, physical_resource_set={:?}", + pass.name, self + ); + } + + for r in pass.textures().iter() { + if r.access().is_graphics_attachment() { + let s = &mut self.assignment_sizes[r.index as usize]; + if s.is_none() { + *s = pass_size; + } + } + } + } +} + +#[derive(Hash, Debug)] +pub struct PhysicalResources { + textures: PhysicalResourceSet, + buffers: PhysicalResourceSet, + hash: u64, +} + +impl Default for PhysicalResources { + fn default() -> Self { + Self::new() + } +} + +impl PhysicalResources { + pub const fn new() -> Self { + Self { + textures: PhysicalResourceSet::new(), + buffers: PhysicalResourceSet::new(), + hash: 0, + } + } + + pub fn assign_physical( + &mut self, + graph: &RenderGraph, + backend: &mut B, + ) -> bool { + self.textures.reset(&graph.textures); + self.buffers.reset(&graph.buffers); + + self.textures + .assign_externals(&graph.textures, &graph.textures_ext, backend); + self.buffers + .assign_externals(&graph.buffers, &graph.buffers_ext, backend); + + self.textures.derive_framebuffer_sizes(&graph.passes); + + self.textures + .assign_transient(&graph.textures, &graph.textures_ext, backend); + self.buffers + .assign_transient(&graph.buffers, &graph.buffers_ext, backend); + + let new_hash = { + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + self.textures.hash(&mut hasher); + self.buffers.hash(&mut hasher); + hasher.finish() + }; + let changed = self.hash != new_hash; + self.hash = new_hash; + changed + } + + pub fn get_texture(&self, idx: ResourceIndex) -> Option<&PhysicalResource> { + self.textures.get_physical(idx) + } + + pub fn get_buffer(&self, idx: ResourceIndex) -> Option<&PhysicalResource> { + self.buffers.get_physical(idx) + } +} + +struct PhysicalResourceMap { + external: Vec, + transient: Vec, + _phanton: PhantomData R>, +} + +impl PhysicalResourceMap { + pub const fn new() -> Self { + Self { + external: Vec::new(), + transient: Vec::new(), + _phanton: PhantomData, + } + } + fn clear(&mut self) { + self.external.clear(); + self.transient.clear(); + } + pub fn reset(&mut self, p: &PhysicalResourceSet) { + self.clear(); + self.external.resize(p.externals.len(), T::default()); + self.transient.resize(p.externals.len(), T::default()); + } + pub fn get(&self, p: &PhysicalResourceSet, i: ResourceIndex) -> Option<&T> { + match p.assignments.get(i as usize)? { + ExternalOrTransient::None => None, + ExternalOrTransient::External(i) => self.external.get(*i as usize), + ExternalOrTransient::Transient(i) => self.transient.get(*i as usize), + } + } + pub fn get_mut(&mut self, p: &PhysicalResourceSet, i: ResourceIndex) -> Option<&mut T> { + match p.assignments.get(i as usize)? { + ExternalOrTransient::None => None, + ExternalOrTransient::External(i) => self.external.get_mut(*i as usize), + ExternalOrTransient::Transient(i) => self.transient.get_mut(*i as usize), + } + } +} + +pub struct PhysicalResourceAccessTracker { + textures: PhysicalResourceMap, + buffers: PhysicalResourceMap, + total: Access, +} + +impl Default for PhysicalResourceAccessTracker { + fn default() -> Self { + Self::new() + } +} + +impl PhysicalResourceAccessTracker { + pub const fn new() -> Self { + Self { + textures: PhysicalResourceMap::new(), + buffers: PhysicalResourceMap::new(), + total: Access::empty(), + } + } + + pub fn reset(&mut self, r: &PhysicalResources) { + self.textures.reset(&r.textures); + self.buffers.reset(&r.buffers); + self.total = Access::empty(); + } + + pub(crate) fn get_current_texture_access( + &self, + p: &PhysicalResources, + resource_index: ResourceIndex, + ) -> Access { + *self + .textures + .get(&p.textures, resource_index) + .expect("no valid physical buffer resource") + } + pub(crate) fn get_current_buffer_access( + &self, + p: &PhysicalResources, + resource_index: ResourceIndex, + ) -> Access { + *self + .buffers + .get(&p.buffers, resource_index) + .expect("no valid physical buffer resource") + } + + pub(crate) fn update_texture_access( + &mut self, + p: &PhysicalResources, + resource_index: ResourceIndex, + new_access: Access, + ) -> Access { + let dest = self + .textures + .get_mut(&p.textures, resource_index) + .expect("no valid physical texture resource"); + self.total |= new_access; + std::mem::replace(dest, new_access) + } + pub(crate) fn update_buffer_access( + &mut self, + p: &PhysicalResources, + resource_index: ResourceIndex, + new_access: Access, + ) -> Access { + let dest = self + .buffers + .get_mut(&p.buffers, resource_index) + .expect("no valid physical buffer resource"); + self.total |= new_access; + std::mem::replace(dest, new_access) + } +} + +impl> GraphImport for &T { + fn import(&self) -> R::ExternHandle { + T::import(self) + } +} + +impl> GraphExport for &T { + fn export(&self) -> R::ExternHandle { + T::export(self) + } +} + +impl> GraphImport for &mut T { + fn import(&self) -> R::ExternHandle { + T::import(self) + } +} + +impl> GraphExport for &mut T { + fn export(&self) -> R::ExternHandle { + T::export(self) + } +} + +impl GraphImport for Handle { + fn import(&self) -> RenderTarget { + RenderTarget::Image(*self) + } +} + +impl GraphExport for Handle { + fn export(&self) -> RenderTarget { + RenderTarget::Image(*self) + } +} + +impl GraphExport for WindowId { + fn export(&self) -> RenderTarget { + RenderTarget::Window(*self) + } +} + +impl GraphExport for RenderTarget { + fn export(&self) -> Self { + *self + } +} + +impl GraphImport for Handle { + fn import(&self) -> Self { + *self + } +} + +impl GraphExport for Handle { + fn export(&self) -> Self { + *self + } +} + +impl ResolveExtern for B +where + B: PhysicalResourceResolver, +{ + fn resolve_extern(&mut self, handle: &RenderTarget) -> Option> { + self.resolve_render_target(handle) + } +} + +impl ResolveExtern for B +where + B: PhysicalResourceResolver, +{ + fn resolve_extern(&mut self, handle: &Handle) -> Option> { + self.resolve_buffer(handle) + } +} + +impl CreateTransient for B +where + B: PhysicalResourceResolver, +{ + fn create_transient( + &mut self, + format: TextureFormat, + size: TextureDimensions, + access: Access, + ) -> Option { + self.create_transient_texture(format, size, access) + } +} +impl CreateTransient for B +where + B: PhysicalResourceResolver, +{ + fn create_transient(&mut self, _format: (), size: usize, access: Access) -> Option { + self.create_transient_buffer(size, access) + } +} diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs new file mode 100644 index 0000000..2c28bcf --- /dev/null +++ b/crates/render/src/lib.rs @@ -0,0 +1,115 @@ +#![warn( + // missing_docs, + // rustdoc::missing_doc_code_examples, + future_incompatible, + rust_2018_idioms, + unused, + trivial_casts, + trivial_numeric_casts, + unused_lifetimes, + unused_qualifications, + unused_crate_dependencies, + clippy::cargo, + clippy::multiple_crate_versions, + clippy::empty_line_after_outer_attr, + clippy::fallible_impl_from, + clippy::redundant_pub_crate, + clippy::use_self, + clippy::suspicious_operation_groupings, + clippy::useless_let_if_seq, + // clippy::missing_errors_doc, + // clippy::missing_panics_doc, + clippy::wildcard_imports +)] +#![doc(html_logo_url = "https://raw.githubusercontent.com/HellButcher/pulz/master/docs/logo.png")] +#![doc(html_no_source)] +#![doc = include_str!("../README.md")] + +use camera::{Camera, Projection, RenderTarget}; +use graph::{RenderGraph, RenderGraphBuilder}; +use pulz_assets::Assets; +use pulz_ecs::{ + define_label_enum, + label::{CoreSystemPhase, SystemPhase}, + prelude::*, +}; + +pub mod backend; +pub mod buffer; +pub mod camera; +pub mod draw; +pub mod graph; +pub mod mesh; +pub mod pipeline; +pub mod shader; +pub mod surface; +pub mod texture; +pub mod utils; + +pub use pulz_window as window; + +pub mod color { + pub use palette::{Hsla, LinSrgba, Srgba}; +} + +#[doc(hidden)] +pub use ::encase as __encase; +pub use pulz_transform::math; +use surface::WindowSurfaces; + +define_label_enum! { + pub enum RenderSystemPhase: SystemPhase { + UpdateCamera, + Sorting, + BuildGraph, + UpdateGraph, + Render, + } +} + +pub struct RenderModule; + +impl Module for RenderModule { + fn install_once(&self, res: &mut Resources) { + Assets::::install_into(res); + + res.init::(); + res.init::(); + res.init::(); + // TODO: + //res.init::(); + //render_graph::graph::RenderGraph::install_into(res, schedule); + + let mut world = res.world_mut(); + world.init::(); + world.init::(); + world.init::(); + } + + fn install_systems(schedule: &mut Schedule) { + schedule.add_phase_chain([ + RenderSystemPhase::UpdateCamera, + RenderSystemPhase::Sorting, + RenderSystemPhase::BuildGraph, + RenderSystemPhase::UpdateGraph, + RenderSystemPhase::Render, + ]); + // update projection and camera + schedule + .add_system(camera::update_projections_from_render_targets) + .after(CoreSystemPhase::Update) + .before(RenderSystemPhase::UpdateCamera); + schedule + .add_system(camera::update_cameras_from_projections) + .into_phase(RenderSystemPhase::UpdateCamera); + // SORTING after update UPDATE + schedule.add_phase_dependency(CoreSystemPhase::Update, RenderSystemPhase::Sorting); + schedule + .add_system(RenderGraphBuilder::reset) + .before(RenderSystemPhase::BuildGraph); + schedule + .add_system(RenderGraph::build_from_builder) + .after(RenderSystemPhase::BuildGraph) + .before(RenderSystemPhase::UpdateGraph); + } +} diff --git a/crates/render/src/mesh/mod.rs b/crates/render/src/mesh/mod.rs new file mode 100644 index 0000000..01dede6 --- /dev/null +++ b/crates/render/src/mesh/mod.rs @@ -0,0 +1,137 @@ +use crate::pipeline::{IndexFormat, PrimitiveTopology, VertexFormat}; + +pub struct Mesh { + primitive_topology: PrimitiveTopology, + indices: Option, +} + +impl Mesh { + pub const ATTRIBUTE_POSITION: MeshVertexAttribute = + MeshVertexAttribute("Vertex_Position", 0, VertexFormat::Float32x3); + pub const ATTRIBUTE_NORMAL: MeshVertexAttribute = + MeshVertexAttribute("Vertex_Normal", 1, VertexFormat::Float32x3); + pub const ATTRIBUTE_UV_0: MeshVertexAttribute = + MeshVertexAttribute("Vertex_Uv0", 2, VertexFormat::Float32x2); + pub const ATTRIBUTE_UV_1: MeshVertexAttribute = + MeshVertexAttribute("Vertex_Uv1", 3, VertexFormat::Float32x2); + pub const ATTRIBUTE_TANGENT: MeshVertexAttribute = + MeshVertexAttribute("Vertex_Tangent", 4, VertexFormat::Float32x4); + pub const ATTRIBUTE_COLOR_0: MeshVertexAttribute = + MeshVertexAttribute("Vertex_Color0", 5, VertexFormat::Float32x4); + pub const ATTRIBUTE_COLOR_1: MeshVertexAttribute = + MeshVertexAttribute("Vertex_Color1", 6, VertexFormat::Float32x4); + pub const ATTRIBUTE_JOINT_WEIGHT: MeshVertexAttribute = + MeshVertexAttribute("Vertex_JointWeight", 7, VertexFormat::Float32x4); + pub const ATTRIBUTE_JOINT_INDEX: MeshVertexAttribute = + MeshVertexAttribute("Vertex_JointIndex", 8, VertexFormat::Uint16x4); + + pub const fn new(primitive_topology: PrimitiveTopology) -> Self { + Self { + primitive_topology, + indices: None, + } + } + + #[inline] + pub fn primitive_topology(&self) -> PrimitiveTopology { + self.primitive_topology + } + + #[inline] + pub fn indices(&self) -> Option<&Indices> { + self.indices.as_ref() + } +} + +#[derive(Debug, Clone)] +pub enum Indices { + U16(Vec), + U32(Vec), +} + +impl Indices { + pub fn format(&self) -> IndexFormat { + match self { + Self::U16(_) => IndexFormat::Uint16, + Self::U32(_) => IndexFormat::Uint32, + } + } +} + +pub struct MeshVertexAttribute { + pub name: &'static str, + pub id: u32, + pub format: VertexFormat, +} + +#[allow(non_snake_case)] +#[inline] +pub const fn MeshVertexAttribute( + name: &'static str, + id: u32, + format: VertexFormat, +) -> MeshVertexAttribute { + MeshVertexAttribute { name, id, format } +} + +impl MeshVertexAttribute { + #[inline] + pub const fn new(name: &'static str, id: u32, format: VertexFormat) -> Self { + Self { name, id, format } + } + + #[inline] + pub const fn at(&self, shader_location: u32) -> MeshVertexAttributeLocation { + MeshVertexAttributeLocation { + attribute_id: self.id, + shader_location, + } + } +} + +pub struct MeshVertexAttributeLocation { + pub attribute_id: u32, + pub shader_location: u32, +} + +impl VertexFormat { + pub fn size(self) -> usize { + match self { + Self::Uint8x2 => 2, + Self::Uint8x4 => 4, + Self::Sint8x2 => 2, + Self::Sint8x4 => 4, + Self::Unorm8x2 => 2, + Self::Unorm8x4 => 4, + Self::Snorm8x2 => 2, + Self::Snorm8x4 => 4, + Self::Uint16x2 => 4, + Self::Uint16x4 => 8, + Self::Sint16x2 => 4, + Self::Sint16x4 => 8, + Self::Unorm16x2 => 4, + Self::Unorm16x4 => 8, + Self::Snorm16x2 => 4, + Self::Snorm16x4 => 8, + Self::Float16 => 2, + Self::Float16x2 => 4, + Self::Float16x4 => 8, + Self::Float32 => 4, + Self::Float32x2 => 8, + Self::Float32x3 => 12, + Self::Float32x4 => 16, + Self::Float64 => 8, + Self::Float64x2 => 16, + Self::Float64x3 => 24, + Self::Float64x4 => 32, + Self::Uint32 => 4, + Self::Uint32x2 => 8, + Self::Uint32x3 => 12, + Self::Uint32x4 => 16, + Self::Sint32 => 4, + Self::Sint32x2 => 8, + Self::Sint32x3 => 12, + Self::Sint32x4 => 16, + } + } +} diff --git a/crates/render/src/pipeline/binding.rs b/crates/render/src/pipeline/binding.rs new file mode 100644 index 0000000..fe29397 --- /dev/null +++ b/crates/render/src/pipeline/binding.rs @@ -0,0 +1,25 @@ +use std::borrow::Cow; + +pub use pulz_render_macros::AsBindingLayout; +use serde::{Deserialize, Serialize}; + +crate::backend::define_gpu_resource!(BindGroupLayout, BindGroupLayoutDescriptor<'l>); + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct BindGroupLayoutDescriptor<'a> { + pub label: Option<&'a str>, + pub entries: Cow<'a, [BindGroupLayoutEntry]>, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct BindGroupLayoutEntry { + pub binding: u32, + // pub visibility: ShaderStages, + // pub ty: BindingType, + // TODO: + pub count: u32, +} + +pub trait AsBindingLayout { + // TODO (also macro) +} diff --git a/crates/render/src/pipeline/compute_pipeline.rs b/crates/render/src/pipeline/compute_pipeline.rs new file mode 100644 index 0000000..5947619 --- /dev/null +++ b/crates/render/src/pipeline/compute_pipeline.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + pipeline::{PipelineLayout, SpecializationInfo}, + shader::ShaderModule, +}; + +crate::backend::define_gpu_resource!(ComputePipeline, ComputePipelineDescriptor<'l>); + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ComputePipelineDescriptor<'a> { + pub label: Option<&'a str>, + #[serde(with = "crate::utils::serde_slots::option")] + pub layout: Option, + #[serde(with = "crate::utils::serde_slots")] + pub module: ShaderModule, + pub entry_point: &'a str, + pub specialization: SpecializationInfo<'a>, +} diff --git a/crates/render/src/pipeline/graphics_pass.rs b/crates/render/src/pipeline/graphics_pass.rs new file mode 100644 index 0000000..211f370 --- /dev/null +++ b/crates/render/src/pipeline/graphics_pass.rs @@ -0,0 +1,231 @@ +use pulz_transform::math::USize2; +use serde::{Deserialize, Serialize}; + +use crate::{ + graph::{ + PassDescription, RenderGraph, ResourceIndex, + access::Access, + pass::PipelineBindPoint, + resources::{PhysicalResourceAccessTracker, PhysicalResources}, + }, + texture::TextureFormat, +}; + +crate::backend::define_gpu_resource!(GraphicsPass, GraphicsPassDescriptor); + +#[derive( + Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize, +)] +pub enum LoadOp { + #[default] + Load, + Clear, + DontCare, +} + +#[derive( + Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize, +)] +pub enum StoreOp { + #[default] + Store, + DontCare, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] + +pub struct LoadStoreOps { + pub load_op: LoadOp, + pub store_op: StoreOp, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] +pub struct AttachmentDescriptor { + pub format: TextureFormat, + pub access: Access, + pub initial_access: Access, + pub final_access: Access, + pub samples: u8, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] +pub struct SubpassDescriptor { + input_attachments: Vec<(u16, Access)>, + color_attachments: Vec<(u16, Access)>, + depth_stencil_attachment: Option<(u16, Access)>, + //resolve_attachments: Vec, +} + +impl SubpassDescriptor { + #[inline] + pub fn input_attachments(&self) -> &[(u16, Access)] { + &self.input_attachments + } + #[inline] + pub fn color_attachments(&self) -> &[(u16, Access)] { + &self.color_attachments + } + #[inline] + pub fn depth_stencil_attachment(&self) -> Option<(u16, Access)> { + self.depth_stencil_attachment + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] +pub struct GraphicsPassDescriptor { + attachments: Vec, + load_store_ops: Vec, + subpasses: Vec, +} + +impl GraphicsPassDescriptor { + #[inline] + pub fn attachments(&self) -> &[AttachmentDescriptor] { + &self.attachments + } + #[inline] + pub fn load_store_ops(&self) -> &[LoadStoreOps] { + &self.load_store_ops + } + #[inline] + pub fn subpasses(&self) -> &[SubpassDescriptor] { + &self.subpasses + } +} + +pub struct ExtendedGraphicsPassDescriptor { + pub graphics_pass: GraphicsPassDescriptor, + pub resource_indices: Vec, + pub size: USize2, +} + +impl ExtendedGraphicsPassDescriptor { + pub fn from_graph( + graph: &RenderGraph, + physical_resources: &PhysicalResources, + resource_access: &mut PhysicalResourceAccessTracker, + pass: &PassDescription, + ) -> Option { + if pass.bind_point() != PipelineBindPoint::Graphics { + return None; + } + let mut attachment_indices = Vec::with_capacity(pass.textures().len()); + for (i, tex) in pass.textures().deps().iter().enumerate() { + if tex.access().is_graphics_attachment() { + attachment_indices.push(i as ResourceIndex); + } + } + + let mut attachments = Vec::with_capacity(attachment_indices.len()); + let mut load_store_ops = Vec::with_capacity(attachment_indices.len()); + let mut size = USize2::ZERO; + for i in attachment_indices.iter().copied() { + let a = &pass.textures()[i as usize]; + let resource_index = a.resource_index(); + let physical_resource = physical_resources + .get_texture(resource_index) + .expect("unassigned resource"); + let dim = physical_resource.size.subimage_extents(); + if size == USize2::ZERO { + size = dim; + } else if size != dim { + // TODO: error handling + panic!("all framebuffer textures need to have the same dimensions"); + } + + let mut load_store = LoadStoreOps { + // TODO: provide a way to use DONT_CARE or CLEAR + load_op: LoadOp::Clear, + // TODO: is resource used in later pass? then STORE, else DONT_CARE + store_op: StoreOp::Store, + }; + if a.is_read() { + load_store.load_op = LoadOp::Load; + } + + let current_access = + resource_access.get_current_texture_access(physical_resources, resource_index); + + attachments.push(AttachmentDescriptor { + format: physical_resource.format, + samples: 1, // TODO: multi-sample + access: a.access(), + initial_access: current_access, + final_access: current_access, + }); + + load_store_ops.push(load_store); + } + + // map attachment_indices into their actual resource indices + for i in &mut attachment_indices { + // pass.textures() is sorted by resource-index! + *i = pass.textures()[*i as usize].resource_index(); + } + + let mut map_attachment_index_and_update_usage = + |resource_index: u16, mut current_access: Access| { + let a = attachment_indices + .binary_search(&resource_index) + .expect("unvalid resource index") as u16; + if current_access.is_empty() { + current_access = attachments[a as usize].final_access; + } else { + attachments[a as usize].final_access = current_access; + } + (a, current_access) + }; + + let mut subpasses = Vec::with_capacity(pass.sub_pass_range().len()); + for sp in pass.sub_pass_range() { + let sp = graph.get_subpass(sp).unwrap(); + let input_attachments = sp + .input_attachments() + .iter() + .copied() + .map(|(r, u)| map_attachment_index_and_update_usage(r, u)) + .collect(); + let color_attachments = sp + .color_attachments() + .iter() + .copied() + .map(|(r, u)| map_attachment_index_and_update_usage(r, u)) + .collect(); + let depth_stencil_attachment = sp + .depth_stencil_attachment() + .map(|(r, u)| map_attachment_index_and_update_usage(r, u)); + subpasses.push(SubpassDescriptor { + input_attachments, + color_attachments, + depth_stencil_attachment, + }) + // update + } + + // TODO: if this pass is the last pass accessing this resource (and resource not extern), then STOREOP = DON'T CARE + // TODO: if this pass is the last pass accessing this resource, and usage us PRESENT, then finalLayout=PRESENT + + for (a, r) in attachment_indices.iter().copied().enumerate() { + let attachment = &mut attachments[a]; + let physical_resource = physical_resources.get_texture(r).unwrap(); + // TODO: only present, if this is the last pass accessing this resource! + if physical_resource.access.intersects(Access::PRESENT) { + attachment.final_access = Access::PRESENT; + } + + resource_access.update_texture_access(physical_resources, r, attachment.final_access); + } + + let graphics_pass = GraphicsPassDescriptor { + attachments, + load_store_ops, + subpasses, + }; + + Some(Self { + graphics_pass, + resource_indices: attachment_indices, + size, + }) + } +} diff --git a/crates/render/src/pipeline/graphics_pipeline.rs b/crates/render/src/pipeline/graphics_pipeline.rs new file mode 100644 index 0000000..9bb9e3e --- /dev/null +++ b/crates/render/src/pipeline/graphics_pipeline.rs @@ -0,0 +1,470 @@ +use std::{ + borrow::Cow, + hash::{Hash, Hasher}, +}; + +use bitflags::bitflags; +use serde::{Deserialize, Serialize}; + +use crate::{ + pipeline::{GraphicsPass, PipelineLayout, SpecializationInfo}, + shader::ShaderModule, + texture::TextureFormat, +}; + +crate::backend::define_gpu_resource!(GraphicsPipeline, GraphicsPipelineDescriptor<'l>); + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct GraphicsPipelineDescriptor<'a> { + pub label: Option<&'a str>, + #[serde(with = "crate::utils::serde_slots::option")] + pub layout: Option, + pub vertex: VertexState<'a>, + pub primitive: PrimitiveState, + pub depth_stencil: Option, + pub fragment: Option>, + pub samples: u32, + pub specialization: SpecializationInfo<'a>, + #[serde(with = "crate::utils::serde_slots")] + pub graphics_pass: GraphicsPass, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct VertexState<'a> { + #[serde(with = "crate::utils::serde_slots")] + pub module: ShaderModule, + pub entry_point: &'a str, + pub buffers: Cow<'a, [VertexBufferLayout<'a>]>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct VertexBufferLayout<'a> { + pub array_stride: usize, + pub attributes: Cow<'a, [VertexAttribute]>, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct VertexAttribute { + pub format: VertexFormat, + pub offset: usize, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub struct PrimitiveState { + pub topology: PrimitiveTopology, + pub polygon_mode: PolygonMode, + pub front_face: FrontFace, + pub cull_mode: Option, + pub line_width: f32, +} + +impl PrimitiveState { + pub const DEFAULT: Self = Self { + topology: PrimitiveTopology::TriangleList, + polygon_mode: PolygonMode::Fill, + front_face: FrontFace::CounterClockwise, + cull_mode: None, + line_width: 0.0, + }; +} + +impl Default for PrimitiveState { + fn default() -> Self { + Self::DEFAULT + } +} + +impl Eq for PrimitiveState {} + +impl PartialEq for PrimitiveState { + fn eq(&self, other: &Self) -> bool { + self.topology.eq(&other.topology) + && self.polygon_mode.eq(&other.polygon_mode) + && self.front_face.eq(&other.front_face) + && self.cull_mode.eq(&other.cull_mode) + && self.line_width.eq(&other.line_width) + } +} + +impl Hash for PrimitiveState { + fn hash(&self, state: &mut H) { + self.topology.hash(state); + self.polygon_mode.hash(state); + self.front_face.hash(state); + self.cull_mode.hash(state); + state.write_u32(self.line_width.to_bits()); + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct DepthStencilState { + pub format: TextureFormat, + pub depth: DepthState, + pub stencil: StencilState, +} + +impl DepthStencilState { + pub const DEFAULT: Self = Self { + format: TextureFormat::Depth24PlusStencil8, + depth: DepthState::DEFAULT, + stencil: StencilState::DEFAULT, + }; + + pub fn is_depth_enabled(&self) -> bool { + self.depth.compare != CompareFunction::Always || self.depth.write_enabled + } + /// Returns true if the state doesn't mutate either depth or stencil of the target. + pub fn is_read_only(&self) -> bool { + !self.depth.write_enabled && self.stencil.is_read_only() + } +} + +impl Default for DepthStencilState { + fn default() -> Self { + Self::DEFAULT + } +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub struct DepthState { + pub write_enabled: bool, + pub compare: CompareFunction, + + pub bias: i32, + pub bias_slope_scale: f32, + pub bias_clamp: f32, +} + +impl DepthState { + pub const DEFAULT: Self = Self { + write_enabled: false, + compare: CompareFunction::Always, + + bias: 0, + bias_slope_scale: 0.0, + bias_clamp: 0.0, + }; +} + +impl Eq for DepthState {} + +impl PartialEq for DepthState { + fn eq(&self, other: &Self) -> bool { + self.write_enabled.eq(&other.write_enabled) + && self.compare.eq(&other.compare) + && self.bias.eq(&other.bias) + && self.bias_slope_scale.eq(&other.bias_slope_scale) + && self.bias_clamp.eq(&other.bias_clamp) + } +} + +impl Hash for DepthState { + fn hash(&self, state: &mut H) { + self.write_enabled.hash(state); + self.compare.hash(state); + state.write_i32(self.bias); + state.write_u32(self.bias_slope_scale.to_bits()); + state.write_u32(self.bias_clamp.to_bits()); + } +} + +impl Default for DepthState { + fn default() -> Self { + Self::DEFAULT + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct StencilState { + pub front: StencilFaceState, + pub back: StencilFaceState, + + pub read_mask: u32, + pub write_mask: u32, +} + +impl StencilState { + pub const DEFAULT: Self = Self { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + + read_mask: u32::MAX, + write_mask: u32::MAX, + }; + + pub fn is_enabled(&self) -> bool { + (self.front != StencilFaceState::IGNORE || self.back != StencilFaceState::IGNORE) + && (self.read_mask != 0 || self.write_mask != 0) + } + /// Returns true if the state doesn't mutate the target values. + pub fn is_read_only(&self) -> bool { + self.write_mask == 0 + } +} + +impl Default for StencilState { + fn default() -> Self { + Self::DEFAULT + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct StencilFaceState { + pub compare: CompareFunction, + pub fail_op: StencilOperation, + pub depth_fail_op: StencilOperation, + pub pass_op: StencilOperation, +} + +impl StencilFaceState { + pub const IGNORE: Self = Self { + compare: CompareFunction::Always, + fail_op: StencilOperation::Keep, + depth_fail_op: StencilOperation::Keep, + pass_op: StencilOperation::Keep, + }; +} + +impl Default for StencilFaceState { + fn default() -> Self { + Self::IGNORE + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct FragmentState<'a> { + #[serde(with = "crate::utils::serde_slots")] + pub module: ShaderModule, + pub entry_point: &'a str, + pub targets: Cow<'a, [ColorTargetState]>, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ColorTargetState { + pub format: TextureFormat, + pub blend: Option, + pub write_mask: ColorWrite, +} + +impl ColorTargetState { + pub const DEFAULT: Self = Self { + format: TextureFormat::DEFAULT, + blend: None, + write_mask: ColorWrite::ALL, + }; +} + +impl Default for ColorTargetState { + fn default() -> Self { + Self::DEFAULT + } +} + +impl From for ColorTargetState { + fn from(format: TextureFormat) -> Self { + Self { + format, + blend: None, + write_mask: ColorWrite::ALL, + } + } +} + +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct BlendState { + pub color: BlendComponent, + pub alpha: BlendComponent, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct BlendComponent { + pub operation: BlendOperation, + pub src_factor: BlendFactor, + pub dst_factor: BlendFactor, +} + +impl BlendComponent { + pub const DEFAULT: Self = Self { + operation: BlendOperation::Add, + src_factor: BlendFactor::One, + dst_factor: BlendFactor::Zero, + }; +} + +impl Default for BlendComponent { + fn default() -> Self { + Self::DEFAULT + } +} + +#[derive( + Copy, Clone, Default, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, +)] +pub enum PolygonMode { + #[default] + Fill, + Line, + Point, +} + +#[derive( + Copy, Clone, Default, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, +)] +pub enum PrimitiveTopology { + PointList, + LineList, + LineStrip, + #[default] + TriangleList, + TriangleStrip, +} + +#[derive( + Copy, Clone, Default, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, +)] +pub enum VertexStepMode { + #[default] + Vertex, + Instance, +} + +#[derive( + Copy, Clone, Default, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, +)] +#[non_exhaustive] +pub enum VertexFormat { + Uint8x2, + Uint8x4, + Sint8x2, + Sint8x4, + Unorm8x2, + Unorm8x4, + Snorm8x2, + Snorm8x4, + Uint16x2, + Uint16x4, + Sint16x2, + Sint16x4, + Unorm16x2, + Unorm16x4, + Snorm16x2, + Snorm16x4, + Float16, + Float16x2, + Float16x4, + Float32, + Float32x2, + Float32x3, + #[default] + Float32x4, + Float64, + Float64x2, + Float64x3, + Float64x4, + Uint32, + Uint32x2, + Uint32x3, + Uint32x4, + Sint32, + Sint32x2, + Sint32x3, + Sint32x4, +} + +#[derive( + Copy, Clone, Default, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, +)] +pub enum IndexFormat { + Uint16, + #[default] + Uint32, +} + +#[derive( + Copy, Clone, Default, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, +)] +pub enum FrontFace { + #[default] + CounterClockwise, + Clockwise, +} + +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +pub enum Face { + Front, + Back, +} + +#[derive( + Copy, Clone, Default, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, +)] +pub enum CompareFunction { + Never, + Less, + Equal, + LessEqual, + Greater, + NotEqual, + GreaterEqual, + #[default] + Always, +} + +#[derive( + Copy, Clone, Default, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, +)] +pub enum StencilOperation { + #[default] + Keep, + Zero, + Replace, + Invert, + IncrementClamp, + DecrementClamp, + IncrementWrap, + DecrementWrap, +} + +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +pub enum BlendOperation { + Add, + Subtract, + ReverseSubtract, + Min, + Max, +} + +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +pub enum BlendFactor { + Zero, + One, + Src, + OneMinusSrc, + SrcAlpha, + OneMinusSrcAlpha, + Dst, + OneMinusDst, + DstAlpha, + OneMinusDstAlpha, + SrcAlphaSaturated, + Constant, + OneMinusConstant, +} + +bitflags! { + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] + pub struct ColorWrite: u32 { + const RED = 1; + const GREEN = 2; + const BLUE = 4; + const ALPHA = 8; + + const ALL = 0xF; + } +} + +impl Default for ColorWrite { + fn default() -> Self { + Self::ALL + } +} diff --git a/crates/render/src/pipeline/mod.rs b/crates/render/src/pipeline/mod.rs new file mode 100644 index 0000000..7d2d528 --- /dev/null +++ b/crates/render/src/pipeline/mod.rs @@ -0,0 +1,12 @@ +mod binding; +mod compute_pipeline; +mod graphics_pass; +mod graphics_pipeline; +mod pipeline_layout; +mod ray_tracing_pipeline; +mod specialization; + +pub use self::{ + binding::*, compute_pipeline::*, graphics_pass::*, graphics_pipeline::*, pipeline_layout::*, + ray_tracing_pipeline::*, specialization::*, +}; diff --git a/crates/render/src/pipeline/pipeline_layout.rs b/crates/render/src/pipeline/pipeline_layout.rs new file mode 100644 index 0000000..06dd036 --- /dev/null +++ b/crates/render/src/pipeline/pipeline_layout.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; + +use super::BindGroupLayout; + +crate::backend::define_gpu_resource!(PipelineLayout, PipelineLayoutDescriptor<'l>); + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct PipelineLayoutDescriptor<'a> { + pub label: Option<&'a str>, + #[serde(with = "crate::utils::serde_slots::cow_vec")] + pub bind_group_layouts: std::borrow::Cow<'a, [BindGroupLayout]>, +} diff --git a/crates/render/src/pipeline/ray_tracing_pipeline.rs b/crates/render/src/pipeline/ray_tracing_pipeline.rs new file mode 100644 index 0000000..4ceca6a --- /dev/null +++ b/crates/render/src/pipeline/ray_tracing_pipeline.rs @@ -0,0 +1,57 @@ +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; + +use crate::{ + pipeline::{PipelineLayout, SpecializationInfo}, + shader::ShaderModule, +}; + +crate::backend::define_gpu_resource!(RayTracingPipeline, RayTracingPipelineDescriptor<'l>); + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct RayTracingPipelineDescriptor<'a> { + pub label: Option<&'a str>, + #[serde(with = "crate::utils::serde_slots::option")] + pub layout: Option, + pub modules: Cow<'a, [RayTracingShaderModule<'a>]>, + pub groups: Cow<'a, [RayTracingShaderGroup]>, + pub max_recursion_depth: u32, + pub specialization: SpecializationInfo<'a>, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct RayTracingShaderGroup { + pub group_type: RayTracingGroupType, + pub general_shader: u32, + pub closest_hit_shader: u32, + pub any_hit_shader: u32, + pub intersection_shader: u32, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct RayTracingShaderModule<'a> { + pub stage: RayTracingStage, + #[serde(with = "crate::utils::serde_slots")] + pub module: ShaderModule, + pub entry_point: &'a str, +} + +#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum RayTracingStage { + #[default] + Raygen, + AnyHit, + ClosestHit, + Miss, + Intersection, + Callable, +} + +#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum RayTracingGroupType { + #[default] + General, + TrianglesHitGroup, + ProceduralHitGroup, +} diff --git a/crates/render/src/pipeline/specialization.rs b/crates/render/src/pipeline/specialization.rs new file mode 100644 index 0000000..b621a9a --- /dev/null +++ b/crates/render/src/pipeline/specialization.rs @@ -0,0 +1,57 @@ +use std::hash::{Hash, Hasher}; + +use serde::{Deserialize, Serialize}; + +pub type SpecializationInfo<'a> = Vec>; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct SpecializationMapEntry<'a> { + pub constant_id: u32, + pub name: &'a str, + pub value: PipelineConstantValue, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub enum PipelineConstantValue { + Bool(bool), + Float(f32), + Sint(i32), + Uint(u32), +} + +impl Eq for PipelineConstantValue {} + +impl PartialEq for PipelineConstantValue { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Bool(v1), Self::Bool(v2)) => v1.eq(v2), + (Self::Float(v1), Self::Float(v2)) => v1.eq(v2), + (Self::Sint(v1), Self::Sint(v2)) => v1.eq(v2), + (Self::Uint(v1), Self::Uint(v2)) => v1.eq(v2), + _ => false, + } + } +} + +impl Hash for PipelineConstantValue { + fn hash(&self, state: &mut H) { + match self { + Self::Bool(v) => { + state.write_u8(2); + v.hash(state); + } + Self::Float(v) => { + state.write_u8(3); + state.write_u32(v.to_bits()); + } + Self::Sint(v) => { + state.write_u8(5); + state.write_i32(*v); + } + Self::Uint(v) => { + state.write_u8(7); + state.write_u32(*v); + } + } + } +} diff --git a/crates/render/src/shader/mod.rs b/crates/render/src/shader/mod.rs new file mode 100644 index 0000000..7cf38e4 --- /dev/null +++ b/crates/render/src/shader/mod.rs @@ -0,0 +1,111 @@ +use std::borrow::Cow; + +mod preprocessor; + +use serde::{Deserialize, Serialize}; + +mod encase { + pub use ::encase::{ShaderSize, ShaderType, private}; +} +pub use ::pulz_render_macros::ShaderType; + +#[doc(hidden)] +pub use self::encase::*; + +crate::backend::define_gpu_resource!(ShaderModule, ShaderModuleDescriptor<'l>); + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ShaderModuleDescriptor<'a> { + pub label: Option<&'a str>, + pub source: ShaderSource<'a>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[non_exhaustive] +pub enum ShaderSource<'a> { + Wgsl(Cow<'a, str>), + Glsl(Cow<'a, str>), + SpirV(Cow<'a, [u32]>), +} + +// impl<'a> ShaderSource<'a> { +// pub fn from_spirv_bytes(data: &'a [u8]) -> Self { +// Self::SpirV(spirv_raw_from_bytes(data)) +// } +// } + +// fn spirv_raw_from_bytes(data: &[u8]) -> Cow<'_, [u32]> { +// const MAGIC_NUMBER: u32 = 0x0723_0203; + +// assert!(data.len() % 4 == 0, "data size is not a multiple of 4"); + +// let (pre, words, post) = unsafe { data.align_to::() }; +// let words = if pre.is_empty() { +// // is already aligned +// debug_assert!(post.is_empty()); +// Cow::Borrowed(words) +// } else { +// // copy into aligned Vec +// let mut words = vec![0u32; data.len() / 4]; +// unsafe { +// std::ptr::copy_nonoverlapping(data.as_ptr(), words.as_mut_ptr() as *mut u8, data.len()); +// } +// Cow::from(words) +// }; + +// assert_eq!(words[0], MAGIC_NUMBER, "wrong magic word {:x}. Not a SPIRV file.", words[0]); + +// words +// } + +// #[macro_export] +// macro_rules! compile_shader { +// ($filename:literal) => { +// $crate::shader::ShaderModuleDescriptor { +// label: Some($filename), +// #[cfg(not(target_arch = "wasm32"))] +// source: $crate::shader::ShaderSource::SpirV(::std::borrow::Cow::Borrowed($crate::shader::__compile_shader_int!( +// target_format = "SpirV", +// source = $filename, +// ))), +// #[cfg(target_arch = "wasm32")] +// source: $crate::shader::ShaderSource::Wgsl(::std::borrow::Cow::Borrowed($crate::shader::__compile_shader_int!( +// target_format = "Wgsl", +// source = $filename, +// ))), +// } +// }; +// } + +/// Macro to load a WGSL shader module statically. +#[macro_export] +macro_rules! include_wgsl { + ($file:literal) => { + &$crate::shader::ShaderModuleDescriptor { + label: Some($file), + source: $crate::shader::ShaderSource::Wgsl(::std::borrow::Cow::Borrowed(include_str!( + $file + ))), + } + }; +} + +// #[macro_export] +// macro_rules! include_glsl { +// ($file:literal) => { +// &$crate::shader::ShaderModuleDescriptor { +// label: Some($file), +// source: $crate::shader::ShaderSource::Glsl(::std::borrow::Cow::Borrowed(include_str!($file))), +// } +// }; +// } + +// #[macro_export] +// macro_rules! include_spirv { +// ($file:literal) => { +// &$crate::shader::ShaderModuleDescriptor { +// label: Some($file), +// source: $crate::shader::ShaderSource::from_spirv_bytes(include_bytes!($file)), +// } +// }; +// } diff --git a/crates/render/src/shader/preprocessor.rs b/crates/render/src/shader/preprocessor.rs new file mode 100644 index 0000000..0e051c6 --- /dev/null +++ b/crates/render/src/shader/preprocessor.rs @@ -0,0 +1,279 @@ +use std::{borrow::Cow, collections::VecDeque}; + +use fnv::FnvHashMap; + +pub struct Preprocessor<'a> { + input: &'a str, + current_start: usize, + current: usize, + line: usize, + ifdef_state: VecDeque, + next_buffered: Option<&'a str>, + defines: FnvHashMap<&'a str, &'a str>, +} + +#[derive(PartialEq, Eq, Copy, Clone)] +enum PreprocessorState { + Neutral, + String, + Char, + Ident, + DirectiveStart, + DirectiveName(usize), + DirectiveArg(usize, usize), + Other, +} + +enum Token<'a> { + Ident(&'a str), + Directive(&'a str, &'a str), + Other, +} + +impl<'a> Preprocessor<'a> { + pub fn new(input: &'a str) -> Self { + Self { + input, + current_start: 0, + current: 0, + line: 0, + next_buffered: None, + ifdef_state: VecDeque::new(), + defines: FnvHashMap::default(), + } + } + + pub fn define(&mut self, key: &'a str, value: &'a str) -> &mut Self { + self.defines.insert(key, value); + self + } + + pub fn process(mut self) -> Cow<'a, str> { + let Some(first) = self.next() else { + return Cow::Borrowed(""); + }; + let Some(more) = self.next() else { + return Cow::Borrowed(first); + }; + let mut owned = first.to_string(); + owned.push_str(more); + for more in self { + owned.push_str(more); + } + Cow::Owned(owned) + } + + fn next_token(&mut self) -> Option> { + use self::PreprocessorState::*; + self.current_start = self.current; + let mut state = Neutral; + let bytes = self.input.as_bytes(); + while let Some(&c) = bytes.get(self.current) { + match state { + Neutral => { + self.current += 1; + if c == b'#' { + state = DirectiveStart; + } else if c == b'"' { + state = String; + } else if c == b'\'' { + state = Char; + } else if matches!(c, b'A'..=b'Z' | b'a'..=b'z' | b'_') { + state = Ident; + } else if c == b'\n' { + self.line += 1; + state = Other; + } else { + state = Other; + } + } + String | Char => { + self.current += 1; + if state == String && c == b'"' || state == Char && c == b'\'' { + state = Other; + } else if c == b'\\' { + // next character escaped + self.current += 1; + } + } + Ident => { + if matches!(c, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_') { + self.current += 1; + } else { + break; + } + } + DirectiveStart => { + if matches!(c, b' ' | b'\t' | b'\r' | b'\x0C') { + self.current += 1; + } else { + state = DirectiveName(self.current); + } + } + DirectiveName(name_start) => { + if matches!(c, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_') { + self.current += 1; + } else { + state = DirectiveArg(name_start, self.current); + } + } + DirectiveArg(_, _) => { + if c == b'\n' { + break; + } else { + self.current += 1; + } + } + Other => { + if c == b'\n' { + self.line += 1; + self.current += 1; + } else if matches!(c, b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'#' | b'"' | b'\'') { + break; + } else { + self.current += 1; + } + } + } + } + match state { + Neutral => None, + Ident => Some(Token::Ident(&self.input[self.current_start..self.current])), + DirectiveStart => Some(Token::Directive("", "")), + DirectiveName(name_start) => Some(Token::Directive( + self.input[name_start..self.current].trim_start(), + "", + )), + DirectiveArg(name_start, arg_start) => Some(Token::Directive( + self.input[name_start..arg_start].trim_start(), + self.input[arg_start..self.current].trim(), + )), + String | Char | Other => Some(Token::Other), + } + } + + fn evaluate_ifdef(&self, arg: &str) -> bool { + self.defines.contains_key(arg) + } + + fn push_ifdef_state(&mut self, eval: F) -> bool + where + F: FnOnce(&Self) -> bool, + { + let mut state = self.ifdef_state.back().copied().unwrap_or(true); + if state { + state = eval(self); + } + self.ifdef_state.push_back(state); + state + } + + fn pop_ifdef_state(&mut self) -> bool { + self.ifdef_state.pop_back().unwrap_or(true) + } + + fn ifdef_state(&self) -> bool { + self.ifdef_state.back().copied().unwrap_or(true) + } +} + +impl<'a> Iterator for Preprocessor<'a> { + type Item = &'a str; + fn next(&mut self) -> Option<&'a str> { + use self::Token::*; + if let Some(next_buffered) = self.next_buffered.take() { + return Some(next_buffered); + } + + let mut start = None; + let mut end = self.current; + while let Some(token) = self.next_token() { + match token { + Directive("ifdef", arg) => { + self.push_ifdef_state(|s| s.evaluate_ifdef(arg)); + if let Some(start) = start { + return Some(&self.input[start..end]); + } + } + Directive("ifndef", arg) => { + self.push_ifdef_state(|s| !s.evaluate_ifdef(arg)); + if let Some(start) = start { + return Some(&self.input[start..end]); + } + } + Directive("elifdef", arg) => { + let old_state = self.pop_ifdef_state(); + self.push_ifdef_state(|s| !old_state && s.evaluate_ifdef(arg)); + if let Some(start) = start { + return Some(&self.input[start..end]); + } + } + Directive("elifndef", arg) => { + let old_state = self.pop_ifdef_state(); + self.push_ifdef_state(|s| !old_state && !s.evaluate_ifdef(arg)); + if let Some(start) = start { + return Some(&self.input[start..end]); + } + } + Directive("else", _) => { + let old_state = self.pop_ifdef_state(); + self.push_ifdef_state(|_| !old_state); + if let Some(start) = start { + return Some(&self.input[start..end]); + } + } + Directive("endif", _) => { + self.pop_ifdef_state(); + if let Some(start) = start { + return Some(&self.input[start..end]); + } + } + Directive("define", arg) => { + let mut args = arg.splitn(2, |c: char| c.is_ascii_whitespace()); + if let Some(key) = args.next() { + let value = args.next().unwrap_or(""); + self.defines.insert(key, value); + } + if let Some(start) = start { + return Some(&self.input[start..end]); + } + } + Ident(ident) => { + if self.ifdef_state() { + if let Some(value) = self.defines.get(ident) { + if let Some(start) = start { + self.next_buffered = Some(value); + return Some(&self.input[start..end]); + } else { + return Some(value); + } + } else { + if start.is_none() { + start = Some(self.current_start); + } + end = self.current; + } + } else if let Some(start) = start { + return Some(&self.input[start..end]); + } + } + _ => { + // pass unknown directives, and other tokens + if self.ifdef_state() { + if start.is_none() { + start = Some(self.current_start); + } + end = self.current; + } else if let Some(start) = start { + return Some(&self.input[start..end]); + } + } + } + } + if let Some(start) = start { + Some(&self.input[start..end]) + } else { + None + } + } +} diff --git a/crates/render/src/surface.rs b/crates/render/src/surface.rs new file mode 100644 index 0000000..a8aeab6 --- /dev/null +++ b/crates/render/src/surface.rs @@ -0,0 +1,104 @@ +use fnv::FnvHashMap as HashMap; +use pulz_transform::math::{Size2, USize2}; +use pulz_window::{WindowId, WindowsMirror}; + +use crate::texture::{Texture, TextureFormat}; + +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct Surface { + pub texture: Texture, + pub format: TextureFormat, + pub physical_size: USize2, + pub scale_factor: f64, +} + +impl Surface { + #[inline] + pub fn to_logical_size(&self, physical_size: USize2) -> Size2 { + (physical_size.as_dvec2() / self.scale_factor).as_vec2() + } + + #[inline] + pub fn logical_size(&self) -> Size2 { + self.to_logical_size(self.physical_size) + } + + #[inline] + pub fn physical_size(&self) -> USize2 { + self.physical_size + } +} + +pub type Iter<'a, T = Surface> = slotmap::secondary::Iter<'a, WindowId, T>; +pub type IterMut<'a, T = Surface> = slotmap::secondary::IterMut<'a, WindowId, T>; + +pub struct WindowSurfaces { + surfaces: WindowsMirror, + by_texture: HashMap, +} + +impl WindowSurfaces { + pub fn new() -> Self { + Self { + surfaces: WindowsMirror::new(), + by_texture: HashMap::default(), + } + } + + #[inline] + pub fn len(&self) -> usize { + self.surfaces.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.surfaces.is_empty() + } + + #[inline] + pub fn get(&self, id: WindowId) -> Option<&Surface> { + self.surfaces.get(id) + } + + #[inline] + pub fn get_mut(&mut self, id: WindowId) -> Option<&mut Surface> { + self.surfaces.get_mut(id) + } + + #[inline] + pub fn remove(&mut self, id: WindowId) -> bool { + self.surfaces.remove(id).is_some() + } + + #[inline] + pub fn iter(&self) -> Iter<'_> { + self.surfaces.iter() + } + + #[inline] + pub fn iter_mut(&mut self) -> IterMut<'_> { + self.surfaces.iter_mut() + } +} + +impl Default for WindowSurfaces { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl std::ops::Index for WindowSurfaces { + type Output = Surface; + #[inline] + fn index(&self, id: WindowId) -> &Self::Output { + &self.surfaces[id] + } +} + +impl std::ops::IndexMut for WindowSurfaces { + #[inline] + fn index_mut(&mut self, id: WindowId) -> &mut Self::Output { + &mut self.surfaces[id] + } +} diff --git a/crates/render/src/texture/descriptor.rs b/crates/render/src/texture/descriptor.rs new file mode 100644 index 0000000..66825d6 --- /dev/null +++ b/crates/render/src/texture/descriptor.rs @@ -0,0 +1,357 @@ +use bitflags::bitflags; +use pulz_transform::math::{usize2, usize3}; +use serde::{Deserialize, Serialize}; + +use crate::{ + graph::access::Access, + math::{USize2, USize3}, +}; + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct TextureDescriptor { + pub dimensions: TextureDimensions, + pub mip_level_count: u32, + pub sample_count: u8, + pub format: TextureFormat, + pub aspects: TextureAspects, + pub usage: TextureUsage, +} + +impl TextureDescriptor { + pub const fn new() -> Self { + Self { + dimensions: TextureDimensions::D2(USize2::ONE), + mip_level_count: 1, + sample_count: 1, + format: TextureFormat::DEFAULT, + aspects: TextureAspects::DEFAULT, + usage: TextureUsage::empty(), + } + } + + pub fn aspects(&self) -> TextureAspects { + if self.aspects.is_empty() { + self.format.aspects() + } else { + self.aspects + } + } + + #[inline] + pub fn data_layout(&self) -> Option { + ImageDataLayout::from_format(self.dimensions.subimage_extents(), self.format) + } +} + +impl Default for TextureDescriptor { + fn default() -> Self { + Self::new() + } +} + +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub enum TextureDimensions { + D1(u32), + D2(USize2), + D2Array { size: USize2, array_len: u32 }, + Cube(USize2), + CubeArray { size: USize2, array_len: u32 }, + D3(USize3), +} + +impl TextureDimensions { + #[inline] + pub fn extents(&self) -> USize3 { + match *self { + Self::D1(len) => usize3(len, 1, 1), + Self::D2(size) => usize3(size.x, size.y, 1), + Self::D2Array { size, array_len } => usize3(size.x, size.y, array_len), + Self::Cube(size) => usize3(size.x, size.y, 6), + Self::CubeArray { size, array_len } => usize3(size.x, size.y, array_len * 6), + Self::D3(size) => size, + } + } + + #[inline] + pub fn subimage_extents(&self) -> USize2 { + match *self { + Self::D1(len) => usize2(len, 1), + Self::D2(size) => size, + Self::D2Array { size, .. } => size, + Self::Cube(size) => size, + Self::CubeArray { size, .. } => size, + Self::D3(size) => usize2(size.x, size.y), + } + } +} + +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +#[non_exhaustive] +pub enum TextureFormat { + // 8-bit formats + R8Unorm = 0, + R8Snorm = 1, + R8Uint = 2, + R8Sint = 3, + + // 16-bit formats + R16Uint = 4, + R16Sint = 5, + R16Float = 6, + Rg8Unorm = 7, + Rg8Snorm = 8, + Rg8Uint = 9, + Rg8Sint = 10, + + // 32-bit formats + R32Uint = 11, + R32Sint = 12, + R32Float = 13, + Rg16Uint = 14, + Rg16Sint = 15, + Rg16Float = 16, + Rgba8Unorm = 17, + Rgba8UnormSrgb = 18, + Rgba8Snorm = 19, + Rgba8Uint = 20, + Rgba8Sint = 21, + Bgra8Unorm = 22, + Bgra8UnormSrgb = 23, + + // Packed 32-bit formats + Rgb9E5Ufloat = 24, + Rgb10A2Unorm = 25, + Rg11B10Float = 26, + + // 64-bit formats + Rg32Uint = 27, + Rg32Sint = 28, + Rg32Float = 29, + Rgba16Uint = 30, + Rgba16Sint = 31, + Rgba16Float = 32, + + // 128-bit formats + Rgba32Uint = 33, + Rgba32Sint = 34, + Rgba32Float = 35, + + // Depth and stencil formats + // https://gpuweb.github.io/gpuweb/#depth-formats + Depth16Unorm = 36, // depth, 2 bytes per pixel + Depth24Plus = 37, // depth, (3-)4 bytes per pixel!!! + Depth32Float = 38, // depth 4 bytes per pixel + + // // these can have a variable size! + Stencil8 = 39, // stencil, 1-4 bytes per pixel + Depth24PlusStencil8 = 40, // depth+stencil, 4-8 bytes per pixel +} + +impl TextureFormat { + pub const DEFAULT: Self = Self::Rgba8UnormSrgb; + + pub fn num_components(self) -> u8 { + use self::TextureFormat::*; + match self { + // 8-bit formats + R8Unorm | R8Snorm | R8Uint | R8Sint => 1, + + // 16-bit formats + R16Uint | R16Sint | R16Float => 1, + Rg8Unorm | Rg8Snorm | Rg8Uint | Rg8Sint => 2, + + // 32-bit formats + R32Uint | R32Sint | R32Float => 1, + Rg16Uint | Rg16Sint | Rg16Float => 2, + Rgba8Unorm | Rgba8UnormSrgb | Rgba8Snorm | Rgba8Uint | Rgba8Sint | Bgra8Unorm + | Bgra8UnormSrgb => 4, + + // Packed 32-bit formats + Rgb9E5Ufloat => 3, + Rgb10A2Unorm => 4, + Rg11B10Float => 3, + + // 64-bit formats + Rg32Uint | Rg32Sint | Rg32Float => 2, + Rgba16Uint | Rgba16Sint | Rgba16Float => 4, + + // 128-bit formats + Rgba32Uint | Rgba32Sint | Rgba32Float => 4, + + // Depth and stencil formats + Depth32Float => 1, + Depth16Unorm => 1, + + // // these can have a variable size! + Stencil8 => 1, + Depth24Plus => 1, + Depth24PlusStencil8 => 2, + } + } + + pub fn bytes_per_pixel(self) -> Option { + use self::TextureFormat::*; + Some(match self { + // 8-bit formats + R8Unorm | R8Snorm | R8Uint | R8Sint => 1, + + // 16-bit formats + R16Uint | R16Sint | R16Float | Rg8Unorm | Rg8Snorm | Rg8Uint | Rg8Sint => 2, + + // 32-bit formats + R32Uint | R32Sint | R32Float | Rg16Uint | Rg16Sint | Rg16Float | Rgba8Unorm + | Rgba8UnormSrgb | Rgba8Snorm | Rgba8Uint | Rgba8Sint | Bgra8Unorm | Bgra8UnormSrgb => { + 4 + } + + // Packed 32-bit formats + Rgb9E5Ufloat | Rgb10A2Unorm | Rg11B10Float => 4, + + // 64-bit formats + Rg32Uint | Rg32Sint | Rg32Float | Rgba16Uint | Rgba16Sint | Rgba16Float => 8, + + // 128-bit formats + Rgba32Uint | Rgba32Sint | Rgba32Float => 16, + + // Depth and stencil formats + Depth16Unorm => 2, + Depth32Float => 4, + + // these can have a variable size! + // https://gpuweb.github.io/gpuweb/#depth-formats + // TODO: provide a way to query these + Stencil8 | Depth24Plus | Depth24PlusStencil8 => { + return None; + } + }) + } + + pub fn aspects(self) -> TextureAspects { + match self { + Self::Stencil8 => TextureAspects::STENCIL, + Self::Depth16Unorm | Self::Depth24Plus | Self::Depth32Float => TextureAspects::DEPTH, + Self::Depth24PlusStencil8 => TextureAspects::DEPTH | TextureAspects::STENCIL, + + _ => TextureAspects::COLOR, + } + } +} + +impl Default for TextureFormat { + #[inline] + fn default() -> Self { + Self::DEFAULT + } +} + +bitflags! { + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] + pub struct TextureAspects: u32 { + const COLOR = 1; + const DEPTH = 2; + const STENCIL = 4; + + const DEFAULT = 0; + } +} + +impl Default for TextureAspects { + #[inline] + fn default() -> Self { + Self::DEFAULT + } +} + +bitflags! { + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] + pub struct TextureUsage: u32 { + const TRANSFER_READ = 0x0001; + const TRANSFER_WRITE = 0x0002; + const HOST_READ = 0x0004; + const HOST_WRITE = 0x0008; + const SAMPLED = 0x0010; + const STORAGE = 0x0020; + const COLOR_ATTACHMENT = 0x0040; + const DEPTH_STENCIL_ATTACHMENT = 0x0080; + const INPUT_ATTACHMENT = 0x0100; + const TRANSIENT = 0x0200; + // modifiers + const PRESENT = 0x1000; + const BY_REGION = 0x2000; + const NONE = 0; + const ALL_ATTACHMENTS = Self::COLOR_ATTACHMENT.bits() | Self::DEPTH_STENCIL_ATTACHMENT.bits() | Self::INPUT_ATTACHMENT.bits(); + } +} + +impl TextureUsage { + #[inline] + pub const fn is_attachment(self) -> bool { + self.intersects(Self::ALL_ATTACHMENTS) + } + #[inline] + pub const fn is_non_attachment(self) -> bool { + self.intersects(Self::ALL_ATTACHMENTS.complement()) + } +} + +impl Access { + pub fn as_texture_usage(self) -> TextureUsage { + let mut result = TextureUsage::NONE; + if self.intersects( + Self::COLOR_INPUT_ATTACHMENT_READ | Self::DEPTH_STENCIL_INPUT_ATTACHMENT_READ, + ) { + result |= TextureUsage::INPUT_ATTACHMENT; + } + + if self.intersects(Self::SHADER_READ | Self::SHADER_WRITE) { + result |= TextureUsage::STORAGE; + } + if self.intersects(Self::SAMPLED_READ) { + result |= TextureUsage::SAMPLED; + } + + if self.intersects(Self::COLOR_ATTACHMENT_READ | Self::COLOR_ATTACHMENT_WRITE) { + result |= TextureUsage::COLOR_ATTACHMENT; + } + if self + .intersects(Self::DEPTH_STENCIL_ATTACHMENT_READ | Self::DEPTH_STENCIL_ATTACHMENT_WRITE) + { + result |= TextureUsage::DEPTH_STENCIL_ATTACHMENT; + } + if self.intersects(Self::TRANSFER_READ) { + result |= TextureUsage::TRANSFER_READ; + } + if self.intersects(Self::TRANSFER_WRITE) { + result |= TextureUsage::TRANSFER_WRITE; + } + if self.intersects(Self::PRESENT) { + result |= TextureUsage::PRESENT; + } + result + } +} + +impl From for TextureUsage { + #[inline] + fn from(access: Access) -> Self { + access.as_texture_usage() + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default, Serialize, Deserialize)] +pub struct ImageDataLayout { + pub offset: usize, + pub bytes_per_row: u32, + pub rows_per_image: u32, +} + +impl ImageDataLayout { + pub fn from_format(extents: USize2, format: TextureFormat) -> Option { + let bytes_per_pixel = format.bytes_per_pixel()?; + Some(Self { + offset: 0, + bytes_per_row: extents.x * bytes_per_pixel as u32, + rows_per_image: extents.y, + }) + } +} diff --git a/crates/render/src/texture/image.rs b/crates/render/src/texture/image.rs new file mode 100644 index 0000000..def7aa3 --- /dev/null +++ b/crates/render/src/texture/image.rs @@ -0,0 +1,154 @@ +use image::{ImageFormat, buffer::ConvertBuffer}; +use thiserror::Error; + +use super::{ImageDataLayout, TextureDescriptor, TextureDimensions, TextureFormat}; +use crate::math::uvec2; + +pub struct Image { + pub data: Vec, + pub layout: ImageDataLayout, + pub descriptor: TextureDescriptor, +} + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum ImageError { + #[error("Unable to detect image format")] + UnknownFormat, + + #[error("Unsupported image layout")] + UnsupportedImageLayout, + + #[error("unable to load image")] + ImageError(#[from] image::ImageError), +} + +// impl RenderAsset for Image { +// type Target = Texture; + +// fn prepare(&self, backend: &mut B) -> Self::Target { +// let texture = backend.create_texture(&self.descriptor).unwrap(); +// backend.write_image(&texture, self); +// texture +// } +// } + +impl Image { + fn from_image_buffer_with_format

( + format: TextureFormat, + image: image::ImageBuffer>, + ) -> Self + where + P: image::Pixel + 'static, + P::Subpixel: bytemuck::Pod, + { + let (width, height) = image.dimensions(); + let bytes_per_pixel = size_of::

(); + let bytes_per_row = bytes_per_pixel as u32 * width; + let data = image.into_raw(); + let bytes: Vec = bytemuck::cast_vec(data); + Self { + data: bytes, + layout: ImageDataLayout { + offset: 0, + bytes_per_row, + rows_per_image: height, + }, + descriptor: TextureDescriptor { + format, + dimensions: TextureDimensions::D2(uvec2(width, height)), + ..Default::default() + }, + } + } + + pub fn load( + file_ext: Option<&str>, + mimetype: Option<&str>, + data: &[u8], + ) -> Result { + let format = mimetype + .and_then(format_from_mimetype) + .or_else(|| file_ext.and_then(ImageFormat::from_extension)) + .or_else(|| image::guess_format(data).ok()) + .ok_or(ImageError::UnknownFormat)?; + let image = image::load_from_memory_with_format(data, format)?; + image.try_into() + } +} + +fn format_from_mimetype(mime: &str) -> Option { + // STRIP: ("image/"|"application/")(-x)?EXTENSION(:.*) + let mut ext = mime + .strip_prefix("image/") + .or_else(|| mime.strip_prefix("application/"))?; + if let Some(offset) = ext.find(':') { + ext = &ext[0..offset]; + }; + match ext { + // map special names + "x-icon" | "vnd.microsoft.icon" => Some(ImageFormat::Ico), + "vnd.radiance" => Some(ImageFormat::Hdr), + "x-portable-bitmap" | "x-portable-pixmap" => Some(ImageFormat::Pnm), + // map names by extension + _ => { + if let Some(rest) = ext.strip_prefix("x-") { + ext = rest; + } + ImageFormat::from_extension(ext) + } + } +} + +impl TryFrom for Image { + type Error = ImageError; + fn try_from(image: image::DynamicImage) -> Result { + use image::DynamicImage; + Ok(match image { + DynamicImage::ImageLuma8(buffer) => buffer.into(), + DynamicImage::ImageLumaA8(buffer) => buffer.into(), + DynamicImage::ImageRgb8(buffer) => buffer.into(), + DynamicImage::ImageRgba8(buffer) => buffer.into(), + DynamicImage::ImageLuma16(buffer) => buffer.into(), + DynamicImage::ImageLumaA16(buffer) => buffer.into(), + DynamicImage::ImageRgb16(buffer) => buffer.into(), + DynamicImage::ImageRgba16(buffer) => buffer.into(), + DynamicImage::ImageRgb32F(buffer) => buffer.into(), + DynamicImage::ImageRgba32F(buffer) => buffer.into(), + _ => { + return Err(ImageError::UnsupportedImageLayout); + } + }) + } +} + +macro_rules! impl_from_imagebuffer { + ($( + $imgform:ident <$prim:ty> $(as $imgconv:ident <$convprim:ty>)? => $texform:ident ; + )*) => {$( + impl From, Vec<$prim>>> for Image { + #[inline] + fn from(image: image::ImageBuffer, Vec<$prim>>) -> Self { + $( + let image: image::ImageBuffer, Vec<$convprim>> = image.convert(); + )? + Self::from_image_buffer_with_format(TextureFormat::$texform, image) + } + } + )*}; +} + +impl_from_imagebuffer! { + Luma => R8Unorm; + LumaA => Rg8Unorm; + Rgb as Rgba => Rgba8UnormSrgb; + Rgba => Rgba8UnormSrgb; + Luma as Luma => R32Float; + LumaA as LumaA => Rg32Float; + Rgb as Rgba => Rgba32Float; + Rgba as Rgba => Rgba32Float; + Luma => R32Float; + LumaA => Rg32Float; + Rgb as Rgba => Rgba32Float; + Rgba => Rgba32Float; +} diff --git a/crates/render/src/texture/mod.rs b/crates/render/src/texture/mod.rs new file mode 100644 index 0000000..896f689 --- /dev/null +++ b/crates/render/src/texture/mod.rs @@ -0,0 +1,6 @@ +mod descriptor; +mod image; + +pub use self::{descriptor::*, image::*}; + +crate::backend::define_gpu_resource!(Texture, TextureDescriptor); diff --git a/crates/render/src/utils.rs b/crates/render/src/utils.rs new file mode 100644 index 0000000..7b1f45d --- /dev/null +++ b/crates/render/src/utils.rs @@ -0,0 +1,310 @@ +pub mod hash { + + #[derive(Default)] + pub struct SimpleU64Hasher(u64); + pub type SimpleU64BuildHasher = std::hash::BuildHasherDefault; + + impl std::hash::Hasher for SimpleU64Hasher { + #[inline] + fn finish(&self) -> u64 { + self.0 + } + + #[inline] + fn write(&mut self, _bytes: &[u8]) { + panic!("PreHashedHasherHasher should only be used with u64") + } + + #[inline] + fn write_u64(&mut self, i: u64) { + self.0 = i; + } + + #[inline] + fn write_i64(&mut self, i: i64) { + self.0 = i as u64; + } + } + + pub type U64HashMap = std::collections::HashMap; + pub type TypeIdHashMap = + std::collections::HashMap; +} + +pub mod serde_slots { + use std::{ + any::{Any, TypeId}, + cell::RefCell, + marker::PhantomData, + }; + + use super::hash::{TypeIdHashMap, U64HashMap}; + + thread_local! { + static CURENT_MAPPER: RefCell>> = RefCell::new(TypeIdHashMap::default()); + } + + struct TypedEntry(U64HashMap); + + impl TypedEntry { + fn new() -> Self { + Self(U64HashMap::default()) + } + fn new_box_any() -> Box + where + K: 'static, + { + Box::new(Self::new()) + } + fn define(&mut self, k: u64, v: K) -> Option { + self.0.insert(k, v) + } + fn resolve(&self, k: u64) -> Option<&K> { + self.0.get(&k) + } + } + pub struct SlotDeserializationMapper { + _private: (), + } + + impl SlotDeserializationMapper { + const INSTANCE: Self = Self { _private: () }; + + pub fn define(&mut self, old: u64, new: K) -> Option { + CURENT_MAPPER.with(|m| { + m.borrow_mut() + .entry(TypeId::of::>()) + .or_insert_with(TypedEntry::::new_box_any) + .downcast_mut::>() + .expect("inconsistend typeid map") + .define(old, new) + }) + } + pub fn resolve(&self, old: u64) -> Option { + CURENT_MAPPER.with(|m| { + m.borrow() + .get(&TypeId::of::>())? + .downcast_ref::>()? + .resolve(old) + .copied() + }) + } + + pub fn with(f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + let is_empty = CURENT_MAPPER.with(|m| m.borrow().is_empty()); + assert!(is_empty, "nested calls are not allowed"); + let mut tmp = Self::INSTANCE; + let r = f(&mut tmp); + // free the map + CURENT_MAPPER.with(|m| std::mem::take(&mut *m.borrow_mut())); + r + } + } + + struct SlotVisitor(PhantomData); + + impl<'de, K: slotmap::Key + 'static> serde::de::Visitor<'de> for SlotVisitor { + type Value = K; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("an integer between 0 and 2^64") + } + + fn visit_i64(self, value: i64) -> Result + where + E: serde::de::Error, + { + self.visit_u64(value as u64) + } + + fn visit_u64(self, old_value: u64) -> Result + where + E: serde::de::Error, + { + if let Some(new_value) = SlotDeserializationMapper::INSTANCE.resolve(old_value) { + Ok(new_value) + } else { + Err(E::custom(format!( + "The reference {} for {} was not defined", + old_value, + std::any::type_name::() + ))) + } + } + } + + struct OptionVisitor(PhantomData); + impl<'de, T> serde::de::Visitor<'de> for OptionVisitor + where + T: slotmap::Key + 'static, + { + type Value = Option; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("option") + } + + #[inline] + fn visit_unit(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } + + #[inline] + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } + + #[inline] + fn visit_some(self, deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + deserialize(deserializer).map(Some) + } + } + + struct VecVisitor(PhantomData); + + impl<'de, T> serde::de::Visitor<'de> for VecVisitor + where + T: slotmap::Key + 'static, + { + type Value = Vec; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("a sequence") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut values = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + + while let Some(SerdeSlotKey(value)) = seq.next_element()? { + values.push(value); + } + + Ok(values) + } + } + + pub struct SerdeSlotKey(pub K); + + impl serde::Serialize for SerdeSlotKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serialize(&self.0, serializer) + } + } + + impl<'de, K: slotmap::Key + 'static> serde::Deserialize<'de> for SerdeSlotKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserialize(deserializer).map(SerdeSlotKey) + } + } + + #[inline] + pub fn serialize(value: &K, s: S) -> Result + where + S: serde::ser::Serializer, + K: slotmap::Key + 'static, + { + s.serialize_u64(value.data().as_ffi()) + } + + #[inline] + pub fn deserialize<'de, D, K>(d: D) -> Result + where + D: serde::Deserializer<'de>, + K: slotmap::Key + 'static, + { + d.deserialize_u64(SlotVisitor(PhantomData::)) + } + + pub mod option { + use std::marker::PhantomData; + + #[inline] + pub fn serialize(value: &Option, s: S) -> Result + where + S: serde::ser::Serializer, + K: slotmap::Key + 'static, + { + if let Some(value) = value { + s.serialize_some(&super::SerdeSlotKey(*value)) + } else { + s.serialize_none() + } + } + + #[inline] + pub fn deserialize<'de, D, K>(d: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + K: slotmap::Key + 'static, + { + d.deserialize_u64(super::OptionVisitor(PhantomData::)) + } + } + + pub mod slice { + use serde::ser::SerializeSeq; + + #[inline] + pub fn serialize(value: &[K], s: S) -> Result + where + S: serde::ser::Serializer, + K: slotmap::Key + 'static, + { + let mut seq = s.serialize_seq(Some(value.len()))?; + for item in value { + seq.serialize_element(&super::SerdeSlotKey(*item))?; + } + seq.end() + } + } + + pub mod vec { + use std::marker::PhantomData; + + pub use super::slice::serialize; + + #[inline] + pub fn deserialize<'de, D, K>(d: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + K: slotmap::Key + 'static, + { + d.deserialize_seq(super::VecVisitor(PhantomData::)) + } + } + + pub mod cow_vec { + pub use super::slice::serialize; + + #[inline] + pub fn deserialize<'de, D, K>(d: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + K: slotmap::Key + 'static, + { + let vec = super::vec::deserialize(d)?; + Ok(std::borrow::Cow::Owned(vec)) + } + } +} diff --git a/crates/transform/CHANGELOG.md b/crates/transform/CHANGELOG.md new file mode 100644 index 0000000..7fda291 --- /dev/null +++ b/crates/transform/CHANGELOG.md @@ -0,0 +1,6 @@ +# `pulz-transform` Changelog +All notable changes to this crate will be documented in this file. + +## Unreleased (DATE) + + * Initial version diff --git a/crates/transform/Cargo.toml b/crates/transform/Cargo.toml new file mode 100644 index 0000000..bb8c9ab --- /dev/null +++ b/crates/transform/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "pulz-transform" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +readme = "README.md" + +[dependencies] +pulz-ecs = { path = "../ecs" } + +glam = { workspace = true, features = ["mint", "bytemuck", "serde"] } +serde = { workspace = true, features = ["derive"] } diff --git a/crates/transform/README.md b/crates/transform/README.md new file mode 100644 index 0000000..fb0bb1b --- /dev/null +++ b/crates/transform/README.md @@ -0,0 +1,34 @@ +# `pulz-transform` + + + +[![Crates.io](https://img.shields.io/crates/v/pulz-transform.svg?label=pulz-transform)](https://crates.io/crates/pulz-transform) +[![docs.rs](https://docs.rs/pulz-transform/badge.svg)](https://docs.rs/pulz-transform/) +[![license: MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](#license) +[![Rust CI](https://github.com/HellButcher/pulz/actions/workflows/rust.yml/badge.svg)](https://github.com/HellButcher/pulz/actions/workflows/rust.yml) + + +**TODO** + +## Example + + +**TODO** + +## License + +[license]: #license + +This project is licensed under either of + +* MIT license ([LICENSE-MIT] or ) +* Apache License, Version 2.0, ([LICENSE-APACHE] or ) + +at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +[LICENSE-MIT]: ../../LICENSE-MIT +[LICENSE-APACHE]: ../../LICENSE-APACHE diff --git a/crates/transform/src/components/mod.rs b/crates/transform/src/components/mod.rs new file mode 100644 index 0000000..6efac93 --- /dev/null +++ b/crates/transform/src/components/mod.rs @@ -0,0 +1,5 @@ +mod parent; +mod transform; + +pub use parent::*; +pub use transform::*; diff --git a/crates/transform/src/components/parent.rs b/crates/transform/src/components/parent.rs new file mode 100644 index 0000000..6857703 --- /dev/null +++ b/crates/transform/src/components/parent.rs @@ -0,0 +1,18 @@ +use pulz_ecs::prelude::*; + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Component)] +pub struct Parent(pub Entity); + +impl std::ops::Deref for Parent { + type Target = Entity; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for Parent { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/crates/transform/src/components/transform.rs b/crates/transform/src/components/transform.rs new file mode 100644 index 0000000..852d1f8 --- /dev/null +++ b/crates/transform/src/components/transform.rs @@ -0,0 +1,176 @@ +use pulz_ecs::prelude::*; + +use crate::math::{Mat3, Mat4, Quat, Vec3}; + +macro_rules! define_transform { + ($Transform:ident) => { + #[derive(Debug, PartialEq, Clone, Copy, Component)] + pub struct $Transform { + pub translation: Vec3, + pub rotation: Quat, + pub scale: Vec3, + } + + impl $Transform { + pub const IDENTITY: Self = Self::identity(); + + #[inline] + pub fn from_xyz(x: f32, y: f32, z: f32) -> Self { + Self::from_translation(Vec3::new(x, y, z)) + } + + #[inline] + pub const fn identity() -> Self { + Self { + translation: Vec3::ZERO, + rotation: Quat::IDENTITY, + scale: Vec3::ONE, + } + } + + #[inline] + pub fn from_matrix(matrix: Mat4) -> Self { + let (scale, rotation, translation) = matrix.to_scale_rotation_translation(); + Self { + translation, + rotation, + scale, + } + } + + #[inline] + pub const fn from_translation(translation: Vec3) -> Self { + Self { + translation, + ..Self::identity() + } + } + + #[inline] + pub const fn from_rotation(rotation: Quat) -> Self { + Self { + rotation, + ..Self::identity() + } + } + + #[inline] + pub const fn from_scale(scale: Vec3) -> Self { + Self { + scale, + ..Self::identity() + } + } + + #[inline] + pub fn looking_at(mut self, target: Vec3, up: Vec3) -> Self { + self.look_at(target, up); + self + } + + #[inline] + pub fn to_matrix(&self) -> Mat4 { + Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation) + } + + #[inline] + pub fn rotate(&mut self, rotation: Quat) { + self.rotation *= rotation; + } + + #[inline] + pub fn look_at(&mut self, target: Vec3, up: Vec3) { + let forward = Vec3::normalize(self.translation - target); + let right = up.cross(forward).normalize(); + let up = forward.cross(right); + self.rotation = Quat::from_mat3(&Mat3::from_cols(right, up, forward)); + } + } + + impl Default for $Transform { + #[inline] + fn default() -> Self { + Self::identity() + } + } + + impl std::ops::Mul for $Transform { + type Output = Self; + + #[inline] + fn mul(self, transform: Self) -> Self::Output { + let translation = self * transform.translation; + let rotation = self.rotation * transform.rotation; + let scale = self.scale * transform.scale; + Self { + translation, + rotation, + scale, + } + } + } + + impl std::ops::Mul for &$Transform { + type Output = $Transform; + + #[inline] + fn mul(self, transform: Self) -> Self::Output { + let translation = self * transform.translation; + let rotation = self.rotation * transform.rotation; + let scale = self.scale * transform.scale; + $Transform { + translation, + rotation, + scale, + } + } + } + + impl std::ops::Mul for $Transform { + type Output = Vec3; + + #[inline] + fn mul(self, mut value: Vec3) -> Self::Output { + value = self.rotation * value; + value = self.scale * value; + value += self.translation; + value + } + } + + impl std::ops::Mul for &$Transform { + type Output = Vec3; + + #[inline] + fn mul(self, mut value: Vec3) -> Self::Output { + value = self.rotation * value; + value = self.scale * value; + value += self.translation; + value + } + } + }; +} + +define_transform!(Transform); +define_transform!(GlobalTransform); + +impl From for Transform { + fn from(transform: GlobalTransform) -> Self { + Self { + translation: transform.translation, + rotation: transform.rotation, + scale: transform.scale, + } + } +} + +impl From for GlobalTransform { + fn from(transform: Transform) -> Self { + Self { + translation: transform.translation, + rotation: transform.rotation, + scale: transform.scale, + } + } +} diff --git a/crates/transform/src/lib.rs b/crates/transform/src/lib.rs new file mode 100644 index 0000000..fd6f211 --- /dev/null +++ b/crates/transform/src/lib.rs @@ -0,0 +1,38 @@ +#![warn( + // missing_docs, + // rustdoc::missing_doc_code_examples, + future_incompatible, + rust_2018_idioms, + unused, + trivial_casts, + trivial_numeric_casts, + unused_lifetimes, + unused_qualifications, + unused_crate_dependencies, + clippy::cargo, + clippy::multiple_crate_versions, + clippy::empty_line_after_outer_attr, + clippy::fallible_impl_from, + clippy::redundant_pub_crate, + clippy::use_self, + clippy::suspicious_operation_groupings, + clippy::useless_let_if_seq, + // clippy::missing_errors_doc, + // clippy::missing_panics_doc, + clippy::wildcard_imports +)] +#![doc(html_logo_url = "https://raw.githubusercontent.com/HellButcher/pulz/master/docs/logo.png")] +#![doc(html_no_source)] +#![doc = include_str!("../README.md")] + +pub mod math { + pub use glam::*; + pub type Point2 = Vec2; + pub type Size2 = Vec2; + pub type Point3 = Vec3; + pub type Size3 = Vec3; + pub type USize2 = UVec2; + pub type USize3 = UVec3; + pub use glam::{uvec2 as usize2, uvec3 as usize3, vec2 as size2, vec3 as size3}; +} +pub mod components; diff --git a/crates/window-winit/CHANGELOG.md b/crates/window-winit/CHANGELOG.md new file mode 100644 index 0000000..a251215 --- /dev/null +++ b/crates/window-winit/CHANGELOG.md @@ -0,0 +1,6 @@ +# `pulz-window-winit` Changelog +All notable changes to this crate will be documented in this file. + +## Unreleased (DATE) + + * Initial version diff --git a/crates/window-winit/Cargo.toml b/crates/window-winit/Cargo.toml new file mode 100644 index 0000000..c26be03 --- /dev/null +++ b/crates/window-winit/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "pulz-window-winit" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +readme = "README.md" + +[features] +default = ["x11", "wayland"] +x11 = ["winit/x11"] +wayland = ["winit/wayland", "winit/wayland-dlopen", "winit/wayland-csd-adwaita"] +android-native-activity = ["winit/android-native-activity"] +android-game-activity = ["winit/android-game-activity"] + +[dependencies] +pulz-ecs = { path = "../ecs" } +pulz-window = { path = "../window" } +pulz-input = { path = "../input" } + +fnv = { workspace = true } +tracing = { workspace = true } +image = { workspace = true, features = ["png"] } +winit = { version = "0.30", default-features = false, features = ["rwh_06"] } +raw-window-handle = { workspace = true, features = ["std"] } + +[target.'cfg(not(target_os = "unknown"))'.dev-dependencies] +tracing-subscriber = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +tracing-wasm = { workspace = true } +tracing-log = { workspace = true } +console_error_panic_hook = { workspace = true } +wasm-bindgen = { workspace = true } +web-sys = { workspace = true, features = ["HtmlCanvasElement"] } + +[package.metadata.docs.rs] +all-features = true +targets = ["x86_64-unknown-linux-gnu"] +rustdoc-args = ["--cfg", "docsrs"] diff --git a/crates/window-winit/README.md b/crates/window-winit/README.md new file mode 100644 index 0000000..8aae1f4 --- /dev/null +++ b/crates/window-winit/README.md @@ -0,0 +1,34 @@ +# `pulz-window-winit` + + + +[![Crates.io](https://img.shields.io/crates/v/pulz-window-winit.svg?label=pulz-window-winit)](https://crates.io/crates/pulz-window-winit) +[![docs.rs](https://docs.rs/pulz-window-winit/badge.svg)](https://docs.rs/pulz-window-winit/) +[![license: MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](#license) +[![Rust CI](https://github.com/HellButcher/pulz/actions/workflows/rust.yml/badge.svg)](https://github.com/HellButcher/pulz/actions/workflows/rust.yml) + + +**TODO** + +## Example + + +**TODO** + +## License + +[license]: #license + +This project is licensed under either of + +* MIT license ([LICENSE-MIT] or ) +* Apache License, Version 2.0, ([LICENSE-APACHE] or ) + +at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +[LICENSE-MIT]: ../../LICENSE-MIT +[LICENSE-APACHE]: ../../LICENSE-APACHE diff --git a/crates/window-winit/examples/window-winit-demo.rs b/crates/window-winit/examples/window-winit-demo.rs new file mode 100644 index 0000000..0afbb0a --- /dev/null +++ b/crates/window-winit/examples/window-winit-demo.rs @@ -0,0 +1,61 @@ +use std::error::Error; + +use pulz_ecs::prelude::*; +use pulz_window_winit::Application; +use tracing::*; +use winit::event_loop::EventLoop; + +fn init() -> Resources { + info!("Initializing..."); + Resources::new() + /* + let window_attributes = pulz_window_winit::default_window_attributes(&event_loop); + let (window_system, _window_id, window) = + WinitWindowModule::new(WindowDescriptor::default(), &event_loop) + .unwrap() + .install(&mut resources); + + resources + */ +} + +#[cfg(not(target_arch = "wasm32"))] +fn main() -> Result<(), Box> { + use tracing_subscriber::{EnvFilter, fmt::format::FmtSpan}; + let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + tracing_subscriber::fmt() + .with_env_filter(env_filter) + .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE) + .init(); + + let event_loop = EventLoop::new().unwrap(); + let resources = init(); + let mut app = Application::new(resources); + event_loop.run_app(&mut app).map_err(Into::into) +} + +#[cfg(target_arch = "wasm32")] +fn main() { + use wasm_bindgen::prelude::*; + use winit::platform::web::*; + + console_error_panic_hook::set_once(); + tracing_log::LogTracer::init().expect("unable to create log-tracer"); + tracing_wasm::set_as_global_default(); + + let event_loop = EventLoop::new().unwrap(); + let resources = init(); + let app = Application::new(resources); + + /* + let canvas = window.canvas(); + canvas.style().set_css_text("background-color: teal;"); + web_sys::window() + .and_then(|win| win.document()) + .and_then(|doc| doc.body()) + .and_then(|body| body.append_child(&web_sys::Element::from(canvas)).ok()) + .expect("couldn't append canvas to document body"); + */ + + event_loop.spawn_app(app); +} diff --git a/crates/window-winit/src/icon.png b/crates/window-winit/src/icon.png new file mode 100644 index 0000000..c45f9bd Binary files /dev/null and b/crates/window-winit/src/icon.png differ diff --git a/crates/window-winit/src/lib.rs b/crates/window-winit/src/lib.rs new file mode 100644 index 0000000..00271de --- /dev/null +++ b/crates/window-winit/src/lib.rs @@ -0,0 +1,419 @@ +#![warn( + // missing_docs, + // rustdoc::missing_doc_code_examples, + future_incompatible, + rust_2018_idioms, + unused, + trivial_casts, + trivial_numeric_casts, + unused_lifetimes, + unused_qualifications, + unused_crate_dependencies, + clippy::cargo, + clippy::multiple_crate_versions, + clippy::empty_line_after_outer_attr, + clippy::fallible_impl_from, + clippy::redundant_pub_crate, + clippy::use_self, + clippy::suspicious_operation_groupings, + clippy::useless_let_if_seq, + // clippy::missing_errors_doc, + // clippy::missing_panics_doc, + clippy::wildcard_imports +)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc(html_logo_url = "https://raw.githubusercontent.com/HellButcher/pulz/master/docs/logo.png")] +#![doc(html_no_source)] +#![doc = include_str!("../README.md")] + +use std::collections::HashMap; + +use pulz_ecs::{prelude::*, resource::RemovedResource}; +use pulz_window::{ + Window, WindowAttributes, WindowId, Windows, WindowsMirror, listener::WindowSystemListener, +}; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use tracing::{debug, info, warn}; +pub use winit; +use winit::{ + application::ApplicationHandler, + error::OsError, + event::WindowEvent, + event_loop::{ActiveEventLoop, ControlFlow}, + window::{ + Icon, Window as WinitWindow, WindowAttributes as WinitWindowAttributes, + WindowId as WinitWindowId, + }, +}; + +struct WindowState { + window: WinitWindow, + id: WindowId, +} + +struct WinitWindowFactory { + icon: Icon, +} +struct WinitWindowMap { + ids: WindowsMirror, + state: HashMap, +} + +pub struct Application { + resources: Resources, + window_factory: WinitWindowFactory, + window_map: WinitWindowMap, + active: bool, + schedule: RemovedResource, + windows_resource_id: ResourceId, +} + +impl WinitWindowFactory { + pub const DEFAULT_TITLE: &'static str = + concat!(env!("CARGO_PKG_NAME"), ": ", env!("CARGO_PKG_VERSION")); + fn create_winit_window( + &mut self, + event_loop: &ActiveEventLoop, + mut attributes: WinitWindowAttributes, + ) -> Result { + if attributes.title.is_empty() { + attributes.title = Self::DEFAULT_TITLE.to_owned(); + } + if attributes.window_icon.is_none() { + attributes.window_icon = Some(self.icon.clone()); + } + + #[cfg(all( + any(feature = "x11", feature = "wayland"), + unix, + not(target_vendor = "apple"), + not(target_os = "android"), + not(target_os = "emscripten"), + not(target_os = "redox"), + ))] + { + use winit::platform::startup_notify::{ + EventLoopExtStartupNotify, WindowAttributesExtStartupNotify, + }; + if let Some(token) = event_loop.read_token_from_env() { + winit::platform::startup_notify::reset_activation_token_env(); + info!({ ?token }, "Using token to activate a window"); + attributes = attributes.with_activation_token(token); + } + } + + event_loop.create_window(attributes) + } +} + +impl WinitWindowMap { + fn insert_winit_window(&mut self, id: WindowId, winit_window: WinitWindow) { + let winit_window_id = winit_window.id(); + self.ids.insert(id, winit_window_id); + self.state.insert( + winit_window_id, + WindowState { + window: winit_window, + id, + }, + ); + info!({ ?id, ?winit_window_id }, "new window"); + } + + fn contains_id(&self, id: WindowId) -> bool { + self.ids.contains_key(id) + } + fn get_mut_by_winit_id(&mut self, id: WinitWindowId) -> Option<&mut WindowState> { + self.state.get_mut(&id) + } + + fn is_empty(&self) -> bool { + self.state.is_empty() + } + + fn remove_by_winit_id(&mut self, winit_window_id: WinitWindowId) -> Option { + if let Some(window_state) = self.state.remove(&winit_window_id) { + self.ids.remove(window_state.id); + Some(window_state) + } else { + None + } + } +} + +impl Application { + pub fn new(mut resources: Resources) -> Self { + let windows_resource_id = resources.init::(); + let schedule = resources.remove::().expect("schedule"); + let icon = load_icon(include_bytes!("icon.png")); + Self { + resources, + window_factory: WinitWindowFactory { icon }, + window_map: WinitWindowMap { + ids: WindowsMirror::new(), + state: HashMap::new(), + }, + active: false, + schedule, + windows_resource_id, + } + } + + pub fn into_resources(self) -> Resources { + let Self { + mut resources, + schedule, + .. + } = self; + resources.insert_again(schedule); + resources + } + + #[inline] + pub fn resources(&self) -> &Resources { + &self.resources + } + + #[inline] + pub fn resources_mut(&mut self) -> &mut Resources { + &mut self.resources + } + + pub fn default_window_attributes() -> WinitWindowAttributes { + let attributes = WinitWindow::default_attributes().with_transparent(true); + + #[cfg(all(target_family = "wasm", target_os = "unknown"))] + { + use winit::platform::web::WindowAttributesExtWebSys; + attributes = attributes.with_append(true); + } + + #[cfg(target_os = "windows")] + { + use winit::platform::windows::WindowAttributesExtWindows; + attributes = attributes.with_drag_and_drop(false); + } + + attributes + } + + fn winit_window_attributes_from_attributes( + attributes: WindowAttributes, + ) -> WinitWindowAttributes { + let mut winit_attributes = Self::default_window_attributes(); + if let Some(size) = attributes.size { + winit_attributes.inner_size = + Some(winit::dpi::PhysicalSize::new(size.x, size.y).into()); + } + if !attributes.title.is_empty() { + winit_attributes.title = attributes.title.into_owned(); + } + winit_attributes + } + + pub fn create_window( + &mut self, + event_loop: &ActiveEventLoop, + winit_window_attributes: WinitWindowAttributes, + ) -> Result { + let winit_window = self + .window_factory + .create_winit_window(event_loop, winit_window_attributes)?; + let mut windows = self + .resources + .borrow_res_mut_id(self.windows_resource_id) + .expect("Windows"); + let (id, window) = windows.create_new(); + update_window_from_winit(window, &winit_window); + let display_handle = winit_window.display_handle().unwrap(); + let window_handle = winit_window.window_handle().unwrap(); + self.resources + .foreach_meta_mut(|l: &mut dyn WindowSystemListener| { + l.on_created(id, window, display_handle, window_handle) + }); + self.window_map.insert_winit_window(id, winit_window); + Ok(id) + } + + fn sync_create_windows(&mut self, event_loop: &ActiveEventLoop) -> Result<(), OsError> { + let mut windows = self + .resources + .borrow_res_mut_id(self.windows_resource_id) + .expect("Windows"); + while let Some((id, window, window_attributes)) = windows.pop_next_window_to_create() { + if window.is_pending && !window.is_close_requested && !self.window_map.contains_id(id) { + let winit_window_attributes = + Self::winit_window_attributes_from_attributes(window_attributes); + let winit_window = self + .window_factory + .create_winit_window(event_loop, winit_window_attributes)?; + update_window_from_winit(window, &winit_window); + let display_handle = winit_window.display_handle().unwrap(); + let window_handle = winit_window.window_handle().unwrap(); + self.resources + .foreach_meta_mut(|l: &mut dyn WindowSystemListener| { + l.on_created(id, window, display_handle, window_handle) + }); + self.window_map.insert_winit_window(id, winit_window); + } + } + Ok(()) + } + + fn sync_close_windows(&mut self) -> bool { + let windows = self + .resources + .get_mut_id(self.windows_resource_id) + .expect("Windows"); + let mut to_close = Vec::new(); + for (window_id, window_state) in self.window_map.state.iter() { + match windows.get(window_state.id) { + Some(w) if !w.is_close_requested => {} + _ => to_close.push(*window_id), + } + } + if !to_close.is_empty() { + debug!("Closing {} windows", to_close.len()); + for winit_window_id in to_close { + self.close_window_by_winit_id(winit_window_id); + } + } + self.window_map.is_empty() // all windows closed + } + + fn close_window_by_winit_id(&mut self, winit_window_id: WinitWindowId) -> bool { + if let Some(window_state) = self.window_map.remove_by_winit_id(winit_window_id) { + info!({id=?window_state.id, ?winit_window_id}, "Window closing"); + self.resources + .foreach_meta_mut(|l: &mut dyn WindowSystemListener| l.on_closed(window_state.id)); + let windows = self + .resources + .get_mut_id(self.windows_resource_id) + .expect("Windows"); + windows.close(window_state.id); + true + } else { + false + } + } + + fn get_window_with_state_by_winit_id_mut( + &mut self, + winit_window_id: WinitWindowId, + ) -> Option<(&mut Window, &mut WindowState)> { + let window_state = self.window_map.get_mut_by_winit_id(winit_window_id)?; + let windows = self.resources.get_mut_id(self.windows_resource_id)?; + let window = windows.get_mut(window_state.id)?; + Some((window, window_state)) + } + + fn run_schedule(&mut self) { + self.schedule.run(&mut self.resources); + } +} + +fn update_window_from_winit(window: &mut Window, winit_window: &WinitWindow) { + window.scale_factor = winit_window.scale_factor(); + let phys_size: [u32; 2] = winit_window.inner_size().into(); + window.size = phys_size.into(); + window.is_pending = false; +} + +impl ApplicationHandler for Application { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + info!("resumed"); + if !self.active { + self.active = true; + self.sync_create_windows(event_loop).unwrap(); + self.resources + .foreach_meta_mut(|l: &mut dyn WindowSystemListener| l.on_resumed()); + } + event_loop.set_control_flow(ControlFlow::Poll); + } + + fn suspended(&mut self, event_loop: &ActiveEventLoop) { + info!("suspended"); + if self.active { + self.active = false; + if self.sync_close_windows() { + // all windows closed + event_loop.exit(); + } + self.resources + .foreach_meta_mut(|l: &mut dyn WindowSystemListener| l.on_suspended()); + } + event_loop.set_control_flow(ControlFlow::Wait); + } + + fn exiting(&mut self, _event_loop: &ActiveEventLoop) { + info!("event loop ended"); + if self.active { + self.active = false; + self.sync_close_windows(); + self.resources + .foreach_meta_mut(|l: &mut dyn WindowSystemListener| l.on_suspended()); + } + self.resources.clear(); + } + + fn window_event( + &mut self, + _event_loop: &ActiveEventLoop, + winit_window_id: WinitWindowId, + event: WindowEvent, + ) { + match event { + WindowEvent::Destroyed => { + self.close_window_by_winit_id(winit_window_id); + } + WindowEvent::CloseRequested => { + let Some((window, window_state)) = + self.get_window_with_state_by_winit_id_mut(winit_window_id) + else { + return; + }; + debug!({ id=?window_state.id, ?winit_window_id}, "close requested"); + window.is_close_requested = true; + } + WindowEvent::Resized(size) => { + let Some((window, _window_state)) = + self.get_window_with_state_by_winit_id_mut(winit_window_id) + else { + return; + }; + let phys_size: [u32; 2] = size.into(); + window.size = phys_size.into(); + } + WindowEvent::ScaleFactorChanged { scale_factor, .. } => { + let Some((window, _window_state)) = + self.get_window_with_state_by_winit_id_mut(winit_window_id) + else { + return; + }; + window.scale_factor = scale_factor; + } + _ => {} + } + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if self.active { + self.run_schedule(); + self.sync_create_windows(event_loop).unwrap(); + if self.sync_close_windows() { + // all windows closed + event_loop.exit(); + } + } + } +} + +fn load_icon(bytes: &[u8]) -> Icon { + let (icon_rgba, icon_width, icon_height) = { + let image = image::load_from_memory(bytes).unwrap().into_rgba8(); + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + (rgba, width, height) + }; + Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon") +} diff --git a/crates/window/CHANGELOG.md b/crates/window/CHANGELOG.md new file mode 100644 index 0000000..8758e92 --- /dev/null +++ b/crates/window/CHANGELOG.md @@ -0,0 +1,6 @@ +# `pulz-window` Changelog +All notable changes to this crate will be documented in this file. + +## Unreleased (DATE) + + * Initial version diff --git a/crates/window/Cargo.toml b/crates/window/Cargo.toml new file mode 100644 index 0000000..7487bcc --- /dev/null +++ b/crates/window/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "pulz-window" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +readme = "README.md" + +[dependencies] +pulz-ecs = { path = "../ecs" } +raw-window-handle = { workspace = true } + +slotmap = { workspace = true } +glam = { workspace = true, features = ["mint", "bytemuck"] } diff --git a/crates/window/README.md b/crates/window/README.md new file mode 100644 index 0000000..24154db --- /dev/null +++ b/crates/window/README.md @@ -0,0 +1,34 @@ +# `pulz-window` + + + +[![Crates.io](https://img.shields.io/crates/v/pulz-window.svg?label=pulz-window)](https://crates.io/crates/pulz-window) +[![docs.rs](https://docs.rs/pulz-window/badge.svg)](https://docs.rs/pulz-window/) +[![license: MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](#license) +[![Rust CI](https://github.com/HellButcher/pulz/actions/workflows/rust.yml/badge.svg)](https://github.com/HellButcher/pulz/actions/workflows/rust.yml) + + +**TODO** + +## Example + + +**TODO** + +## License + +[license]: #license + +This project is licensed under either of + +* MIT license ([LICENSE-MIT] or ) +* Apache License, Version 2.0, ([LICENSE-APACHE] or ) + +at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +[LICENSE-MIT]: ../../LICENSE-MIT +[LICENSE-APACHE]: ../../LICENSE-APACHE diff --git a/crates/window/src/event.rs b/crates/window/src/event.rs new file mode 100644 index 0000000..fec49e1 --- /dev/null +++ b/crates/window/src/event.rs @@ -0,0 +1,21 @@ +use crate::{Point2, Size2, WindowId}; + +#[derive(Debug, Clone)] +pub struct WindowResized { + pub id: WindowId, + pub size: Size2, +} + +#[derive(Debug, Clone)] +pub enum WindowEvent { + Created(WindowId), + CloseRequested(WindowId), + Closed(WindowId), +} + +#[derive(Debug, Clone)] +pub enum CursorEvent { + Enter(WindowId), + Move(WindowId, Point2), + Leave(WindowId), +} diff --git a/crates/window/src/lib.rs b/crates/window/src/lib.rs new file mode 100644 index 0000000..3d6a292 --- /dev/null +++ b/crates/window/src/lib.rs @@ -0,0 +1,37 @@ +#![warn( + // missing_docs, + // rustdoc::missing_doc_code_examples, + future_incompatible, + rust_2018_idioms, + unused, + trivial_casts, + trivial_numeric_casts, + unused_lifetimes, + unused_qualifications, + unused_crate_dependencies, + clippy::cargo, + clippy::multiple_crate_versions, + clippy::empty_line_after_outer_attr, + clippy::fallible_impl_from, + clippy::redundant_pub_crate, + clippy::use_self, + clippy::suspicious_operation_groupings, + clippy::useless_let_if_seq, + // clippy::missing_errors_doc, + // clippy::missing_panics_doc, + clippy::wildcard_imports +)] +#![doc(html_logo_url = "https://raw.githubusercontent.com/HellButcher/pulz/master/docs/logo.png")] +#![doc(html_no_source)] +#![doc = include_str!("../README.md")] + +pub mod event; +pub mod listener; +mod window; + +pub type Point2 = glam::IVec2; +pub type Size2 = glam::UVec2; + +pub use raw_window_handle::{DisplayHandle, WindowHandle}; + +pub use crate::window::*; diff --git a/crates/window/src/listener.rs b/crates/window/src/listener.rs new file mode 100644 index 0000000..bf69f2a --- /dev/null +++ b/crates/window/src/listener.rs @@ -0,0 +1,19 @@ +use pulz_ecs::impl_any_cast; + +use crate::{DisplayHandle, Window, WindowHandle, WindowId}; + +pub trait WindowSystemListener: 'static { + fn on_created( + &mut self, + _window_id: WindowId, + _window_props: &Window, + _display_handle: DisplayHandle<'_>, + _window_handle: WindowHandle<'_>, + ) { + } + fn on_closed(&mut self, _window_id: WindowId) {} + fn on_resumed(&mut self) {} + fn on_suspended(&mut self) {} +} + +impl_any_cast!(dyn WindowSystemListener); diff --git a/crates/window/src/window.rs b/crates/window/src/window.rs new file mode 100644 index 0000000..defad23 --- /dev/null +++ b/crates/window/src/window.rs @@ -0,0 +1,197 @@ +use std::{borrow::Cow, collections::VecDeque}; + +use pulz_ecs::{Component, module::ModuleWithOutput}; +use slotmap::{SlotMap, new_key_type}; + +use crate::Size2; + +new_key_type! { + #[derive(Component)] + pub struct WindowId; +} + +pub type Iter<'a, T = Window> = slotmap::basic::Iter<'a, WindowId, T>; +pub type IterMut<'a, T = Window> = slotmap::basic::IterMut<'a, WindowId, T>; +pub type WindowsMirror = slotmap::SecondaryMap; + +#[derive(Debug)] +pub struct WindowAttributes { + pub size: Option, + pub title: Cow<'static, str>, + pub vsync: bool, +} + +pub struct Window { + pub size: Size2, + pub scale_factor: f64, + pub vsync: bool, + pub is_pending: bool, + pub is_close_requested: bool, + command_queue: VecDeque, +} + +pub struct Windows { + windows: SlotMap, + created: VecDeque<(WindowId, WindowAttributes)>, +} + +impl WindowAttributes { + pub const fn new() -> Self { + Self { + size: None, + title: Cow::Borrowed(""), + vsync: true, + } + } +} + +impl Default for WindowAttributes { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl Window { + #[inline] + pub const fn new() -> Self { + Self { + size: Size2::ZERO, + scale_factor: 1.0, + vsync: true, + is_pending: true, + is_close_requested: false, + command_queue: VecDeque::new(), + } + } + + pub fn from_attributes(attributes: &WindowAttributes) -> Self { + Self { + size: attributes.size.unwrap_or(Size2::ZERO), + vsync: attributes.vsync, + ..Self::new() + } + } +} + +impl Default for Window { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl Windows { + pub fn new() -> Self { + Self { + windows: SlotMap::with_key(), + created: VecDeque::new(), + } + } + + #[inline] + pub fn create(&mut self, attributes: WindowAttributes) -> WindowId { + let window = Window::from_attributes(&attributes); + let id = self.windows.insert(window); + self.created.push_back((id, attributes)); + id + } + + #[doc(hidden)] + pub fn create_new(&mut self) -> (WindowId, &mut Window) { + let id = self.windows.insert(Window::new()); + let window = self.get_mut(id).unwrap(); + (id, window) + } + + #[doc(hidden)] + pub fn pop_next_window_to_create( + &mut self, + ) -> Option<(WindowId, &mut Window, WindowAttributes)> { + let (id, attributes) = loop { + let (id, attributes) = self.created.pop_front()?; + if self.windows.contains_key(id) { + break (id, attributes); + } + }; + let window = self.windows.get_mut(id).unwrap(); + Some((id, window, attributes)) + } + + #[inline] + pub fn len(&self) -> usize { + self.windows.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.windows.is_empty() + } + + #[inline] + pub fn get(&self, id: WindowId) -> Option<&Window> { + self.windows.get(id) + } + + #[inline] + pub fn get_mut(&mut self, id: WindowId) -> Option<&mut Window> { + self.windows.get_mut(id) + } + + #[inline] + pub fn close(&mut self, id: WindowId) -> bool { + self.windows.remove(id).is_some() + } + + #[inline] + pub fn iter(&self) -> Iter<'_> { + self.windows.iter() + } + + #[inline] + pub fn iter_mut(&mut self) -> IterMut<'_> { + self.windows.iter_mut() + } +} + +impl Default for Windows { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl std::ops::Index for Windows { + type Output = Window; + #[inline] + fn index(&self, id: WindowId) -> &Self::Output { + &self.windows[id] + } +} + +impl std::ops::IndexMut for Windows { + #[inline] + fn index_mut(&mut self, id: WindowId) -> &mut Self::Output { + &mut self.windows[id] + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub enum WindowCommand { + SetTitle(Cow<'static, String>), + SetVisible(bool), + SetFullscreen(bool), + Close, +} + +pub struct WindowModule; + +impl ModuleWithOutput for WindowModule { + type Output<'l> = &'l mut Windows; + + fn install_resources(self, resources: &mut pulz_ecs::prelude::Resources) -> Self::Output<'_> { + let id = resources.init::(); + resources.get_mut_id(id).unwrap() + } +} diff --git a/examples/android/.gitignore b/examples/android/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/examples/android/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/examples/android/Cargo.toml b/examples/android/Cargo.toml new file mode 100644 index 0000000..df91ced --- /dev/null +++ b/examples/android/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "example-android" +publish = false +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true + +[lib] +name="pulzdemo_android" +crate-type=["cdylib"] + +[dependencies] +pulz-ecs = { path = "../../crates/ecs" } +pulz-render = { path = "../../crates/render" } +pulz-render-ash = { path = "../../crates/render-ash" } +pulz-render-pipeline-core = { path = "../../crates/render-pipeline-core" } +pulz-window = { path = "../../crates/window" } +pulz-window-winit = { path = "../../crates/window-winit", features = ["android-game-activity"]} + +log = "0.4" +android_logger = "0.15" +tracing = { workspace = true, features = ["log"] } diff --git a/examples/android/app/.gitignore b/examples/android/app/.gitignore new file mode 100644 index 0000000..1410476 --- /dev/null +++ b/examples/android/app/.gitignore @@ -0,0 +1,2 @@ +/build +/src/main/jniLibs/ diff --git a/examples/android/app/build.gradle b/examples/android/app/build.gradle new file mode 100644 index 0000000..faf04cb --- /dev/null +++ b/examples/android/app/build.gradle @@ -0,0 +1,61 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' + id 'org.mozilla.rust-android-gradle.rust-android' +} + +android { + namespace 'eu.chommel.pulzdemo' + compileSdk 33 + + defaultConfig { + applicationId "eu.chommel.pulzdemo" + minSdk 28 + targetSdk 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + prefab true + } +} + +cargo { + module = "../" + targetDirectory = "../../../target" + libname = "pulzdemo_android" + targets = ["arm", "arm64", "x86", "x86_64"] +} + +tasks.whenTaskAdded { task -> + if ((task.name == 'javaPreCompileDebug' || task.name == 'javaPreCompileRelease')) { + task.dependsOn 'cargoBuild' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.appcompat:appcompat:1.6.0' + implementation 'com.google.android.material:material:1.7.0' + implementation 'androidx.games:games-activity:1.1.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' +} diff --git a/examples/android/app/proguard-rules.pro b/examples/android/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/examples/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/examples/android/app/src/androidTest/java/eu/chommel/pulzdemo/ExampleInstrumentedTest.kt b/examples/android/app/src/androidTest/java/eu/chommel/pulzdemo/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..c5e27f7 --- /dev/null +++ b/examples/android/app/src/androidTest/java/eu/chommel/pulzdemo/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package eu.chommel.pulzdemo + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("eu.chommel.pulzdemo", appContext.packageName) + } +} \ No newline at end of file diff --git a/examples/android/app/src/main/AndroidManifest.xml b/examples/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2dc983b --- /dev/null +++ b/examples/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + diff --git a/examples/android/app/src/main/assets/android_robot.png b/examples/android/app/src/main/assets/android_robot.png new file mode 100644 index 0000000..61385f2 Binary files /dev/null and b/examples/android/app/src/main/assets/android_robot.png differ diff --git a/examples/android/app/src/main/java/eu/chommel/pulzdemo/MainActivity.kt b/examples/android/app/src/main/java/eu/chommel/pulzdemo/MainActivity.kt new file mode 100644 index 0000000..20f9362 --- /dev/null +++ b/examples/android/app/src/main/java/eu/chommel/pulzdemo/MainActivity.kt @@ -0,0 +1,39 @@ +package eu.chommel.pulzdemo + +import android.view.View +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import com.google.androidgamesdk.GameActivity + +class MainActivity : GameActivity() { + companion object { + init { + System.loadLibrary("pulzdemo_android") + } + } + + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + if (hasFocus) { + hideSystemUi() + } + } + + private fun hideSystemUi() { + val decorView = window.decorView +// decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY +// or View.SYSTEM_UI_FLAG_LAYOUT_STABLE +// or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION +// or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN +// or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION +// or View.SYSTEM_UI_FLAG_FULLSCREEN) + + val windowInsetsController = + WindowCompat.getInsetsController(window, decorView) + // Configure the behavior of the hidden system bars. + windowInsetsController.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + windowInsetsController.hide(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()) + } +} diff --git a/examples/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/examples/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/examples/android/app/src/main/res/drawable/ic_launcher_background.xml b/examples/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/examples/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/examples/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/examples/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/examples/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/examples/android/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml b/examples/android/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/examples/android/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/examples/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/examples/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/examples/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/examples/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/examples/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/examples/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/examples/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/examples/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/examples/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/examples/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/examples/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/examples/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/examples/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/examples/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/examples/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/examples/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/examples/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/examples/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/examples/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/examples/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/examples/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/examples/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/examples/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/examples/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/examples/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/examples/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/examples/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/examples/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/examples/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/examples/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/examples/android/app/src/main/res/values-night/themes.xml b/examples/android/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..833cde0 --- /dev/null +++ b/examples/android/app/src/main/res/values-night/themes.xml @@ -0,0 +1,19 @@ + + + + diff --git a/examples/android/app/src/main/res/values/colors.xml b/examples/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/examples/android/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/examples/android/app/src/main/res/values/strings.xml b/examples/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..c462f23 --- /dev/null +++ b/examples/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Pulz Demo + \ No newline at end of file diff --git a/examples/android/app/src/main/res/values/themes.xml b/examples/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..a989b4f --- /dev/null +++ b/examples/android/app/src/main/res/values/themes.xml @@ -0,0 +1,19 @@ + + + + diff --git a/examples/android/app/src/main/res/xml/backup_rules.xml b/examples/android/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/examples/android/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/examples/android/app/src/main/res/xml/data_extraction_rules.xml b/examples/android/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/examples/android/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/examples/android/app/src/test/java/eu/chommel/pulzdemo/ExampleUnitTest.kt b/examples/android/app/src/test/java/eu/chommel/pulzdemo/ExampleUnitTest.kt new file mode 100644 index 0000000..5c9b8ca --- /dev/null +++ b/examples/android/app/src/test/java/eu/chommel/pulzdemo/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package eu.chommel.pulzdemo + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/examples/android/build.gradle b/examples/android/build.gradle new file mode 100644 index 0000000..3178ca6 --- /dev/null +++ b/examples/android/build.gradle @@ -0,0 +1,11 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '7.4.2' apply false + id 'com.android.library' version '7.4.2' apply false + id 'org.jetbrains.kotlin.android' version '1.7.21' apply false + id "org.mozilla.rust-android-gradle.rust-android" version "0.9.3" +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/examples/android/gradle.properties b/examples/android/gradle.properties new file mode 100644 index 0000000..3c5031e --- /dev/null +++ b/examples/android/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/examples/android/gradle/wrapper/gradle-wrapper.jar b/examples/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/examples/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/android/gradle/wrapper/gradle-wrapper.properties b/examples/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..404f439 --- /dev/null +++ b/examples/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Jan 17 19:51:29 CET 2023 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/examples/android/gradlew b/examples/android/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/examples/android/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/examples/android/gradlew.bat b/examples/android/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/examples/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/android/settings.gradle b/examples/android/settings.gradle new file mode 100644 index 0000000..8215ca4 --- /dev/null +++ b/examples/android/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "Pulz Demo" +include ':app' diff --git a/examples/android/src/lib.rs b/examples/android/src/lib.rs new file mode 100644 index 0000000..b5ee7e9 --- /dev/null +++ b/examples/android/src/lib.rs @@ -0,0 +1,48 @@ +use log::info; +#[cfg(target_os = "android")] +use platform::android::activity::AndroidApp; +use pulz_ecs::prelude::*; +use pulz_render::camera::{Camera, RenderTarget}; +use pulz_render_ash::AshRenderer; +use pulz_render_pipeline_core::core_3d::CoreShadingModule; +use pulz_window::{WindowAttributes, WindowId, WindowModule}; + +fn init() -> Resources { + info!("Initializing..."); + let mut resources = Resources::new(); + resources.install(CoreShadingModule); + resources.install(AshRenderer::new().unwrap()); + + let windows = resources.install(WindowModule); + let window_id = windows.create(WindowAttributes::new()); + + setup_demo_scene(&mut resources, window_id); + + resources +} + +fn setup_demo_scene(resources: &mut Resources, window: WindowId) { + let mut world = resources.world_mut(); + + world + .spawn() + .insert(Camera::new()) + .insert(RenderTarget::Window(window)); +} + +#[cfg(target_os = "android")] +#[no_mangle] +pub fn android_main(app: AndroidApp) { + use pulz_window_winit::Application; + use winit::platform::android::EventLoopBuilderExtAndroid; + // #[cfg(debug_assertions)] + // std::env::set_var("RUST_BACKTRACE", "1"); + android_logger::init_once( + android_logger::Config::default().with_max_level(log::LevelFilter::Info), + ); + + let event_loop = EventLoopBuilder::new().with_android_app(app).build(); + let resources = init(); + let mut app = Application::new(resources); + event_loop.run_app(&mut app).unwrap(); +} diff --git a/justfile b/justfile index 8b19b33..dd958a6 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,7 @@ alias b := build alias t := test alias c := check +alias re := run-example default: @just --list @@ -12,13 +13,61 @@ clippy: cargo clippy --workspace --all-targets --all-features clippy-fix: - cargo clippy --fix --workspace --all-targets --all-features + cargo clippy --fix --workspace --all-targets --all-features --allow-dirty --allow-staged test *testname: cargo test --workspace --all-targets --all-features {{testname}} -run: - cargo run +_run_defaults_sh := ' +export RUST_LOG="${RUST_LOG:-debug}" +export PULZ_DUMP_SCHEDULE="dump.dot" +export RUST_BACKTRACE="${RUST_BACKTRACE:-1}" +' + +_vk_layers_sh := ' +export VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation +export VK_DEVICE_LAYERS=VK_LAYER_KHRONOS_validation +export VK_LAYER_DISABLES= +export VK_LAYER_ENABLES="VK_VALIDATION_FEATURE_ENABLE_SYNCHRONIZATION_VALIDATION_EXT:VALIDATION_CHECK_ENABLE_SYNCHRONIZATION_VALIDATION_QUEUE_SUBMIT:VK_VALIDATION_FEATURE_ENABLE_BEST_PRACTICES_EXT" +# :VALIDATION_CHECK_ENABLE_VENDOR_SPECIFIC_NVIDIA:VALIDATION_CHECK_ENABLE_VENDOR_SPECIFIC_AMD +' + +_default_example := 'render-ash-demo' + +list-examples: + cargo run --example + +run-example example=_default_example: + #!/usr/bin/bash + set -e + {{ _run_defaults_sh }} + set -x + exec cargo run --example {{example}} + +validate-example example=_default_example: + #!/usr/bin/bash + set -ex + {{ _run_defaults_sh }} + {{ _vk_layers_sh }} + set -x + exec cargo run --example {{example}} + +capture-example example=_default_example: + #!/usr/bin/bash + set -e + {{ _run_defaults_sh }} + # force usage of x11 + unset WAYLAND_DISPLAY + export CAPTURE_OPTS="\ + --capture-file renderdoc-capture.rdc + --opt-capture-callstacks \ + --opt-hook-children \ + --opt-api-validation + --opt-api-validation-unmute + --wait-for-exit \ + " + set -x + exec renderdoccmd capture $CAPTURE_OPTS cargo run --example {{example}} fmt: cargo +nightly fmt --all diff --git a/run-wasm/Cargo.toml b/run-wasm/Cargo.toml new file mode 100644 index 0000000..433e857 --- /dev/null +++ b/run-wasm/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "run-wasm" +publish = false +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true + +[dependencies] +cargo-run-wasm = "0.4.0" diff --git a/run-wasm/src/main.rs b/run-wasm/src/main.rs new file mode 100644 index 0000000..6961358 --- /dev/null +++ b/run-wasm/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + cargo_run_wasm::run_wasm_with_css("body { margin: 0px; }"); +}