├── .cargo └── config ├── .github └── workflows │ └── check.yaml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── blade-asset ├── Cargo.toml ├── README.md ├── src │ ├── arena.rs │ ├── flat.rs │ └── lib.rs └── tests │ └── main.rs ├── blade-egui ├── Cargo.toml ├── README.md ├── etc │ └── scene-editor.jpg ├── shader.wgsl └── src │ └── lib.rs ├── blade-graphics ├── Cargo.toml ├── README.md ├── etc │ ├── FAQ.md │ ├── motivation.md │ ├── particles.png │ ├── performance.md │ └── ray-query.gif └── src │ ├── derive.rs │ ├── gles │ ├── command.rs │ ├── egl.rs │ ├── mod.rs │ ├── pipeline.rs │ ├── resource.rs │ └── web.rs │ ├── lib.rs │ ├── metal │ ├── command.rs │ ├── mod.rs │ ├── pipeline.rs │ ├── resource.rs │ └── surface.rs │ ├── shader.rs │ ├── traits.rs │ ├── util.rs │ └── vulkan │ ├── command.rs │ ├── descriptor.rs │ ├── init.rs │ ├── mod.rs │ ├── pipeline.rs │ ├── resource.rs │ └── surface.rs ├── blade-helpers ├── Cargo.toml └── src │ ├── camera.rs │ ├── hud.rs │ └── lib.rs ├── blade-macros ├── Cargo.toml ├── src │ ├── as_primitive.rs │ ├── flat.rs │ ├── lib.rs │ ├── shader_data.rs │ └── vertex.rs └── tests │ └── main.rs ├── blade-render ├── Cargo.toml ├── README.md ├── code │ ├── a-trous.wgsl │ ├── camera.inc.wgsl │ ├── debug-blit.wgsl │ ├── debug-draw.wgsl │ ├── debug-param.inc.wgsl │ ├── debug.inc.wgsl │ ├── env-importance.inc.wgsl │ ├── env-prepare.wgsl │ ├── fill-gbuf.wgsl │ ├── gbuf.inc.wgsl │ ├── post-proc.wgsl │ ├── quaternion.inc.wgsl │ ├── random.inc.wgsl │ ├── ray-trace.wgsl │ └── surface.inc.wgsl ├── etc │ └── sponza.jpg └── src │ ├── asset_hub.rs │ ├── lib.rs │ ├── model │ └── mod.rs │ ├── render │ ├── debug.rs │ ├── dummy.rs │ ├── env_map.rs │ └── mod.rs │ ├── shader.rs │ ├── texture │ └── mod.rs │ └── util │ ├── frame_pacer.rs │ └── mod.rs ├── blade-util ├── Cargo.toml ├── README.md └── src │ ├── belt.rs │ └── lib.rs ├── docs ├── CHANGELOG.md ├── README.md ├── architecture2.png ├── logo.png └── vehicle-colliders.jpg ├── examples ├── README.md ├── bunnymark │ ├── main.rs │ └── shader.wgsl ├── init │ ├── env-sample.wgsl │ └── main.rs ├── mini │ ├── main.rs │ └── shader.wgsl ├── move │ ├── data │ │ ├── plane.glb │ │ └── sphere.glb │ └── main.rs ├── particle │ ├── main.rs │ ├── particle.rs │ └── particle.wgsl ├── ray-query │ ├── main.rs │ └── shader.wgsl ├── scene │ ├── data │ │ ├── monkey.bin │ │ ├── monkey.gltf │ │ ├── plane.glb │ │ └── scene.ron │ └── main.rs └── vehicle │ ├── config.rs │ ├── data │ ├── ground.bin │ ├── ground.gltf │ ├── level.ron │ ├── orange_light_grid.png │ ├── raceFuture-body.glb │ ├── raceFuture.ron │ └── wheelRacing.glb │ └── main.rs ├── run-wasm ├── Cargo.toml └── src │ └── main.rs ├── src ├── config.rs ├── lib.rs └── trimesh.rs ├── tests └── parse_shaders.rs └── vk_layer_settings.txt /.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | run-wasm = "run --release --package run-wasm --" 3 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: check 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | tags: [v0.*] 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | include: 14 | - name: Linux 15 | os: ubuntu-latest 16 | target: x86_64-unknown-linux-gnu 17 | full: true 18 | 19 | - name: Windows 20 | os: windows-latest 21 | target: x86_64-pc-windows-msvc 22 | full: true 23 | 24 | - name: macOS 25 | os: macos-latest 26 | target: aarch64-apple-darwin 27 | full: true 28 | 29 | - name: iOS 30 | os: macos-latest 31 | target: aarch64-apple-ios 32 | full: false 33 | 34 | - name: Web 35 | os: ubuntu-latest 36 | target: wasm32-unknown-unknown 37 | full: false 38 | 39 | runs-on: ${{ matrix.os }} 40 | 41 | steps: 42 | - uses: actions/checkout@v3 43 | 44 | - name: Setup Rust 45 | uses: actions-rs/toolchain@v1 46 | with: 47 | toolchain: stable 48 | profile: minimal 49 | target: ${{ matrix.target }} 50 | override: true 51 | 52 | - name: Check Basics 53 | uses: actions-rs/cargo@v1 54 | with: 55 | command: check 56 | args: --target ${{ matrix.target }} --workspace --all --no-default-features 57 | 58 | - name: Test Basics 59 | if: matrix.full 60 | uses: actions-rs/cargo@v1 61 | with: 62 | command: test 63 | args: -p blade-render --no-default-features 64 | 65 | - name: Test All 66 | if: matrix.full 67 | uses: actions-rs/cargo@v1 68 | with: 69 | command: test 70 | args: --workspace --all-features 71 | 72 | - name: Test GLES 73 | if: matrix.name == 'Linux' 74 | uses: actions-rs/cargo@v1 75 | with: 76 | command: build 77 | args: --example bunnymark 78 | env: 79 | RUSTFLAGS: "--cfg gles" 80 | 81 | fmt: 82 | name: Format 83 | runs-on: ubuntu-latest 84 | steps: 85 | - name: checkout repo 86 | uses: actions/checkout@v3 87 | 88 | - name: install rust 89 | uses: actions-rs/toolchain@v1 90 | with: 91 | profile: minimal 92 | toolchain: stable 93 | override: true 94 | components: rustfmt 95 | 96 | - name: run rustfmt 97 | run: | 98 | cargo fmt -- --check 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /target-gl 3 | /target-vk 4 | /Cargo.lock 5 | 6 | /asset-cache 7 | /blade-asset/cooked 8 | /_failure.wgsl 9 | 10 | libEGL.dylib 11 | libGLESv2.dylib 12 | 13 | .DS_Store 14 | /.vs 15 | /.vscode 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "blade-asset", 5 | "blade-egui", 6 | "blade-graphics", 7 | "blade-helpers", 8 | "blade-macros", 9 | "blade-render", 10 | "run-wasm", 11 | ] 12 | exclude = [] 13 | 14 | [workspace.dependencies] 15 | base64 = "0.21" 16 | bitflags = "2" 17 | bytemuck = { version = "1", features = ["derive"] } 18 | choir = "0.7" 19 | egui = "0.29" 20 | glam = { version = "0.28", features = ["mint"] } 21 | gltf = { version = "1.1", default-features = false } 22 | log = "0.4" 23 | mint = "0.5" 24 | naga = { version = "25.0", features = ["wgsl-in"] } 25 | profiling = "1" 26 | slab = "0.4" 27 | strum = { version = "0.26", features = ["derive"] } 28 | web-sys = "0.3.60" 29 | winit = { version = "0.30" } 30 | 31 | [lib] 32 | 33 | [package] 34 | name = "blade" 35 | description = "Sharp and simple graphics library" 36 | version = "0.3.0" 37 | edition = "2021" 38 | rust-version = "1.65" 39 | keywords = ["graphics"] 40 | license = "MIT" 41 | repository = "https://github.com/kvark/blade" 42 | readme = "docs/README.md" 43 | 44 | [features] 45 | 46 | [dependencies] 47 | blade-asset = { version = "0.2", path = "blade-asset" } 48 | blade-egui = { version = "0.6", path = "blade-egui" } 49 | blade-graphics = { version = "0.6", path = "blade-graphics" } 50 | blade-helpers = { version = "0.1", path = "blade-helpers" } 51 | blade-util = { version = "0.2", path = "blade-util" } 52 | base64 = { workspace = true } 53 | choir = { workspace = true } 54 | colorsys = "0.6" 55 | egui = { workspace = true } 56 | gltf = { workspace = true } 57 | nalgebra = { version = "0.33", features = ["mint"] } 58 | log = { workspace = true } 59 | mint = { workspace = true, features = ["serde"] } 60 | num_cpus = "1" 61 | profiling = { workspace = true } 62 | rapier3d = { version = "0.23", features = ["debug-render"] } 63 | serde = { version = "1", features = ["serde_derive"] } 64 | slab = "0.4" 65 | winit = { workspace = true } 66 | 67 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 68 | blade-render = { version = "0.4", path = "blade-render" } 69 | 70 | [dev-dependencies] 71 | blade-macros = { version = "0.3", path = "blade-macros" } 72 | bytemuck = { workspace = true } 73 | choir = { workspace = true } 74 | egui = { workspace = true } 75 | egui-winit = { version = "0.29", default-features = false, features = [ 76 | "links", 77 | ] } 78 | transform-gizmo-egui = "0.4" 79 | env_logger = "0.11" 80 | glam = { workspace = true } 81 | log = { workspace = true } 82 | mint = { workspace = true, features = ["serde"] } 83 | naga = { workspace = true } 84 | nanorand = { version = "0.7", default-features = false, features = ["wyrand"] } 85 | profiling = { workspace = true } 86 | ron = "0.8" 87 | serde = { version = "1", features = ["serde_derive"] } 88 | strum = { workspace = true } 89 | # not following semver :( 90 | del-msh-core = "=0.1.33" 91 | del-geo = "=0.1.29" 92 | 93 | [target.'cfg(target_arch = "wasm32")'.dev-dependencies] 94 | console_error_panic_hook = "0.1.7" 95 | console_log = "1" 96 | web-sys = { workspace = true, features = ["Window"] } 97 | getrandom = { version = "0.2", features = ["js"] } 98 | 99 | [target.'cfg(any(target_os = "windows", target_os = "linux"))'.dev-dependencies] 100 | renderdoc = "0.12" 101 | 102 | # This is too slow in Debug 103 | [profile.dev.package.texpresso] 104 | opt-level = 3 105 | 106 | [package.metadata.cargo_check_external_types] 107 | #TODO: reconsider `egui`/`epaint` and `winit` here 108 | allowed_external_types = [ 109 | "egui::*", 110 | "epaint::*", 111 | "mint::*", 112 | "profiling::*", 113 | "serde::*", 114 | "winit::*", 115 | ] 116 | 117 | [lints.rust] 118 | unexpected_cfgs = { level = "allow", check-cfg = ['cfg(gles)'] } 119 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dzmitry Malyshau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /blade-asset/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blade-asset" 3 | version = "0.2.0" 4 | edition = "2021" 5 | description = "Asset manager for Blade" 6 | keywords = ["asset"] 7 | license = "MIT" 8 | repository = "https://github.com/kvark/blade" 9 | 10 | [lib] 11 | 12 | [dependencies] 13 | base64 = { workspace = true } 14 | bytemuck = { workspace = true } 15 | choir = { workspace = true } 16 | log = { workspace = true } 17 | profiling = { workspace = true } 18 | 19 | [package.metadata.cargo_check_external_types] 20 | allowed_external_types = ["choir::*"] 21 | -------------------------------------------------------------------------------- /blade-asset/README.md: -------------------------------------------------------------------------------- 1 | # Blade Asset Pipeline 2 | 3 | [![Docs](https://docs.rs/blade-asset/badge.svg)](https://docs.rs/blade-asset) 4 | [![Crates.io](https://img.shields.io/crates/v/blade-asset.svg?maxAge=2592000)](https://crates.io/crates/blade-asset) 5 | 6 | This is an abstract library for defininig the pipeline of asset processing. 7 | It's tightly integrated with [Choir](https://github.com/kvark/choir) but otherwise doesn't know about GPUs and such. 8 | 9 | Watch the [RustGamedev June 2023 talk](https://youtu.be/1DiA3OYqvqU) about it. See [the slides](https://hackmd.io/@kvark/blade-asset-pipeline#/). 10 | -------------------------------------------------------------------------------- /blade-asset/src/arena.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt, hash, 3 | marker::PhantomData, 4 | mem, 5 | num::NonZeroU8, 6 | ops, ptr, 7 | sync::{ 8 | atomic::{AtomicPtr, Ordering}, 9 | Mutex, 10 | }, 11 | }; 12 | 13 | #[repr(C)] 14 | #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Hash, Ord)] 15 | struct Address { 16 | index: u32, 17 | chunk: NonZeroU8, 18 | } 19 | 20 | pub struct Handle(Address, PhantomData); 21 | impl Clone for Handle { 22 | fn clone(&self) -> Self { 23 | Handle(self.0, PhantomData) 24 | } 25 | } 26 | impl Copy for Handle {} 27 | impl PartialEq for Handle { 28 | fn eq(&self, other: &Self) -> bool { 29 | self.0 == other.0 30 | } 31 | } 32 | impl hash::Hash for Handle { 33 | fn hash(&self, hasher: &mut H) { 34 | self.0.hash(hasher); 35 | self.1.hash(hasher); 36 | } 37 | } 38 | impl fmt::Debug for Handle { 39 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 40 | self.0.fmt(f) 41 | } 42 | } 43 | 44 | const MAX_CHUNKS: usize = 30; 45 | 46 | #[derive(Default)] 47 | struct FreeManager { 48 | chunk_bases: Vec<*mut [T]>, 49 | free_list: Vec
, 50 | } 51 | 52 | // These are safe beacuse members aren't exposed 53 | unsafe impl Send for FreeManager {} 54 | unsafe impl Sync for FreeManager {} 55 | 56 | pub struct Arena { 57 | min_size: usize, 58 | chunks: [AtomicPtr; MAX_CHUNKS], 59 | freeman: Mutex>, 60 | } 61 | 62 | impl ops::Index> for Arena { 63 | type Output = T; 64 | fn index(&self, handle: Handle) -> &T { 65 | let first_ptr = &self.chunks[handle.0.chunk.get() as usize].load(Ordering::Acquire); 66 | unsafe { &*first_ptr.add(handle.0.index as usize) } 67 | } 68 | } 69 | 70 | impl Arena { 71 | pub fn new(min_size: usize) -> Self { 72 | assert_ne!(min_size, 0); 73 | let dummy_data = Some(T::default()).into_iter().collect::>(); 74 | Self { 75 | min_size, 76 | chunks: Default::default(), 77 | freeman: Mutex::new(FreeManager { 78 | chunk_bases: vec![Box::into_raw(dummy_data)], 79 | free_list: Vec::new(), 80 | }), 81 | } 82 | } 83 | 84 | fn chunk_size(&self, chunk: NonZeroU8) -> usize { 85 | self.min_size << chunk.get() 86 | } 87 | 88 | pub fn alloc(&self, value: T) -> Handle { 89 | let mut freeman = self.freeman.lock().unwrap(); 90 | let (address, chunk_start) = match freeman.free_list.pop() { 91 | Some(address) => { 92 | let chunk_start = self.chunks[address.chunk.get() as usize].load(Ordering::Acquire); 93 | (address, chunk_start) 94 | } 95 | None => { 96 | let address = Address { 97 | index: 0, 98 | chunk: NonZeroU8::new(freeman.chunk_bases.len() as _).unwrap(), 99 | }; 100 | let size = self.chunk_size(address.chunk); 101 | let mut data = (0..size).map(|_| T::default()).collect::>(); 102 | let chunk_start: *mut T = data.first_mut().unwrap(); 103 | self.chunks[address.chunk.get() as usize].store(chunk_start, Ordering::Release); 104 | freeman.chunk_bases.push(Box::into_raw(data)); 105 | freeman 106 | .free_list 107 | .extend((1..size as u32).map(|index| Address { index, ..address })); 108 | (address, chunk_start) 109 | } 110 | }; 111 | unsafe { 112 | ptr::write(chunk_start.add(address.index as usize), value); 113 | } 114 | Handle(address, PhantomData) 115 | } 116 | 117 | pub fn alloc_default(&self) -> (Handle, *mut T) { 118 | let handle = self.alloc(T::default()); 119 | (handle, self.get_mut_ptr(handle)) 120 | } 121 | 122 | pub fn get_mut_ptr(&self, handle: Handle) -> *mut T { 123 | let first_ptr = &self.chunks[handle.0.chunk.get() as usize].load(Ordering::Acquire); 124 | unsafe { first_ptr.add(handle.0.index as usize) } 125 | } 126 | 127 | pub fn _dealloc(&self, handle: Handle) -> T { 128 | let mut freeman = self.freeman.lock().unwrap(); 129 | freeman.free_list.push(handle.0); 130 | let ptr = self.get_mut_ptr(handle); 131 | mem::take(unsafe { &mut *ptr }) 132 | } 133 | 134 | fn for_internal(&self, mut fun: impl FnMut(Address, *mut T)) { 135 | let mut freeman = self.freeman.lock().unwrap(); 136 | freeman.free_list.sort(); // enables fast search 137 | for (chunk_index, chunk_start) in self.chunks[..freeman.chunk_bases.len()] 138 | .iter() 139 | .enumerate() 140 | .skip(1) 141 | { 142 | let first_ptr = chunk_start.load(Ordering::Acquire); 143 | let chunk = NonZeroU8::new(chunk_index as _).unwrap(); 144 | for index in 0..self.chunk_size(chunk) { 145 | let address = Address { 146 | index: index as u32, 147 | chunk, 148 | }; 149 | if freeman.free_list.binary_search(&address).is_err() { 150 | //Note: accessing this is only safe if `get_mut_ptr` isn't called 151 | // for example, during hot reloading. 152 | fun(address, unsafe { first_ptr.add(index) }); 153 | } 154 | } 155 | } 156 | } 157 | 158 | pub fn for_each(&self, mut fun: impl FnMut(Handle, &T)) { 159 | self.for_internal(|address, ptr| fun(Handle(address, PhantomData), unsafe { &*ptr })) 160 | } 161 | 162 | pub fn dealloc_each(&self, mut fun: impl FnMut(Handle, T)) { 163 | self.for_internal(|address, ptr| { 164 | fun( 165 | Handle(address, PhantomData), 166 | mem::take(unsafe { &mut *ptr }), 167 | ) 168 | }) 169 | } 170 | } 171 | 172 | impl Drop for FreeManager { 173 | fn drop(&mut self) { 174 | for base in self.chunk_bases.drain(..) { 175 | let _ = unsafe { Box::from_raw(base) }; 176 | } 177 | } 178 | } 179 | 180 | // Ensure the `Option` doesn't have any overhead 181 | #[cfg(test)] 182 | unsafe fn _test_option_handle(handle: Handle) -> Option> { 183 | std::mem::transmute(handle) 184 | } 185 | 186 | #[test] 187 | fn test_single_thread() { 188 | let arena = Arena::::new(1); 189 | let _ = arena.alloc(3); 190 | let _ = arena.alloc(4); 191 | } 192 | -------------------------------------------------------------------------------- /blade-asset/src/flat.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, mem, num::NonZeroUsize, ptr, slice}; 2 | 3 | pub trait Flat { 4 | /// Type alignment, must be a power of two. 5 | const ALIGNMENT: usize; 6 | /// Size of the type, only if it's fixed. 7 | const FIXED_SIZE: Option; 8 | /// Size of the object. 9 | fn size(&self) -> usize { 10 | Self::FIXED_SIZE.unwrap().get() 11 | } 12 | /// Write self at the specified pointer. 13 | /// The pointer is guaranteed to be valid and aligned accordingly. 14 | /// 15 | /// # Safety 16 | /// Only safe if the available space in `ptr` is at least `self.size()` 17 | unsafe fn write(&self, ptr: *mut u8); 18 | /// Read self from the specified pointer. 19 | /// The pointer is guaranteed to be valid and aligned accordingly. 20 | /// 21 | /// # Safety 22 | /// Only safe when the `ptr` points to the same data as previously 23 | /// was written with [`Flat::write`]. 24 | unsafe fn read(ptr: *const u8) -> Self; 25 | } 26 | 27 | macro_rules! impl_basic { 28 | ($ty: ident) => { 29 | impl Flat for $ty { 30 | const ALIGNMENT: usize = mem::align_of::(); 31 | const FIXED_SIZE: Option = NonZeroUsize::new(mem::size_of::()); 32 | unsafe fn write(&self, ptr: *mut u8) { 33 | ptr::write(ptr as *mut Self, *self); 34 | } 35 | unsafe fn read(ptr: *const u8) -> Self { 36 | ptr::read(ptr as *const Self) 37 | } 38 | } 39 | }; 40 | } 41 | 42 | impl_basic!(bool); 43 | impl_basic!(u32); 44 | impl_basic!(u64); 45 | impl_basic!(usize); 46 | impl_basic!(f32); 47 | 48 | /* 49 | impl Flat for T { 50 | const ALIGNMENT: usize = mem::align_of::(); 51 | const FIXED_SIZE: Option = NonZeroUsize::new(mem::size_of::()); 52 | unsafe fn write(&self, ptr: *mut u8) { 53 | ptr::write(ptr as *mut T, *self); 54 | } 55 | unsafe fn read(ptr: *const u8) { 56 | ptr::read(ptr as *const T) 57 | } 58 | }*/ 59 | 60 | pub fn round_up(size: usize, alignment: usize) -> usize { 61 | (size + alignment - 1) & !(alignment - 1) 62 | } 63 | 64 | impl Flat for Vec { 65 | const ALIGNMENT: usize = T::ALIGNMENT; 66 | const FIXED_SIZE: Option = None; 67 | fn size(&self) -> usize { 68 | self.iter().fold(mem::size_of::(), |offset, item| { 69 | round_up(offset, T::ALIGNMENT) + item.size() 70 | }) 71 | } 72 | unsafe fn write(&self, ptr: *mut u8) { 73 | ptr::write(ptr as *mut usize, self.len()); 74 | let mut offset = mem::size_of::(); 75 | for item in self.iter() { 76 | offset = round_up(offset, T::ALIGNMENT); 77 | item.write(ptr.add(offset)); 78 | offset += item.size(); 79 | } 80 | } 81 | unsafe fn read(ptr: *const u8) -> Self { 82 | let counter = ptr::read(ptr as *const usize); 83 | let mut offset = mem::size_of::(); 84 | (0..counter) 85 | .map(|_| { 86 | offset = round_up(offset, T::ALIGNMENT); 87 | let value = T::read(ptr.add(offset)); 88 | offset += value.size(); 89 | value 90 | }) 91 | .collect() 92 | } 93 | } 94 | 95 | impl Flat for [T; C] { 96 | const ALIGNMENT: usize = mem::align_of::(); 97 | const FIXED_SIZE: Option = NonZeroUsize::new(mem::size_of::()); 98 | unsafe fn write(&self, ptr: *mut u8) { 99 | ptr::copy_nonoverlapping(self.as_ptr(), ptr as *mut T, C); 100 | } 101 | unsafe fn read(ptr: *const u8) -> Self { 102 | ptr::read(ptr as *const Self) 103 | } 104 | } 105 | 106 | impl<'a, T: bytemuck::Pod> Flat for &'a [T] { 107 | const ALIGNMENT: usize = mem::align_of::(); 108 | const FIXED_SIZE: Option = None; 109 | fn size(&self) -> usize { 110 | let elem_size = round_up(mem::size_of::(), mem::align_of::()); 111 | round_up(mem::size_of::(), mem::align_of::()) + elem_size * self.len() 112 | } 113 | unsafe fn write(&self, ptr: *mut u8) { 114 | ptr::write(ptr as *mut usize, self.len()); 115 | if !self.is_empty() { 116 | let offset = round_up(mem::size_of::(), mem::align_of::()); 117 | ptr::copy_nonoverlapping(self.as_ptr(), ptr.add(offset) as *mut T, self.len()); 118 | } 119 | } 120 | unsafe fn read(ptr: *const u8) -> Self { 121 | let counter = ptr::read(ptr as *const usize); 122 | if counter != 0 { 123 | let offset = round_up(mem::size_of::(), mem::align_of::()); 124 | slice::from_raw_parts(ptr.add(offset) as *const T, counter) 125 | } else { 126 | &[] 127 | } 128 | } 129 | } 130 | 131 | impl<'a, T: bytemuck::Pod> Flat for Cow<'a, [T]> { 132 | const ALIGNMENT: usize = mem::align_of::(); 133 | const FIXED_SIZE: Option = None; 134 | fn size(&self) -> usize { 135 | self.as_ref().size() 136 | } 137 | unsafe fn write(&self, ptr: *mut u8) { 138 | self.as_ref().write(ptr) 139 | } 140 | unsafe fn read(ptr: *const u8) -> Self { 141 | Cow::Borrowed(<&'a [T] as Flat>::read(ptr)) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /blade-asset/tests/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt, 3 | path::PathBuf, 4 | sync::{ 5 | atomic::{AtomicBool, Ordering}, 6 | Arc, 7 | }, 8 | }; 9 | 10 | struct Baker { 11 | allow_cooking: AtomicBool, 12 | } 13 | impl blade_asset::Baker for Baker { 14 | type Meta = u32; 15 | type Data<'a> = u32; 16 | type Output = usize; 17 | fn cook( 18 | &self, 19 | _source: &[u8], 20 | _extension: &str, 21 | meta: u32, 22 | cooker: Arc>, 23 | _exe_context: &choir::ExecutionContext, 24 | ) { 25 | assert!(self.allow_cooking.load(Ordering::SeqCst)); 26 | let _ = cooker.add_dependency("README.md".as_ref()); 27 | cooker.finish(meta); 28 | } 29 | fn serve(&self, cooked: u32, _exe_context: &choir::ExecutionContext) -> usize { 30 | cooked as usize 31 | } 32 | fn delete(&self, _output: usize) {} 33 | } 34 | 35 | #[test] 36 | fn test_asset() { 37 | let choir = choir::Choir::new(); 38 | let _w1 = choir.add_worker("main"); 39 | let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 40 | let am = blade_asset::AssetManager::::new( 41 | &root.join("cooked"), 42 | &choir, 43 | Baker { 44 | allow_cooking: AtomicBool::new(true), 45 | }, 46 | ); 47 | let meta = 5; 48 | let path = root.join("Cargo.toml"); 49 | let (handle, task) = am.load(&path, meta); 50 | task.join(); 51 | assert_eq!(am[handle], meta as usize); 52 | 53 | // now try to load it again and check that we aren't re-cooking 54 | am.baker.allow_cooking.store(false, Ordering::SeqCst); 55 | let (h, t) = am.load(&path, meta); 56 | assert_eq!(h, handle); 57 | t.join(); 58 | } 59 | 60 | fn flat_roundtrip(data: F) { 61 | let mut vec = vec![0u8; data.size()]; 62 | unsafe { data.write(vec.as_mut_ptr()) }; 63 | let other = unsafe { F::read(vec.as_ptr()) }; 64 | assert_eq!(data, other); 65 | } 66 | 67 | #[test] 68 | fn test_flatten() { 69 | flat_roundtrip([0u32, 1u32, 2u32]); 70 | flat_roundtrip(&[2u32, 4u32, 6u32][..]); 71 | flat_roundtrip(vec![1u32, 2, 3]); 72 | } 73 | -------------------------------------------------------------------------------- /blade-egui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blade-egui" 3 | version = "0.6.0" 4 | edition = "2021" 5 | description = "egui integration for Blade" 6 | keywords = ["graphics"] 7 | license = "MIT" 8 | repository = "https://github.com/kvark/blade" 9 | 10 | [lib] 11 | 12 | [dependencies] 13 | blade-graphics = { version = "0.6", path = "../blade-graphics" } 14 | blade-macros = { version = "0.3", path = "../blade-macros" } 15 | blade-util = { version = "0.2", path = "../blade-util" } 16 | egui = { workspace = true, features = ["bytemuck"] } 17 | bytemuck = { workspace = true } 18 | profiling = { workspace = true } 19 | 20 | [package.metadata.cargo_check_external_types] 21 | allowed_external_types = ["blade_graphics::*", "epaint::*"] 22 | -------------------------------------------------------------------------------- /blade-egui/README.md: -------------------------------------------------------------------------------- 1 | # Blade EGUI 2 | 3 | [![Docs](https://docs.rs/blade-egui/badge.svg)](https://docs.rs/blade-egui) 4 | [![Crates.io](https://img.shields.io/crates/v/blade-egui.svg?maxAge=2592000)](https://crates.io/crates/blade-egui) 5 | 6 | [EGUI](https://www.egui.rs/) support for [Blade-graphics](https://crates.io/crates/blade-graphics). 7 | 8 | ![scene editor](etc/scene-editor.jpg) 9 | 10 | ## Instructions 11 | 12 | Just the usual :crab: workflow. E.g. to run the bunny-mark benchmark run: 13 | ```bash 14 | cargo run --release --example bunnymark 15 | ``` 16 | 17 | ## Platforms 18 | 19 | The full-stack Blade Engine can only run on Vulkan with hardware Ray Tracing support. 20 | However, on secondary platforms, such as Metal and GLES/WebGL2, one can still use Blde-Graphics and Blade-Egui. 21 | -------------------------------------------------------------------------------- /blade-egui/etc/scene-editor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/blade/e0ec4e720957edd51b945b64dd85605ea54bcfe5/blade-egui/etc/scene-editor.jpg -------------------------------------------------------------------------------- /blade-egui/shader.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexOutput { 2 | @location(0) tex_coord: vec2, 3 | @location(1) color: vec4, 4 | @builtin(position) position: vec4, 5 | }; 6 | 7 | struct Uniforms { 8 | screen_size: vec2, 9 | convert_to_linear: f32, 10 | padding: f32, 11 | }; 12 | var r_uniforms: Uniforms; 13 | 14 | //Note: avoiding `vec2` in order to keep the scalar alignment 15 | struct Vertex { 16 | pos_x: f32, 17 | pos_y: f32, 18 | tex_coord_x: f32, 19 | tex_coord_y: f32, 20 | color: u32, 21 | } 22 | var r_vertex_data: array; 23 | 24 | fn linear_from_gamma(srgb: vec3) -> vec3 { 25 | let cutoff = srgb < vec3(0.04045); 26 | let lower = srgb / vec3(12.92); 27 | let higher = pow((srgb + vec3(0.055)) / vec3(1.055), vec3(2.4)); 28 | return select(higher, lower, cutoff); 29 | } 30 | 31 | @vertex 32 | fn vs_main( 33 | @builtin(vertex_index) v_index: u32, 34 | ) -> VertexOutput { 35 | let input = r_vertex_data[v_index]; 36 | var out: VertexOutput; 37 | out.tex_coord = vec2(input.tex_coord_x, input.tex_coord_y); 38 | out.color = unpack4x8unorm(input.color); 39 | out.position = vec4( 40 | 2.0 * input.pos_x / r_uniforms.screen_size.x - 1.0, 41 | 1.0 - 2.0 * input.pos_y / r_uniforms.screen_size.y, 42 | 0.0, 43 | 1.0, 44 | ); 45 | return out; 46 | } 47 | 48 | var r_texture: texture_2d; 49 | var r_sampler: sampler; 50 | 51 | @fragment 52 | fn fs_main(in: VertexOutput) -> @location(0) vec4 { 53 | //Note: we always assume rendering to linear color space, 54 | // but Egui wants to blend in gamma space, see 55 | // https://github.com/emilk/egui/pull/2071 56 | let blended = in.color * textureSample(r_texture, r_sampler, in.tex_coord); 57 | return vec4f(linear_from_gamma(blended.xyz), blended.a); 58 | } 59 | -------------------------------------------------------------------------------- /blade-graphics/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blade-graphics" 3 | version = "0.6.0" 4 | edition = "2021" 5 | description = "Graphics abstraction for Blade" 6 | keywords = ["graphics"] 7 | license = "MIT" 8 | repository = "https://github.com/kvark/blade" 9 | 10 | [lib] 11 | 12 | [dependencies] 13 | bitflags = { workspace = true } 14 | bytemuck = { workspace = true } 15 | codespan-reporting = "0.11" 16 | hidden-trait = "0.1" 17 | log = { workspace = true } 18 | mint = { workspace = true } 19 | naga = { workspace = true } 20 | raw-window-handle = "0.6" 21 | 22 | [target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] 23 | objc2 = "0.6" 24 | objc2-foundation = { version = "0.3", default-features = false, features = [ 25 | "std", 26 | "NSArray", 27 | ] } 28 | objc2-core-foundation = { version = "0.3", default-features = false, features = [ 29 | "CFCGTypes", 30 | ] } 31 | objc2-metal = { version = "0.3", default-features = false, features = [ 32 | "std", 33 | "MTLTypes", 34 | "MTLPixelFormat", 35 | "MTLResource", 36 | "MTLBuffer", 37 | "MTLTexture", 38 | "MTLSampler", 39 | "MTLDrawable", 40 | "MTLAccelerationStructure", 41 | "MTLAccelerationStructureTypes", 42 | "MTLCounters", 43 | "MTLLibrary", 44 | "MTLStageInputOutputDescriptor", 45 | "MTLComputePipeline", 46 | "MTLVertexDescriptor", 47 | "MTLDepthStencil", 48 | "MTLComputePipeline", 49 | "MTLRenderPipeline", 50 | "MTLCommandBuffer", 51 | "MTLCommandEncoder", 52 | "MTLAccelerationStructureCommandEncoder", 53 | "MTLBlitCommandEncoder", 54 | "MTLComputeCommandEncoder", 55 | "MTLRenderCommandEncoder", 56 | "MTLBlitPass", 57 | "MTLComputePass", 58 | "MTLRenderPass", 59 | "MTLCommandQueue", 60 | "MTLDevice", 61 | "MTLCaptureManager", 62 | "MTLCaptureScope", 63 | "block2", 64 | ] } 65 | objc2-quartz-core = { version = "0.3", default-features = false, features = [ 66 | "std", 67 | "objc2-metal", 68 | "objc2-core-foundation", 69 | "CALayer", 70 | "CAMetalLayer", 71 | ] } 72 | naga = { workspace = true, features = ["msl-out"] } 73 | 74 | [target.'cfg(target_os = "macos")'.dependencies] 75 | objc2-app-kit = { version = "0.3", default-features = false, features = [ 76 | "std", 77 | "objc2-quartz-core", 78 | "objc2-core-foundation", 79 | "NSResponder", 80 | "NSView", 81 | "NSWindow", 82 | ] } 83 | 84 | [target.'cfg(target_os = "ios")'.dependencies] 85 | objc2-ui-kit = { version = "0.3", default-features = false, features = [ 86 | "std", 87 | "objc2-quartz-core", 88 | "objc2-core-foundation", 89 | "UIResponder", 90 | "UIView", 91 | "UIWindow", 92 | "UIScene", 93 | "UIWindowScene", 94 | "UIScreen", 95 | ] } 96 | 97 | [target.'cfg(any(vulkan, windows, target_os = "linux", target_os = "android", target_os = "freebsd"))'.dependencies] 98 | ash = "0.38" 99 | ash-window = "0.13" 100 | gpu-alloc = "0.6" 101 | gpu-alloc-ash = "0.7" 102 | naga = { workspace = true, features = ["spv-out"] } 103 | slab = { workspace = true } 104 | 105 | [target.'cfg(any(gles, target_arch = "wasm32"))'.dependencies] 106 | glow = "0.14" 107 | naga = { workspace = true, features = ["glsl-out"] } 108 | 109 | [target.'cfg(all(gles, not(target_arch = "wasm32")))'.dependencies] 110 | egl = { package = "khronos-egl", version = "6.0", features = ["dynamic"] } 111 | libloading = { version = "0.8" } 112 | 113 | [target.'cfg(all(target_arch = "wasm32"))'.dependencies] 114 | wasm-bindgen = "0.2.83" 115 | web-sys = { workspace = true, features = [ 116 | "HtmlCanvasElement", 117 | "WebGl2RenderingContext", 118 | ] } 119 | js-sys = "0.3.60" 120 | 121 | [package.metadata.cargo_check_external_types] 122 | allowed_external_types = [ 123 | "bitflags::*", 124 | "mint::*", 125 | "naga::*", 126 | "raw_window_handle::*", 127 | ] 128 | 129 | [lints.rust] 130 | unexpected_cfgs = { level = "allow", check-cfg = ['cfg(gles)'] } 131 | -------------------------------------------------------------------------------- /blade-graphics/README.md: -------------------------------------------------------------------------------- 1 | # Blade Graphics 2 | 3 | [![Docs](https://docs.rs/blade-graphics/badge.svg)](https://docs.rs/blade-graphics) 4 | [![Crates.io](https://img.shields.io/crates/v/blade-graphics.svg?maxAge=2592000)](https://crates.io/crates/blade-graphics) 5 | 6 | Blade-graphics is a lean and mean [GPU abstraction](https://youtu.be/63dnzjw4azI?t=623) aimed at ergonomics and fun. See [motivation](etc/motivation.md), [FAQ](etc/FAQ.md), and [performance](etc/performance.md) for details. 7 | 8 | ## Examples 9 | 10 | ![ray-query example](etc/ray-query.gif) 11 | ![particles example](etc/particles.png) 12 | 13 | ## Platforms 14 | 15 | The backend is selected automatically based on the host platform: 16 | - *Vulkan* on desktop Linux, Windows, and Android 17 | - *Metal* on desktop macOS, and iOS 18 | - *OpenGL ES3* on the Web 19 | 20 | | Feature | Vulkan | Metal | GLES | 21 | | ------- | ------ | ----- | ---- | 22 | | compute | :white_check_mark: | :white_check_mark: | | 23 | | ray tracing | :white_check_mark: | | | 24 | 25 | ### Vulkan 26 | 27 | Required instance extensions: 28 | - VK_EXT_debug_utils 29 | - VK_KHR_get_physical_device_properties2 30 | - VK_KHR_get_surface_capabilities2 31 | 32 | Required device extensions: 33 | - VK_EXT_inline_uniform_block 34 | - VK_KHR_descriptor_update_template 35 | - VK_KHR_timeline_semaphore 36 | - VK_KHR_dynamic_rendering 37 | 38 | Conceptually, Blade requires the baseline Vulkan hardware with a relatively fresh driver. 39 | All of these required extensions are supported in software by the driver on any underlying architecture. 40 | 41 | ### OpenGL ES 42 | 43 | GLES is also supported at a basic level. It's enabled for `wasm32-unknown-unknown` target, and can also be force-enabled on native: 44 | ```bash 45 | RUSTFLAGS="--cfg gles" CARGO_TARGET_DIR=./target-gl cargo run --example bunnymark 46 | ``` 47 | 48 | This path can be activated on all platforms via Angle library. 49 | For example, on macOS it's sufficient to place `libEGL.dylib` and `libGLESv2.dylib` in the working directory. 50 | 51 | On Windows, the quotes aren't expected: 52 | ```bash 53 | set RUSTFLAGS=--cfg gles 54 | ``` 55 | 56 | ### WebGL2 57 | 58 | Following command will start a web server offering the `bunnymark` example: 59 | ```bash 60 | cargo run-wasm --example bunnymark 61 | ``` 62 | 63 | ### Vulkan Portability 64 | 65 | First, ensure to load the environment from the Vulkan SDK: 66 | ```bash 67 | cd /opt/VulkanSDK && source setup-env.sh 68 | ``` 69 | 70 | Vulkan backend can be forced on using "vulkan" config flag. Example invocation that produces a vulkan (portability) build into another target folder: 71 | ```bash 72 | RUSTFLAGS="--cfg vulkan" CARGO_TARGET_DIR=./target-vk cargo test 73 | ``` 74 | -------------------------------------------------------------------------------- /blade-graphics/etc/FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequency Asked Questions 2 | 3 | ## When should I *not* use Blade? 4 | 5 | - When you *target the Web*. Blade currently has no Web backends supported. Targeting WebGPU is desired, but will not be as performant as native. 6 | - Similarly, when you target the *low-end GPUs* or old drivers. Blade has no OpenGL/D3D11 support, and it requires fresh drivers on Vulkan. 7 | - When you render with 10K or *more draw calls*. State switching has overhead with Blade, and it is lower in GPU abstractions/libraries that have barriers and explicit bind groups. 8 | - When you need something *off the shelf*. Blade is experimental and young, it assumes you'll be customizing it. 9 | 10 | ## Why investing into this when there is `wgpu`? 11 | 12 | `wgpu` is becoming a standard solution for GPU access in Rust and beyond. It's wonderful, and by any means just use it if you have any doubts. It's a strong local maxima in a chosen space of low-level portability. It may very well be the global maxima as well, but we don't know this until we explore the *other* local maximas. Blade is an attempt to strike where `wgpu` can't reach, it makes a lot of the opposite design solutions. Try it and see. 13 | 14 | ## Isn't this going to be slow? 15 | 16 | Blade creating a descriptor set (in Vulkan) for each draw call. It doesn't care about pipeline compatibility to preserve the bindings. How is this fast? 17 | 18 | Short answer is - yes, it's unlikely going to be faster than wgpu-hal. Long answer is - slow doesn't matter here. 19 | 20 | Take a look at Vulkan [performance](performance.md) numbers. wgpu-hal can get 60K bunnies on a slow machine, which is pretty much the maximum. Both wgpu and blade can reach about 20K. Honestly, if you are relying on 20K unique draw calls being fast, you are in a strange place. Generally, developers should switch to instancing or other batching methods whenever the object count grows above 100, not to mention a 1000. 21 | 22 | Similar reasoning goes to pipeline switches. If you are relying on many pipeline switches done efficiently, then it's good to reconsider your shaders, perhaps turning into the megashader alley a bit. In D3D12, a pipeline change requires all resources to be rebound anyway (and this is what wgpu-hal/dx12 does regardless of the pipeline compatibility), so this is fine in Blade. 23 | -------------------------------------------------------------------------------- /blade-graphics/etc/motivation.md: -------------------------------------------------------------------------------- 1 | # Motivation 2 | 3 | ## Goal 4 | 5 | Have a layer for graphics programming for those who know what they are doing, and who wants to get the stuff working fast. It's highly opinionated and ergonomic, but also designed specifically for mid to high range hardware and modern APIs. Today, the alternatives are either too high level (engines), too verbose (APIs directly), or just overly general. 6 | 7 | Opinionated means the programming model is very limited. But if something is written against this model, we want to guarantee that it's going to run very efficient, more efficient than any of the more general alternatives would do. 8 | 9 | This is basically a near-perfect graphics layer for myself, which I'd be happy to use on my projects. I hope it can be useful to others, too. 10 | 11 | ## Alternatives 12 | 13 | *wgpu* provides the most thorough graphics abstraction in Rust ecosystem. The main API is portable over pretty much all the (open) platforms, including the Web. However, it is very restricted (by being a least common denominator of the platforms), fairly verbose (possible to write against it directly, but not quite convenient), and has overhead (for safety and portability). 14 | 15 | *wgpu-hal* provides an unsafe portable layer, which has virtually no overhead. The point about verbosity still applies. It's possible to write a more ergonomic layer on top of wgpu-hal, but one can't cut the corners embedded in wgpu-hal's design. For example, wgpu-hal expects resource states to be tracked by the user and changed (on a command encoder) explicitly. 16 | 17 | *rafx* attempts to offer a good vertically integrated engine with multiple backends. *rafx* itself is too high level, while *rafx-api* is too low level and verbose. 18 | 19 | *sierra* abstracts over Vulkan. It has great ergonomic features (some expressed via procedural macros). Essentially it has the same problem (for the purpose of fitting our goal) - choice is between low level overly generic API and a high-level one (*arcana*). 20 | 21 | Finally, we don't consider GL-based abstractions, such as *luminance*, since the API is largely outdated. 22 | 23 | # Design 24 | 25 | The API is supposed to be minimal, targeting the capabilities of mid to high range machines on popular platforms. It's also totally unsafe, assuming the developer knows what they are doing. We realy on native API validation to assist developers. 26 | 27 | ## Compromises 28 | 29 | *Object lifetime* is explicit, no automatic tracking is done. This is similar to most of the alternatives. 30 | 31 | *Object memory* is automatically allocated based on a few profiles. 32 | 33 | Basic *resources*, such buffers and textures, are small `Copy` structs. 34 | 35 | *Resource states* do not exist. The API is built on an assumption that the driver knows better how to track resource states, and so our API doesn't need to care about this. The only command exposed is a catch-all barrier. 36 | 37 | *Bindings* are pushed directly to command encoders. This is similar to Metal Argument Buffers. There are no descriptor sets or pools. You take a structure and push it to the state. This structure includes any uniform data directly. Changing a pipeline invalidates all bindings, just like in DX12. 38 | 39 | In addition, several features may be added late or not added at all for the sake of keeping everything simple: 40 | 41 | - vertex buffers (use storage buffers instead) 42 | - multisampling (too expensive) 43 | 44 | ## Backends 45 | 46 | At first, the API should run on Vulkan and Metal. There is no DX12 support planned. 47 | 48 | On Metal side we want to take advantage of the argument buffers if available. 49 | 50 | On Vulkan we'll require certain features to make the translation simple: 51 | 52 | - [VK_KHR_push_descriptor](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_KHR_push_descriptor.html) 53 | - [VK_KHR_descriptor_update_template](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_KHR_descriptor_update_template.html) 54 | - [VK_EXT_inline_uniform_block](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_EXT_inline_uniform_block.html) 55 | - [VK_KHR_dynamic_rendering](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_KHR_dynamic_rendering.html) 56 | 57 | ## Assumptions 58 | 59 | Blade is based on different set of assumptions from wgpu-hal: 60 | - *safety*: wgpu places safety first and foremost. Self-sufficient, guarantees no UB. Blade is on the opposite - considers safety to be secondary. Expects users to rely on native API's validation and tooling. 61 | - *API reach*: wgpu attempts to be everywhere, having backends for all the APIs it can reach. Blade targets only the essential backends: Vulkan and Metal. 62 | - *abstraction*: wgpu is completely opaque, with only a few unsafe APIs for interacting with external objects. Blade needs to be transparent, since it assumes modifcation by the user, and doesn't provide safety. 63 | - *errors*: wgpu considers all external errors recoverable. Blade doesn't expect any recovery after the initialization is done. 64 | - *object copy*: wgpu-hal hides API objects so that they can only be `Clone`, and some of the backends use `Arc` and other heap-allocated backing for them. Blade keeps the API for resources to be are light as possible and allows them to be copied freely. 65 | - *bind group creation cost*: wgpu considers it expensive, needs to be prepared ahead of time. Blade considers it cheap enough to always create on the fly. 66 | | bind group invalidation | should be avoided by following pipeline compatibility rules | everything is re-bound on pipeline change | 67 | - *barriers*: wgpu attempts to always use the optimal image layouts and can set reduced access flags on resources based on use. Placing the barriers optimally is a non-trivial task to solve, no universal solutions. Blade not only ignores this fight by making the user place the barrier, these barriers are only global, and there are no image layout changes - everything is GENERAL. 68 | - *usage*: wgpu expects to be used as a Rust library. Blade expects to be vendored in and modified according to the needs of a user. Hopefully, some of the changes would appear upstream as PRs. 69 | 70 | In other words, this is a bit **experiment**. It may fail horribly, or it may open up new ideas and perspectives. 71 | -------------------------------------------------------------------------------- /blade-graphics/etc/particles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/blade/e0ec4e720957edd51b945b64dd85605ea54bcfe5/blade-graphics/etc/particles.png -------------------------------------------------------------------------------- /blade-graphics/etc/performance.md: -------------------------------------------------------------------------------- 1 | # Performance 2 | 3 | Blade doesn't expect to be faster than wgpu-hal, but it's important to understand how much the difference is. Testing is done on "bunnymark" example, which is ported from wgpu. Since every draw call is dynamic in Blade, this benchmark is the worst case of the usage. 4 | 5 | ## MacBook Pro 2016 6 | 7 | GPU: Intel Iris Graphics 550 8 | 9 | Metal: 10 | - Blade starts to slow down after about 23K bunnies 11 | - wgpu-hal starts at 60K bunnies 12 | - wgpu starts at 15K bunnies 13 | 14 | Vulkan Portability: 15 | - Blade starts to slow down at around 18K bunnies 16 | 17 | ## Thinkpad T495s 18 | 19 | GPU: Ryzen 3500U 20 | 21 | Windows/Vulkan: 22 | - Blade starts at around 18K bunnies 23 | - wgpu-hal starts at 60K bunnies 24 | - wgpu starts at 20K bunnies 25 | 26 | ## Thinkpad Z13 gen1 27 | 28 | GPU: Ryzen 6850U 29 | 30 | Windows/Vulkan: 31 | - Blade starts at around 50K bunnies 32 | - wgpu-hal starts at 50K bunnies (also GPU-limited) 33 | - wgpu starts at around 15K bunnies 34 | 35 | ## Conclusions 36 | 37 | Amazingly, Blade performance in the worst case scenario is on par with wgpu (but still far from wgpu-hal). This is the best outcome we could hope for. 38 | 39 | As expected, Vulkan path on macOS via MoltenVK is slower than the native Metal backend. 40 | 41 | Ergonomically, our example is 335 LOC versus 830 LOC of wgpu-hal and 370-750 LOC in wgpu (depending on how we count the example framework). 42 | 43 | It's also closer to the hardware (than even wgpu-hal) and easier to debug. 44 | -------------------------------------------------------------------------------- /blade-graphics/etc/ray-query.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/blade/e0ec4e720957edd51b945b64dd85605ea54bcfe5/blade-graphics/etc/ray-query.gif -------------------------------------------------------------------------------- /blade-graphics/src/derive.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | use super::{ResourceIndex, ShaderBinding, VertexFormat}; 4 | 5 | pub trait HasShaderBinding { 6 | const TYPE: ShaderBinding; 7 | } 8 | impl HasShaderBinding for T { 9 | const TYPE: ShaderBinding = ShaderBinding::Plain { 10 | size: mem::size_of::() as u32, 11 | }; 12 | } 13 | impl HasShaderBinding for super::TextureView { 14 | const TYPE: ShaderBinding = ShaderBinding::Texture; 15 | } 16 | impl HasShaderBinding for super::Sampler { 17 | const TYPE: ShaderBinding = ShaderBinding::Sampler; 18 | } 19 | impl HasShaderBinding for super::BufferPiece { 20 | const TYPE: ShaderBinding = ShaderBinding::Buffer; 21 | } 22 | impl<'a, const N: ResourceIndex> HasShaderBinding for &'a super::BufferArray { 23 | const TYPE: ShaderBinding = ShaderBinding::BufferArray { count: N }; 24 | } 25 | impl<'a, const N: ResourceIndex> HasShaderBinding for &'a super::TextureArray { 26 | const TYPE: ShaderBinding = ShaderBinding::TextureArray { count: N }; 27 | } 28 | impl HasShaderBinding for super::AccelerationStructure { 29 | const TYPE: ShaderBinding = ShaderBinding::AccelerationStructure; 30 | } 31 | 32 | pub trait HasVertexAttribute { 33 | const FORMAT: VertexFormat; 34 | } 35 | 36 | impl HasVertexAttribute for f32 { 37 | const FORMAT: VertexFormat = VertexFormat::F32; 38 | } 39 | impl HasVertexAttribute for [f32; 2] { 40 | const FORMAT: VertexFormat = VertexFormat::F32Vec2; 41 | } 42 | impl HasVertexAttribute for [f32; 3] { 43 | const FORMAT: VertexFormat = VertexFormat::F32Vec3; 44 | } 45 | impl HasVertexAttribute for [f32; 4] { 46 | const FORMAT: VertexFormat = VertexFormat::F32Vec4; 47 | } 48 | impl HasVertexAttribute for u32 { 49 | const FORMAT: VertexFormat = VertexFormat::U32; 50 | } 51 | impl HasVertexAttribute for [u32; 2] { 52 | const FORMAT: VertexFormat = VertexFormat::U32Vec2; 53 | } 54 | impl HasVertexAttribute for [u32; 3] { 55 | const FORMAT: VertexFormat = VertexFormat::U32Vec3; 56 | } 57 | impl HasVertexAttribute for [u32; 4] { 58 | const FORMAT: VertexFormat = VertexFormat::U32Vec4; 59 | } 60 | impl HasVertexAttribute for i32 { 61 | const FORMAT: VertexFormat = VertexFormat::I32; 62 | } 63 | impl HasVertexAttribute for [i32; 2] { 64 | const FORMAT: VertexFormat = VertexFormat::I32Vec2; 65 | } 66 | impl HasVertexAttribute for [i32; 3] { 67 | const FORMAT: VertexFormat = VertexFormat::I32Vec3; 68 | } 69 | impl HasVertexAttribute for [i32; 4] { 70 | const FORMAT: VertexFormat = VertexFormat::I32Vec4; 71 | } 72 | 73 | impl HasVertexAttribute for mint::Vector2 { 74 | const FORMAT: VertexFormat = VertexFormat::F32Vec2; 75 | } 76 | impl HasVertexAttribute for mint::Vector3 { 77 | const FORMAT: VertexFormat = VertexFormat::F32Vec3; 78 | } 79 | impl HasVertexAttribute for mint::Vector4 { 80 | const FORMAT: VertexFormat = VertexFormat::F32Vec4; 81 | } 82 | impl HasVertexAttribute for mint::Vector2 { 83 | const FORMAT: VertexFormat = VertexFormat::U32Vec2; 84 | } 85 | impl HasVertexAttribute for mint::Vector3 { 86 | const FORMAT: VertexFormat = VertexFormat::U32Vec3; 87 | } 88 | impl HasVertexAttribute for mint::Vector4 { 89 | const FORMAT: VertexFormat = VertexFormat::U32Vec4; 90 | } 91 | impl HasVertexAttribute for mint::Vector2 { 92 | const FORMAT: VertexFormat = VertexFormat::I32Vec2; 93 | } 94 | impl HasVertexAttribute for mint::Vector3 { 95 | const FORMAT: VertexFormat = VertexFormat::I32Vec3; 96 | } 97 | impl HasVertexAttribute for mint::Vector4 { 98 | const FORMAT: VertexFormat = VertexFormat::I32Vec4; 99 | } 100 | -------------------------------------------------------------------------------- /blade-graphics/src/gles/web.rs: -------------------------------------------------------------------------------- 1 | use glow::HasContext as _; 2 | use wasm_bindgen::JsCast; 3 | 4 | pub struct PlatformContext { 5 | #[allow(unused)] 6 | webgl2: web_sys::WebGl2RenderingContext, 7 | glow: glow::Context, 8 | } 9 | 10 | pub struct PlatformSurface { 11 | info: crate::SurfaceInfo, 12 | extent: crate::Extent, 13 | } 14 | #[derive(Debug)] 15 | pub struct PlatformFrame { 16 | framebuf: glow::Framebuffer, 17 | extent: crate::Extent, 18 | } 19 | 20 | pub type PlatformError = (); 21 | 22 | impl super::Surface { 23 | pub fn info(&self) -> crate::SurfaceInfo { 24 | self.platform.info 25 | } 26 | pub fn acquire_frame(&self) -> super::Frame { 27 | let size = self.platform.extent; 28 | super::Frame { 29 | platform: PlatformFrame { 30 | framebuf: self.framebuf, 31 | extent: self.platform.extent, 32 | }, 33 | texture: super::Texture { 34 | inner: super::TextureInner::Renderbuffer { 35 | raw: self.renderbuf, 36 | }, 37 | target_size: [size.width as u16, size.height as u16], 38 | format: self.platform.info.format, 39 | }, 40 | } 41 | } 42 | } 43 | 44 | impl PlatformContext { 45 | pub(super) fn present(&self, frame: PlatformFrame) { 46 | unsafe { 47 | super::present_blit(&self.glow, frame.framebuf, frame.extent); 48 | } 49 | } 50 | } 51 | 52 | impl super::Context { 53 | pub unsafe fn init(_desc: crate::ContextDesc) -> Result { 54 | let canvas = web_sys::window() 55 | .and_then(|win| win.document()) 56 | .expect("Cannot get document") 57 | .get_element_by_id("blade") 58 | .expect("Canvas is not found") 59 | .dyn_into::() 60 | .expect("Failed to downcast to canvas type"); 61 | 62 | let context_options = js_sys::Object::new(); 63 | js_sys::Reflect::set( 64 | &context_options, 65 | &"antialias".into(), 66 | &wasm_bindgen::JsValue::FALSE, 67 | ) 68 | .expect("Cannot create context options"); 69 | //Note: could also set: "alpha", "premultipliedAlpha" 70 | 71 | let webgl2 = canvas 72 | .get_context_with_context_options("webgl2", &context_options) 73 | .expect("Cannot create WebGL2 context") 74 | .and_then(|context| context.dyn_into::().ok()) 75 | .expect("Cannot convert into WebGL2 context"); 76 | 77 | let glow = glow::Context::from_webgl2_context(webgl2.clone()); 78 | 79 | let capabilities = super::Capabilities::empty(); 80 | let limits = super::Limits { 81 | uniform_buffer_alignment: unsafe { 82 | glow.get_parameter_i32(glow::UNIFORM_BUFFER_OFFSET_ALIGNMENT) as u32 83 | }, 84 | }; 85 | let device_information = crate::DeviceInformation { 86 | is_software_emulated: false, 87 | device_name: glow.get_parameter_string(glow::VENDOR), 88 | driver_name: glow.get_parameter_string(glow::RENDERER), 89 | driver_info: glow.get_parameter_string(glow::VERSION), 90 | }; 91 | 92 | Ok(super::Context { 93 | platform: PlatformContext { webgl2, glow }, 94 | capabilities, 95 | toggles: super::Toggles::default(), 96 | limits, 97 | device_information, 98 | }) 99 | } 100 | 101 | pub fn create_surface( 102 | &self, 103 | _window: &I, 104 | ) -> Result { 105 | let platform = PlatformSurface { 106 | info: crate::SurfaceInfo { 107 | format: crate::TextureFormat::Rgba8Unorm, 108 | alpha: crate::AlphaMode::PreMultiplied, 109 | }, 110 | extent: crate::Extent::default(), 111 | }; 112 | Ok(unsafe { 113 | super::Surface { 114 | platform, 115 | renderbuf: self.platform.glow.create_renderbuffer().unwrap(), 116 | framebuf: self.platform.glow.create_framebuffer().unwrap(), 117 | } 118 | }) 119 | } 120 | 121 | pub fn destroy_surface(&self, _surface: &mut super::Surface) {} 122 | 123 | pub fn reconfigure_surface(&self, surface: &mut super::Surface, config: crate::SurfaceConfig) { 124 | //TODO: create WebGL context here 125 | let format_desc = super::describe_texture_format(surface.platform.info.format); 126 | let gl = &self.platform.glow; 127 | //Note: this code can be shared with EGL 128 | unsafe { 129 | gl.bind_renderbuffer(glow::RENDERBUFFER, Some(surface.renderbuf)); 130 | gl.renderbuffer_storage( 131 | glow::RENDERBUFFER, 132 | format_desc.internal, 133 | config.size.width as _, 134 | config.size.height as _, 135 | ); 136 | gl.bind_framebuffer(glow::READ_FRAMEBUFFER, Some(surface.framebuf)); 137 | gl.framebuffer_renderbuffer( 138 | glow::READ_FRAMEBUFFER, 139 | glow::COLOR_ATTACHMENT0, 140 | glow::RENDERBUFFER, 141 | Some(surface.renderbuf), 142 | ); 143 | gl.bind_renderbuffer(glow::RENDERBUFFER, None); 144 | } 145 | surface.platform.extent = config.size; 146 | } 147 | 148 | /// Obtain a lock to the EGL context and get handle to the [`glow::Context`] that can be used to 149 | /// do rendering. 150 | pub(super) fn lock(&self) -> &glow::Context { 151 | &self.platform.glow 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /blade-graphics/src/metal/surface.rs: -------------------------------------------------------------------------------- 1 | use objc2::rc::Retained; 2 | use objc2::ClassType; 3 | use objc2_core_foundation::CGSize; 4 | use objc2_quartz_core::CAMetalLayer; 5 | 6 | const SURFACE_INFO: crate::SurfaceInfo = crate::SurfaceInfo { 7 | format: crate::TextureFormat::Rgba8Unorm, 8 | alpha: crate::AlphaMode::Ignored, 9 | }; 10 | 11 | impl super::Surface { 12 | /// Get the CALayerMetal for this surface, if any. 13 | /// This is platform specific API. 14 | pub fn metal_layer(&self) -> Retained { 15 | self.render_layer.clone() 16 | } 17 | 18 | pub fn info(&self) -> crate::SurfaceInfo { 19 | self.info 20 | } 21 | 22 | pub fn acquire_frame(&self) -> super::Frame { 23 | use objc2_quartz_core::CAMetalDrawable as _; 24 | let (drawable, texture) = objc2::rc::autoreleasepool(|_| unsafe { 25 | let drawable = self.render_layer.nextDrawable().unwrap(); 26 | let texture = drawable.texture(); 27 | (Retained::cast_unchecked(drawable), texture) 28 | }); 29 | super::Frame { drawable, texture } 30 | } 31 | } 32 | 33 | impl super::Context { 34 | pub fn create_surface( 35 | &self, 36 | window: &I, 37 | ) -> Result { 38 | use objc2_foundation::NSObjectProtocol as _; 39 | 40 | Ok(match window.window_handle().unwrap().as_raw() { 41 | #[cfg(target_os = "ios")] 42 | raw_window_handle::RawWindowHandle::UiKit(handle) => unsafe { 43 | let view = 44 | Retained::retain(handle.ui_view.as_ptr() as *mut objc2_ui_kit::UIView).unwrap(); 45 | let main_layer = view.layer(); 46 | let render_layer = if main_layer.isKindOfClass(&CAMetalLayer::class()) { 47 | Retained::cast_unchecked(main_layer) 48 | } else { 49 | use objc2_ui_kit::UIViewAutoresizing as Var; 50 | let new_layer = CAMetalLayer::new(); 51 | new_layer.setFrame(main_layer.frame()); 52 | // Unlike NSView, UIView does not allow to replace main layer. 53 | main_layer.addSublayer(&new_layer); 54 | view.setAutoresizingMask( 55 | Var::FlexibleLeftMargin 56 | | Var::FlexibleWidth 57 | | Var::FlexibleRightMargin 58 | | Var::FlexibleTopMargin 59 | | Var::FlexibleHeight 60 | | Var::FlexibleBottomMargin, 61 | ); 62 | if let Some(scene) = view.window().and_then(|w| w.windowScene()) { 63 | new_layer.setContentsScale(scene.screen().nativeScale()); 64 | } 65 | new_layer 66 | }; 67 | super::Surface { 68 | view: Some(Retained::cast_unchecked(view)), 69 | render_layer, 70 | info: SURFACE_INFO, 71 | } 72 | }, 73 | #[cfg(target_os = "macos")] 74 | raw_window_handle::RawWindowHandle::AppKit(handle) => unsafe { 75 | let view = Retained::retain(handle.ns_view.as_ptr() as *mut objc2_app_kit::NSView) 76 | .unwrap(); 77 | let main_layer = view.layer(); 78 | let render_layer = if main_layer 79 | .as_ref() 80 | .map_or(false, |layer| layer.isKindOfClass(&CAMetalLayer::class())) 81 | { 82 | Retained::cast_unchecked(main_layer.unwrap()) 83 | } else { 84 | let new_layer = CAMetalLayer::new(); 85 | if let Some(layer) = main_layer { 86 | new_layer.setFrame(layer.frame()); 87 | } 88 | view.setLayer(Some(&new_layer)); 89 | view.setWantsLayer(true); 90 | new_layer.setContentsGravity(objc2_quartz_core::kCAGravityTopLeft); 91 | if let Some(window) = view.window() { 92 | new_layer.setContentsScale(window.backingScaleFactor()); 93 | } 94 | new_layer 95 | }; 96 | super::Surface { 97 | view: Some(view.downcast().unwrap()), 98 | render_layer, 99 | info: SURFACE_INFO, 100 | } 101 | }, 102 | _ => return Err(crate::NotSupportedError::PlatformNotSupported), 103 | }) 104 | } 105 | 106 | pub fn destroy_surface(&self, surface: &mut super::Surface) { 107 | surface.view = None; 108 | } 109 | 110 | pub fn reconfigure_surface(&self, surface: &mut super::Surface, config: crate::SurfaceConfig) { 111 | let device = self.device.lock().unwrap(); 112 | surface.info = crate::SurfaceInfo { 113 | format: match config.color_space { 114 | crate::ColorSpace::Linear => crate::TextureFormat::Bgra8UnormSrgb, 115 | crate::ColorSpace::Srgb => crate::TextureFormat::Bgra8Unorm, 116 | }, 117 | alpha: if config.transparent { 118 | crate::AlphaMode::PostMultiplied 119 | } else { 120 | //Warning: it's not really ignored! Instead, it's assumed to be 1: 121 | // https://developer.apple.com/documentation/quartzcore/calayer/1410763-isopaque 122 | crate::AlphaMode::Ignored 123 | }, 124 | }; 125 | let vsync = match config.display_sync { 126 | crate::DisplaySync::Block => true, 127 | crate::DisplaySync::Recent | crate::DisplaySync::Tear => false, 128 | }; 129 | 130 | unsafe { 131 | surface.render_layer.setOpaque(!config.transparent); 132 | surface.render_layer.setDevice(Some(device.as_ref())); 133 | surface 134 | .render_layer 135 | .setPixelFormat(super::map_texture_format(surface.info.format)); 136 | surface 137 | .render_layer 138 | .setFramebufferOnly(config.usage == crate::TextureUsage::TARGET); 139 | surface.render_layer.setMaximumDrawableCount(3); 140 | surface.render_layer.setDrawableSize(CGSize { 141 | width: config.size.width as f64, 142 | height: config.size.height as f64, 143 | }); 144 | surface.render_layer.setDisplaySyncEnabled(vsync); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /blade-graphics/src/traits.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, hash::Hash}; 2 | 3 | pub trait ResourceDevice { 4 | type Buffer: Send + Sync + Clone + Copy + Debug + Hash + PartialEq; 5 | type Texture: Send + Sync + Clone + Copy + Debug + Hash + PartialEq; 6 | type TextureView: Send + Sync + Clone + Copy + Debug + Hash + PartialEq; 7 | type Sampler: Send + Sync + Clone + Copy + Debug + Hash + PartialEq; 8 | type AccelerationStructure: Send + Sync + Clone + Copy + Debug + Hash + PartialEq; 9 | 10 | fn create_buffer(&self, desc: super::BufferDesc) -> Self::Buffer; 11 | fn sync_buffer(&self, buffer: Self::Buffer); 12 | fn destroy_buffer(&self, buffer: Self::Buffer); 13 | fn create_texture(&self, desc: super::TextureDesc) -> Self::Texture; 14 | fn destroy_texture(&self, texture: Self::Texture); 15 | fn create_texture_view( 16 | &self, 17 | texture: Self::Texture, 18 | desc: super::TextureViewDesc, 19 | ) -> Self::TextureView; 20 | fn destroy_texture_view(&self, view: Self::TextureView); 21 | fn create_sampler(&self, desc: super::SamplerDesc) -> Self::Sampler; 22 | fn destroy_sampler(&self, sampler: Self::Sampler); 23 | fn create_acceleration_structure( 24 | &self, 25 | desc: super::AccelerationStructureDesc, 26 | ) -> Self::AccelerationStructure; 27 | fn destroy_acceleration_structure(&self, acceleration_structure: Self::AccelerationStructure); 28 | } 29 | 30 | pub trait ShaderDevice { 31 | type ComputePipeline: Send + Sync; 32 | type RenderPipeline: Send + Sync; 33 | 34 | fn create_compute_pipeline(&self, desc: super::ComputePipelineDesc) -> Self::ComputePipeline; 35 | fn destroy_compute_pipeline(&self, pipeline: &mut Self::ComputePipeline); 36 | fn create_render_pipeline(&self, desc: super::RenderPipelineDesc) -> Self::RenderPipeline; 37 | fn destroy_render_pipeline(&self, pipeline: &mut Self::RenderPipeline); 38 | } 39 | 40 | pub trait CommandDevice { 41 | type CommandEncoder; 42 | type SyncPoint: Clone + Debug; 43 | 44 | fn create_command_encoder(&self, desc: super::CommandEncoderDesc) -> Self::CommandEncoder; 45 | fn destroy_command_encoder(&self, encoder: &mut Self::CommandEncoder); 46 | fn submit(&self, encoder: &mut Self::CommandEncoder) -> Self::SyncPoint; 47 | fn wait_for(&self, sp: &Self::SyncPoint, timeout_ms: u32) -> bool; 48 | } 49 | 50 | pub trait CommandEncoder { 51 | type Texture: Send + Sync + Clone + Copy + Debug; 52 | type Frame: Send + Sync + Debug; 53 | fn start(&mut self); 54 | fn init_texture(&mut self, texture: Self::Texture); 55 | fn present(&mut self, frame: Self::Frame); 56 | fn timings(&self) -> &super::Timings; 57 | } 58 | 59 | pub trait TransferEncoder { 60 | type BufferPiece: Send + Sync + Clone + Copy + Debug; 61 | type TexturePiece: Send + Sync + Clone + Copy + Debug; 62 | 63 | fn fill_buffer(&mut self, dst: Self::BufferPiece, size: u64, value: u8); 64 | fn copy_buffer_to_buffer(&mut self, src: Self::BufferPiece, dst: Self::BufferPiece, size: u64); 65 | fn copy_texture_to_texture( 66 | &mut self, 67 | src: Self::TexturePiece, 68 | dst: Self::TexturePiece, 69 | size: super::Extent, 70 | ); 71 | 72 | fn copy_buffer_to_texture( 73 | &mut self, 74 | src: Self::BufferPiece, 75 | bytes_per_row: u32, 76 | dst: Self::TexturePiece, 77 | size: super::Extent, 78 | ); 79 | 80 | fn copy_texture_to_buffer( 81 | &mut self, 82 | src: Self::TexturePiece, 83 | dst: Self::BufferPiece, 84 | bytes_per_row: u32, 85 | size: super::Extent, 86 | ); 87 | } 88 | 89 | pub trait AccelerationStructureEncoder { 90 | type AccelerationStructure: Send + Sync + Clone + Debug; 91 | type AccelerationStructureMesh: Send + Sync + Clone + Debug; 92 | type BufferPiece: Send + Sync + Clone + Copy + Debug; 93 | 94 | fn build_bottom_level( 95 | &mut self, 96 | acceleration_structure: Self::AccelerationStructure, 97 | meshes: &[Self::AccelerationStructureMesh], 98 | scratch_data: Self::BufferPiece, 99 | ); 100 | 101 | fn build_top_level( 102 | &mut self, 103 | acceleration_structure: Self::AccelerationStructure, 104 | bottom_level: &[Self::AccelerationStructure], 105 | instance_count: u32, 106 | instance_data: Self::BufferPiece, 107 | scratch_data: Self::BufferPiece, 108 | ); 109 | } 110 | 111 | pub trait RenderEncoder { 112 | fn set_scissor_rect(&mut self, rect: &super::ScissorRect); 113 | fn set_viewport(&mut self, viewport: &super::Viewport); 114 | fn set_stencil_reference(&mut self, reference: u32); 115 | } 116 | 117 | pub trait PipelineEncoder { 118 | fn bind(&mut self, group: u32, data: &D); 119 | } 120 | 121 | pub trait ComputePipelineEncoder: PipelineEncoder { 122 | type BufferPiece: Send + Sync + Clone + Copy + Debug; 123 | 124 | fn dispatch(&mut self, groups: [u32; 3]); 125 | fn dispatch_indirect(&mut self, indirect_buf: Self::BufferPiece); 126 | } 127 | 128 | pub trait RenderPipelineEncoder: PipelineEncoder + RenderEncoder { 129 | type BufferPiece: Send + Sync + Clone + Copy + Debug; 130 | 131 | //Note: does this need to be available outside of the pipeline? 132 | fn bind_vertex(&mut self, index: u32, vertex_buf: Self::BufferPiece); 133 | fn draw( 134 | &mut self, 135 | first_vertex: u32, 136 | vertex_count: u32, 137 | first_instance: u32, 138 | instance_count: u32, 139 | ); 140 | fn draw_indexed( 141 | &mut self, 142 | index_buf: Self::BufferPiece, 143 | index_type: super::IndexType, 144 | index_count: u32, 145 | base_vertex: i32, 146 | start_instance: u32, 147 | instance_count: u32, 148 | ); 149 | fn draw_indirect(&mut self, indirect_buf: Self::BufferPiece); 150 | fn draw_indexed_indirect( 151 | &mut self, 152 | index_buf: Self::BufferPiece, 153 | index_type: crate::IndexType, 154 | indirect_buf: Self::BufferPiece, 155 | ); 156 | } 157 | -------------------------------------------------------------------------------- /blade-graphics/src/util.rs: -------------------------------------------------------------------------------- 1 | use codespan_reporting::{ 2 | diagnostic::{Diagnostic, Label}, 3 | files::SimpleFile, 4 | term::{ 5 | self, 6 | termcolor::{ColorChoice, StandardStream}, 7 | }, 8 | }; 9 | use std::error::Error; 10 | 11 | pub fn print_err(error: &dyn Error) { 12 | eprint!("{}", error); 13 | 14 | let mut e = error.source(); 15 | if e.is_some() { 16 | eprintln!(": "); 17 | } else { 18 | eprintln!(); 19 | } 20 | 21 | while let Some(source) = e { 22 | eprintln!("\t{}", source); 23 | e = source.source(); 24 | } 25 | } 26 | 27 | pub fn emit_annotated_error(ann_err: &naga::WithSpan, filename: &str, source: &str) { 28 | let files = SimpleFile::new(filename, source); 29 | let config = term::Config::default(); 30 | let writer = StandardStream::stderr(ColorChoice::Auto); 31 | 32 | let diagnostic = Diagnostic::error().with_labels( 33 | ann_err 34 | .spans() 35 | .map(|&(span, ref desc)| { 36 | Label::primary((), span.to_range().unwrap()).with_message(desc.to_owned()) 37 | }) 38 | .collect(), 39 | ); 40 | 41 | term::emit(&mut writer.lock(), &config, &files, &diagnostic).expect("cannot write error"); 42 | } 43 | 44 | impl super::TextureFormat { 45 | pub fn block_info(&self) -> super::TexelBlockInfo { 46 | fn uncompressed(size: u8) -> super::TexelBlockInfo { 47 | super::TexelBlockInfo { 48 | dimensions: (1, 1), 49 | size, 50 | } 51 | } 52 | fn cx_bc(size: u8) -> super::TexelBlockInfo { 53 | super::TexelBlockInfo { 54 | dimensions: (4, 4), 55 | size, 56 | } 57 | } 58 | match *self { 59 | Self::R8Unorm => uncompressed(1), 60 | Self::Rg8Unorm => uncompressed(2), 61 | Self::Rg8Snorm => uncompressed(2), 62 | Self::Rgba8Unorm => uncompressed(4), 63 | Self::Rgba8UnormSrgb => uncompressed(4), 64 | Self::Bgra8Unorm => uncompressed(4), 65 | Self::Bgra8UnormSrgb => uncompressed(4), 66 | Self::Rgba8Snorm => uncompressed(4), 67 | Self::R16Float => uncompressed(2), 68 | Self::Rg16Float => uncompressed(4), 69 | Self::Rgba16Float => uncompressed(8), 70 | Self::R32Float => uncompressed(4), 71 | Self::Rg32Float => uncompressed(8), 72 | Self::Rgba32Float => uncompressed(16), 73 | Self::R32Uint => uncompressed(4), 74 | Self::Rg32Uint => uncompressed(8), 75 | Self::Rgba32Uint => uncompressed(16), 76 | Self::Depth32Float => uncompressed(4), 77 | Self::Depth32FloatStencil8Uint => uncompressed(5), 78 | Self::Stencil8Uint => uncompressed(1), 79 | Self::Bc1Unorm => cx_bc(8), 80 | Self::Bc1UnormSrgb => cx_bc(8), 81 | Self::Bc2Unorm => cx_bc(16), 82 | Self::Bc2UnormSrgb => cx_bc(16), 83 | Self::Bc3Unorm => cx_bc(16), 84 | Self::Bc3UnormSrgb => cx_bc(16), 85 | Self::Bc4Unorm => cx_bc(8), 86 | Self::Bc4Snorm => cx_bc(8), 87 | Self::Bc5Unorm => cx_bc(16), 88 | Self::Bc5Snorm => cx_bc(16), 89 | Self::Bc6hUfloat => cx_bc(16), 90 | Self::Bc6hFloat => cx_bc(16), 91 | Self::Bc7Unorm => cx_bc(16), 92 | Self::Bc7UnormSrgb => cx_bc(16), 93 | Self::Rgb10a2Unorm => uncompressed(4), 94 | Self::Rg11b10Ufloat => uncompressed(4), 95 | Self::Rgb9e5Ufloat => uncompressed(4), 96 | } 97 | } 98 | 99 | pub fn aspects(&self) -> super::TexelAspects { 100 | match *self { 101 | Self::Depth32Float => super::TexelAspects::DEPTH, 102 | 103 | Self::Depth32FloatStencil8Uint => { 104 | super::TexelAspects::DEPTH | super::TexelAspects::STENCIL 105 | } 106 | 107 | Self::Stencil8Uint => super::TexelAspects::STENCIL, 108 | 109 | _ => super::TexelAspects::COLOR, 110 | } 111 | } 112 | } 113 | 114 | impl super::TextureColor { 115 | pub fn stencil_clear_value(&self) -> u32 { 116 | match self { 117 | crate::TextureColor::TransparentBlack => 0, 118 | crate::TextureColor::OpaqueBlack => !0, 119 | crate::TextureColor::White => !0, 120 | } 121 | } 122 | 123 | pub fn depth_clear_value(&self) -> f32 { 124 | match self { 125 | crate::TextureColor::TransparentBlack => 0.0, 126 | crate::TextureColor::OpaqueBlack => 0.0, 127 | crate::TextureColor::White => 1.0, 128 | } 129 | } 130 | } 131 | 132 | impl super::ComputePipeline { 133 | /// Return the dispatch group counts sufficient to cover the given extent. 134 | pub fn get_dispatch_for(&self, extent: super::Extent) -> [u32; 3] { 135 | let wg_size = self.get_workgroup_size(); 136 | [ 137 | (extent.width + wg_size[0] - 1) / wg_size[0], 138 | (extent.height + wg_size[1] - 1) / wg_size[1], 139 | (extent.depth + wg_size[2] - 1) / wg_size[2], 140 | ] 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /blade-graphics/src/vulkan/descriptor.rs: -------------------------------------------------------------------------------- 1 | use ash::vk; 2 | 3 | //TODO: replace by an abstraction in `gpu-descriptor` 4 | // https://github.com/zakarumych/gpu-descriptor/issues/42 5 | const COUNT_BASE: u32 = 16; 6 | 7 | #[derive(Debug)] 8 | pub struct DescriptorPool { 9 | sub_pools: Vec, 10 | growth_iter: usize, 11 | } 12 | 13 | impl super::Device { 14 | fn create_descriptor_sub_pool(&self, max_sets: u32) -> vk::DescriptorPool { 15 | log::info!("Creating a descriptor pool for at most {} sets", max_sets); 16 | let mut descriptor_sizes = vec![ 17 | vk::DescriptorPoolSize { 18 | ty: vk::DescriptorType::INLINE_UNIFORM_BLOCK_EXT, 19 | descriptor_count: max_sets * crate::limits::PLAIN_DATA_SIZE, 20 | }, 21 | vk::DescriptorPoolSize { 22 | ty: vk::DescriptorType::STORAGE_BUFFER, 23 | descriptor_count: max_sets, 24 | }, 25 | vk::DescriptorPoolSize { 26 | ty: vk::DescriptorType::SAMPLED_IMAGE, 27 | descriptor_count: 2 * max_sets, 28 | }, 29 | vk::DescriptorPoolSize { 30 | ty: vk::DescriptorType::SAMPLER, 31 | descriptor_count: max_sets, 32 | }, 33 | vk::DescriptorPoolSize { 34 | ty: vk::DescriptorType::STORAGE_IMAGE, 35 | descriptor_count: max_sets, 36 | }, 37 | ]; 38 | if self.ray_tracing.is_some() { 39 | descriptor_sizes.push(vk::DescriptorPoolSize { 40 | ty: vk::DescriptorType::ACCELERATION_STRUCTURE_KHR, 41 | descriptor_count: max_sets, 42 | }); 43 | } 44 | 45 | let mut inline_uniform_block_info = vk::DescriptorPoolInlineUniformBlockCreateInfoEXT { 46 | max_inline_uniform_block_bindings: max_sets, 47 | ..Default::default() 48 | }; 49 | 50 | let descriptor_pool_info = vk::DescriptorPoolCreateInfo::default() 51 | .max_sets(max_sets) 52 | .flags(self.workarounds.extra_descriptor_pool_create_flags) 53 | .pool_sizes(&descriptor_sizes) 54 | .push_next(&mut inline_uniform_block_info); 55 | 56 | unsafe { 57 | self.core 58 | .create_descriptor_pool(&descriptor_pool_info, None) 59 | .unwrap() 60 | } 61 | } 62 | 63 | pub(super) fn create_descriptor_pool(&self) -> DescriptorPool { 64 | let vk_pool = self.create_descriptor_sub_pool(COUNT_BASE); 65 | DescriptorPool { 66 | sub_pools: vec![vk_pool], 67 | growth_iter: 0, 68 | } 69 | } 70 | 71 | pub(super) fn destroy_descriptor_pool(&self, pool: &mut DescriptorPool) { 72 | for sub_pool in pool.sub_pools.drain(..) { 73 | unsafe { self.core.destroy_descriptor_pool(sub_pool, None) }; 74 | } 75 | } 76 | 77 | pub(super) fn allocate_descriptor_set( 78 | &self, 79 | pool: &mut DescriptorPool, 80 | layout: &super::DescriptorSetLayout, 81 | ) -> vk::DescriptorSet { 82 | let descriptor_set_layouts = [layout.raw]; 83 | 84 | loop { 85 | let descriptor_set_info = vk::DescriptorSetAllocateInfo::default() 86 | .descriptor_pool(pool.sub_pools[0]) 87 | .set_layouts(&descriptor_set_layouts); 88 | match unsafe { self.core.allocate_descriptor_sets(&descriptor_set_info) } { 89 | Ok(vk_sets) => return vk_sets[0], 90 | Err(vk::Result::ERROR_OUT_OF_POOL_MEMORY) 91 | | Err(vk::Result::ERROR_FRAGMENTED_POOL) => {} 92 | Err(other) => panic!("Unexpected descriptor allocation error: {:?}", other), 93 | }; 94 | 95 | let next_max_sets = COUNT_BASE.pow(pool.growth_iter as u32 + 1); 96 | pool.growth_iter += 1; 97 | let vk_pool = self.create_descriptor_sub_pool(next_max_sets); 98 | pool.sub_pools.insert(0, vk_pool); 99 | } 100 | } 101 | 102 | pub(super) fn reset_descriptor_pool(&self, pool: &mut DescriptorPool) { 103 | for vk_pool in pool.sub_pools.drain(1..) { 104 | unsafe { 105 | self.core.destroy_descriptor_pool(vk_pool, None); 106 | } 107 | } 108 | 109 | unsafe { 110 | self.core 111 | .reset_descriptor_pool(pool.sub_pools[0], vk::DescriptorPoolResetFlags::empty()) 112 | .unwrap(); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /blade-helpers/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blade-helpers" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Helper classes for Blade apps" 6 | keywords = ["graphics", "engine"] 7 | license = "MIT" 8 | repository = "https://github.com/kvark/blade" 9 | 10 | [lib] 11 | 12 | [features] 13 | 14 | [dependencies] 15 | blade-render = { version = "0.4", path = "../blade-render" } 16 | egui = { workspace = true } 17 | glam = { workspace = true } 18 | mint = { workspace = true } 19 | strum = { workspace = true } 20 | winit = { workspace = true } 21 | 22 | [package.metadata.cargo_check_external_types] 23 | allowed_external_types = ["blade_render::*", "epaint::*", "mint::*", "winit::*"] 24 | 25 | [lints.rust] 26 | unexpected_cfgs = { level = "allow", check-cfg = ['cfg(gles)'] } 27 | -------------------------------------------------------------------------------- /blade-helpers/src/camera.rs: -------------------------------------------------------------------------------- 1 | use super::ExposeHud; 2 | 3 | const MAX_FLY_SPEED: f32 = 1000000.0; 4 | 5 | pub struct ControlledCamera { 6 | pub inner: blade_render::Camera, 7 | pub fly_speed: f32, 8 | } 9 | 10 | impl Default for ControlledCamera { 11 | fn default() -> Self { 12 | Self { 13 | inner: blade_render::Camera { 14 | pos: mint::Vector3 { 15 | x: 0.0, 16 | y: 0.0, 17 | z: 0.0, 18 | }, 19 | rot: mint::Quaternion { 20 | v: mint::Vector3 { 21 | x: 0.0, 22 | y: 0.0, 23 | z: 0.0, 24 | }, 25 | s: 1.0, 26 | }, 27 | fov_y: 0.0, 28 | depth: 0.0, 29 | }, 30 | fly_speed: 0.0, 31 | } 32 | } 33 | } 34 | 35 | impl ControlledCamera { 36 | pub fn get_view_matrix(&self) -> glam::Mat4 { 37 | glam::Mat4::from_rotation_translation(self.inner.rot.into(), self.inner.pos.into()) 38 | .inverse() 39 | } 40 | 41 | pub fn get_projection_matrix(&self, aspect: f32) -> glam::Mat4 { 42 | glam::Mat4::perspective_rh(self.inner.fov_y, aspect, 1.0, self.inner.depth) 43 | } 44 | 45 | pub fn move_by(&mut self, offset: glam::Vec3) { 46 | let dir = glam::Quat::from(self.inner.rot) * offset; 47 | self.inner.pos = (glam::Vec3::from(self.inner.pos) + dir).into(); 48 | } 49 | 50 | pub fn rotate_z_by(&mut self, angle: f32) { 51 | let quat = glam::Quat::from(self.inner.rot); 52 | let rotation = glam::Quat::from_rotation_z(angle); 53 | self.inner.rot = (quat * rotation).into(); 54 | } 55 | 56 | pub fn on_key(&mut self, code: winit::keyboard::KeyCode, delta: f32) -> bool { 57 | use winit::keyboard::KeyCode as Kc; 58 | 59 | let move_offset = self.fly_speed * delta; 60 | let rotate_offset_z = 1000.0 * delta; 61 | match code { 62 | Kc::KeyW => { 63 | self.move_by(glam::Vec3::new(0.0, 0.0, -move_offset)); 64 | } 65 | Kc::KeyS => { 66 | self.move_by(glam::Vec3::new(0.0, 0.0, move_offset)); 67 | } 68 | Kc::KeyA => { 69 | self.move_by(glam::Vec3::new(-move_offset, 0.0, 0.0)); 70 | } 71 | Kc::KeyD => { 72 | self.move_by(glam::Vec3::new(move_offset, 0.0, 0.0)); 73 | } 74 | Kc::KeyZ => { 75 | self.move_by(glam::Vec3::new(0.0, -move_offset, 0.0)); 76 | } 77 | Kc::KeyX => { 78 | self.move_by(glam::Vec3::new(0.0, move_offset, 0.0)); 79 | } 80 | Kc::KeyQ => { 81 | self.rotate_z_by(rotate_offset_z); 82 | } 83 | Kc::KeyE => { 84 | self.rotate_z_by(-rotate_offset_z); 85 | } 86 | _ => return false, 87 | } 88 | 89 | true 90 | } 91 | 92 | pub fn on_wheel(&mut self, delta: winit::event::MouseScrollDelta) { 93 | let shift = match delta { 94 | winit::event::MouseScrollDelta::LineDelta(_, lines) => lines, 95 | winit::event::MouseScrollDelta::PixelDelta(position) => position.y as f32, 96 | }; 97 | self.fly_speed = (self.fly_speed * shift.exp()).clamp(1.0, MAX_FLY_SPEED); 98 | } 99 | } 100 | 101 | impl ExposeHud for ControlledCamera { 102 | fn populate_hud(&mut self, ui: &mut egui::Ui) { 103 | ui.horizontal(|ui| { 104 | ui.label("Position:"); 105 | ui.add(egui::DragValue::new(&mut self.inner.pos.x)); 106 | ui.add(egui::DragValue::new(&mut self.inner.pos.y)); 107 | ui.add(egui::DragValue::new(&mut self.inner.pos.z)); 108 | }); 109 | ui.horizontal(|ui| { 110 | ui.label("Rotation:"); 111 | ui.add(egui::DragValue::new(&mut self.inner.rot.v.x)); 112 | ui.add(egui::DragValue::new(&mut self.inner.rot.v.y)); 113 | ui.add(egui::DragValue::new(&mut self.inner.rot.v.z)); 114 | ui.add(egui::DragValue::new(&mut self.inner.rot.s)); 115 | }); 116 | ui.add(egui::Slider::new(&mut self.inner.fov_y, 0.5f32..=2.0f32).text("FOV")); 117 | ui.add( 118 | egui::Slider::new(&mut self.fly_speed, 1f32..=MAX_FLY_SPEED) 119 | .text("Fly speed") 120 | .logarithmic(true), 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /blade-helpers/src/hud.rs: -------------------------------------------------------------------------------- 1 | pub trait ExposeHud { 2 | fn populate_hud(&mut self, ui: &mut egui::Ui); 3 | } 4 | 5 | impl ExposeHud for blade_render::RayConfig { 6 | fn populate_hud(&mut self, ui: &mut egui::Ui) { 7 | ui.add( 8 | egui::Slider::new(&mut self.num_environment_samples, 1..=100u32) 9 | .text("Num env samples") 10 | .logarithmic(true), 11 | ); 12 | ui.checkbox( 13 | &mut self.environment_importance_sampling, 14 | "Env importance sampling", 15 | ); 16 | ui.add(egui::widgets::Slider::new(&mut self.tap_count, 0..=10).text("Tap count")); 17 | ui.add(egui::widgets::Slider::new(&mut self.tap_radius, 1..=50).text("Tap radius (px)")); 18 | ui.add( 19 | egui::widgets::Slider::new(&mut self.tap_confidence_near, 1..=50) 20 | .text("Max confidence"), 21 | ); 22 | ui.add( 23 | egui::widgets::Slider::new(&mut self.tap_confidence_far, 1..=50).text("Min confidence"), 24 | ); 25 | ui.add( 26 | egui::widgets::Slider::new(&mut self.t_start, 0.001..=0.5) 27 | .text("T min") 28 | .logarithmic(true), 29 | ); 30 | ui.checkbox(&mut self.pairwise_mis, "Pairwise MIS"); 31 | ui.add( 32 | egui::widgets::Slider::new(&mut self.defensive_mis, 0.0..=1.0).text("Defensive MIS"), 33 | ); 34 | } 35 | } 36 | 37 | impl ExposeHud for blade_render::DenoiserConfig { 38 | fn populate_hud(&mut self, ui: &mut egui::Ui) { 39 | ui.add(egui::Slider::new(&mut self.temporal_weight, 0.0..=1.0f32).text("Temporal weight")); 40 | ui.add(egui::Slider::new(&mut self.num_passes, 0..=5u32).text("A-trous passes")); 41 | } 42 | } 43 | 44 | impl ExposeHud for blade_render::PostProcConfig { 45 | fn populate_hud(&mut self, ui: &mut egui::Ui) { 46 | ui.add( 47 | egui::Slider::new(&mut self.average_luminocity, 0.1f32..=1_000f32) 48 | .text("Average luminocity") 49 | .logarithmic(true), 50 | ); 51 | ui.add( 52 | egui::Slider::new(&mut self.exposure_key_value, 0.01f32..=10f32) 53 | .text("Key value") 54 | .logarithmic(true), 55 | ); 56 | ui.add(egui::Slider::new(&mut self.white_level, 0.1f32..=2f32).text("White level")); 57 | } 58 | } 59 | 60 | impl ExposeHud for blade_render::DebugConfig { 61 | fn populate_hud(&mut self, ui: &mut egui::Ui) { 62 | use strum::IntoEnumIterator as _; 63 | 64 | egui::ComboBox::from_label("View mode") 65 | .selected_text(format!("{:?}", self.view_mode)) 66 | .show_ui(ui, |ui| { 67 | for value in blade_render::DebugMode::iter() { 68 | ui.selectable_value(&mut self.view_mode, value, format!("{value:?}")); 69 | } 70 | }); 71 | 72 | ui.label("Draw debug:"); 73 | for (name, bit) in blade_render::DebugDrawFlags::all().iter_names() { 74 | let mut enabled = self.draw_flags.contains(bit); 75 | ui.checkbox(&mut enabled, name); 76 | self.draw_flags.set(bit, enabled); 77 | } 78 | ui.label("Ignore textures:"); 79 | for (name, bit) in blade_render::DebugTextureFlags::all().iter_names() { 80 | let mut enabled = self.texture_flags.contains(bit); 81 | ui.checkbox(&mut enabled, name); 82 | self.texture_flags.set(bit, enabled); 83 | } 84 | } 85 | } 86 | 87 | pub fn populate_debug_selection( 88 | mouse_pos: &mut Option<[i32; 2]>, 89 | selection: &blade_render::SelectionInfo, 90 | asset_hub: &blade_render::AssetHub, 91 | ui: &mut egui::Ui, 92 | ) { 93 | let screen_pos = match *mouse_pos { 94 | Some(pos) => pos, 95 | None => return, 96 | }; 97 | 98 | let style = ui.style(); 99 | egui::Frame::group(style).show(ui, |ui| { 100 | ui.horizontal(|ui| { 101 | ui.label("Pixel:"); 102 | ui.colored_label( 103 | egui::Color32::WHITE, 104 | format!("{}x{}", screen_pos[0], screen_pos[1]), 105 | ); 106 | if ui.button("Unselect").clicked() { 107 | *mouse_pos = None; 108 | } 109 | }); 110 | ui.horizontal(|ui| { 111 | let sd = &selection.std_deviation; 112 | ui.label("Std Deviation:"); 113 | ui.colored_label( 114 | egui::Color32::WHITE, 115 | format!("{:.2} {:.2} {:.2}", sd.x, sd.y, sd.z), 116 | ); 117 | }); 118 | ui.horizontal(|ui| { 119 | ui.label("Samples:"); 120 | let power = selection 121 | .std_deviation_history 122 | .next_power_of_two() 123 | .trailing_zeros(); 124 | ui.colored_label(egui::Color32::WHITE, format!("2^{}", power)); 125 | }); 126 | ui.horizontal(|ui| { 127 | ui.label("Depth:"); 128 | ui.colored_label(egui::Color32::WHITE, format!("{:.2}", selection.depth)); 129 | }); 130 | ui.horizontal(|ui| { 131 | let tc = &selection.tex_coords; 132 | ui.label("Texture coords:"); 133 | ui.colored_label(egui::Color32::WHITE, format!("{:.2} {:.2}", tc.x, tc.y)); 134 | }); 135 | ui.horizontal(|ui| { 136 | let wp = &selection.position; 137 | ui.label("World pos:"); 138 | ui.colored_label( 139 | egui::Color32::WHITE, 140 | format!("{:.2} {:.2} {:.2}", wp.x, wp.y, wp.z), 141 | ); 142 | }); 143 | ui.horizontal(|ui| { 144 | ui.label("Base color:"); 145 | if let Some(handle) = selection.base_color_texture { 146 | let name = asset_hub 147 | .textures 148 | .get_main_source_path(handle) 149 | .map(|path| path.display().to_string()) 150 | .unwrap_or_default(); 151 | ui.colored_label(egui::Color32::WHITE, name); 152 | } 153 | }); 154 | ui.horizontal(|ui| { 155 | ui.label("Normal:"); 156 | if let Some(handle) = selection.normal_texture { 157 | let name = asset_hub 158 | .textures 159 | .get_main_source_path(handle) 160 | .map(|path| path.display().to_string()) 161 | .unwrap_or_default(); 162 | ui.colored_label(egui::Color32::WHITE, name); 163 | } 164 | }); 165 | }); 166 | } 167 | -------------------------------------------------------------------------------- /blade-helpers/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(any(gles, target_arch = "wasm32")))] 2 | 3 | mod camera; 4 | mod hud; 5 | 6 | pub use blade_render::Camera; 7 | pub use camera::ControlledCamera; 8 | pub use hud::{populate_debug_selection, ExposeHud}; 9 | 10 | pub fn default_ray_config() -> blade_render::RayConfig { 11 | blade_render::RayConfig { 12 | num_environment_samples: 1, 13 | environment_importance_sampling: true, 14 | tap_count: 2, 15 | tap_radius: 20, 16 | tap_confidence_near: 15, 17 | tap_confidence_far: 10, 18 | t_start: 0.01, 19 | pairwise_mis: true, 20 | defensive_mis: 0.1, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /blade-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blade-macros" 3 | version = "0.3.0" 4 | edition = "2021" 5 | description = "Macros helpers for Blade users" 6 | keywords = ["proc-macro"] 7 | license = "MIT" 8 | repository = "https://github.com/kvark/blade" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | syn = { version = "2.0", features = ["full"] } 15 | proc-macro2 = "1" 16 | quote = "1.0" 17 | 18 | [dev-dependencies] 19 | blade-graphics = { version = "0.6", path = "../blade-graphics" } 20 | blade-asset = { version = "0.2", path = "../blade-asset" } 21 | bytemuck = { workspace = true } 22 | mint = { workspace = true } 23 | -------------------------------------------------------------------------------- /blade-macros/src/as_primitive.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | 4 | pub fn generate(input_stream: TokenStream) -> syn::Result { 5 | let item_enum = syn::parse::(input_stream)?; 6 | let enum_name = item_enum.ident; 7 | Ok(quote! { 8 | impl Into for #enum_name { 9 | fn into(self) -> u32 { 10 | self as u32 11 | } 12 | } 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /blade-macros/src/flat.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | 4 | pub fn generate(input_stream: TokenStream) -> syn::Result { 5 | let item_struct = syn::parse::(input_stream)?; 6 | 7 | let struct_name = item_struct.ident; 8 | let mut generics = Vec::new(); 9 | for param in item_struct.generics.params { 10 | match param { 11 | syn::GenericParam::Lifetime(lt) => { 12 | generics.push(lt.lifetime); 13 | } 14 | syn::GenericParam::Type(_) | syn::GenericParam::Const(_) => { 15 | return Err(syn::Error::new( 16 | item_struct.struct_token.span, 17 | "Unsupported generic parameters", 18 | )) 19 | } 20 | } 21 | } 22 | 23 | Ok(match item_struct.fields { 24 | syn::Fields::Unnamed(_) => { 25 | let is_transparent = item_struct.attrs.iter().any(|attr| { 26 | if !attr.path().is_ident("repr") { 27 | return false; 28 | } 29 | let value = attr.parse_args::().unwrap(); 30 | value == "transparent" 31 | }); 32 | if !is_transparent { 33 | return Err(syn::Error::new( 34 | item_struct.struct_token.span, 35 | "Only `repr(transparent)` wrappers are supported", 36 | )); 37 | } 38 | 39 | quote! { 40 | impl<#(#generics),*> blade_asset::Flat for #struct_name<#(#generics),*> { 41 | const ALIGNMENT: usize = std::mem::size_of::(); 42 | const FIXED_SIZE: Option = std::num::NonZeroUsize::new(std::mem::size_of::()); 43 | unsafe fn write(&self, mut ptr: *mut u8) { 44 | std::ptr::write(ptr as *mut Self, *self); 45 | } 46 | unsafe fn read(mut ptr: *const u8) -> Self { 47 | std::ptr::read(ptr as *const Self) 48 | } 49 | } 50 | } 51 | } 52 | syn::Fields::Named(ref fields) => { 53 | let mut expr_alignment = quote!(0); 54 | let mut expr_size = quote!(0); 55 | let mut st_write = Vec::new(); 56 | let mut init_read = Vec::new(); 57 | 58 | for field in fields.named.iter() { 59 | let name = field.ident.as_ref().unwrap(); 60 | let ty = &field.ty; 61 | 62 | let align = quote! { <#ty as blade_asset::Flat>::ALIGNMENT }; 63 | expr_alignment = quote! { 64 | [#expr_alignment, #align][(#expr_alignment < #align) as usize] 65 | }; 66 | expr_size = quote! { 67 | blade_asset::round_up(#expr_size, #align) + self.#name.size() 68 | }; 69 | st_write.push(quote! { 70 | ptr = ptr.add(ptr.align_offset(#align)); 71 | self.#name.write(ptr); 72 | ptr = ptr.add(self.#name.size()); 73 | }); 74 | init_read.push(quote! { 75 | #name: { 76 | ptr = ptr.add(ptr.align_offset(#align)); 77 | let value = <#ty as blade_asset::Flat>::read(ptr); 78 | ptr = ptr.add(value.size()); 79 | value 80 | }, 81 | }); 82 | } 83 | 84 | quote! { 85 | impl<#(#generics),*> blade_asset::Flat for #struct_name<#(#generics),*> { 86 | const ALIGNMENT: usize = #expr_alignment; 87 | //Note: this could be improved if we see all fields being `FIXED_SIZE` 88 | const FIXED_SIZE: Option = None; 89 | fn size(&self) -> usize { 90 | #expr_size 91 | } 92 | unsafe fn write(&self, mut ptr: *mut u8) { 93 | #(#st_write)* 94 | } 95 | unsafe fn read(mut ptr: *const u8) -> Self { 96 | Self { 97 | #(#init_read)* 98 | } 99 | } 100 | } 101 | } 102 | } 103 | _ => { 104 | return Err(syn::Error::new( 105 | item_struct.struct_token.span, 106 | "Structure fields must be named", 107 | )) 108 | } 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /blade-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod as_primitive; 2 | mod flat; 3 | mod shader_data; 4 | mod vertex; 5 | 6 | use proc_macro::TokenStream; 7 | 8 | /// Derive the `ShaderData` trait for a struct 9 | /// 10 | /// ## Example 11 | /// 12 | /// ```rust 13 | /// #[derive(blade_macros::ShaderData)] 14 | /// struct Test { 15 | /// sm: blade_graphics::Sampler, 16 | /// } 17 | /// ``` 18 | #[proc_macro_derive(ShaderData)] 19 | pub fn shader_data_derive(input: TokenStream) -> TokenStream { 20 | let stream = match shader_data::generate(input) { 21 | Ok(tokens) => tokens, 22 | Err(err) => err.into_compile_error(), 23 | }; 24 | stream.into() 25 | } 26 | 27 | /// Derive the `Vertex` trait for a struct. 28 | /// 29 | /// ## Example 30 | /// 31 | /// ```rust 32 | /// #[derive(blade_macros::Vertex)] 33 | /// struct Test { 34 | /// pos: [f32; 3], 35 | /// tc: mint::Vector2, 36 | /// } 37 | /// ``` 38 | #[proc_macro_derive(Vertex)] 39 | pub fn vertex_derive(input: TokenStream) -> TokenStream { 40 | let stream = match vertex::generate(input) { 41 | Ok(tokens) => tokens, 42 | Err(err) => err.into_compile_error(), 43 | }; 44 | stream.into() 45 | } 46 | 47 | /// Derive the `Flat` for a type. 48 | /// 49 | /// Can either be used on a struct that has every field already implementing `blade_asset::Flat`: 50 | /// 51 | /// ```rust 52 | /// #[derive(blade_macros::Flat)] 53 | /// struct FlatData<'a> { 54 | /// array: [u32; 2], 55 | /// single: f32, 56 | /// slice: &'a [u16], 57 | ///} 58 | /// ``` 59 | /// 60 | /// The struct may have a lifetime describing borrowed data members. Borrowing is 61 | /// needed for zero-copy deserialization. 62 | /// 63 | /// Alternatively, can be used on a transparent wrapper to force `blade_asset::Flat` 64 | /// implementation even if the wrapped type doesn't implement it: 65 | /// 66 | /// ```rust 67 | /// #[derive(Clone, Copy)] 68 | /// #[repr(u32)] 69 | /// #[non_exhaustive] 70 | /// enum Foo { 71 | /// A, 72 | /// B, 73 | /// } 74 | /// #[derive(blade_macros::Flat, Clone, Copy)] 75 | /// #[repr(transparent)] 76 | /// struct FooWrap(Foo); 77 | /// ``` 78 | /// 79 | /// This can be particularly useful for types like `bytemuck::Pod` implementors, 80 | /// or plain non-exhaustive enums from 3rd party crates. 81 | #[proc_macro_derive(Flat)] 82 | pub fn flat_derive(input: TokenStream) -> TokenStream { 83 | let stream = match flat::generate(input) { 84 | Ok(tokens) => tokens, 85 | Err(err) => err.into_compile_error(), 86 | }; 87 | stream.into() 88 | } 89 | 90 | /// Derive the `Into` trait for an enum 91 | /// 92 | /// ## Example 93 | /// 94 | /// ```rust 95 | /// #[derive(blade_macros::AsPrimitive)] 96 | /// #[repr(u32)] 97 | /// enum Foo { 98 | /// A, 99 | /// B, 100 | /// } 101 | /// ``` 102 | #[proc_macro_derive(AsPrimitive)] 103 | pub fn as_primitive_derive(input: TokenStream) -> TokenStream { 104 | let stream = match as_primitive::generate(input) { 105 | Ok(tokens) => tokens, 106 | Err(err) => err.into_compile_error(), 107 | }; 108 | stream.into() 109 | } 110 | -------------------------------------------------------------------------------- /blade-macros/src/shader_data.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | 4 | pub fn generate(input_stream: TokenStream) -> syn::Result { 5 | let item_struct = syn::parse::(input_stream)?; 6 | let fields = match item_struct.fields { 7 | syn::Fields::Named(ref fields) => fields, 8 | _ => { 9 | return Err(syn::Error::new( 10 | item_struct.struct_token.span, 11 | "Structure fields must be named", 12 | )) 13 | } 14 | }; 15 | 16 | let mut bindings = Vec::new(); 17 | let mut assignments = Vec::new(); 18 | for (index_usize, field) in fields.named.iter().enumerate() { 19 | let index = index_usize as u32; 20 | let name = field.ident.as_ref().unwrap(); 21 | let ty = &field.ty; 22 | bindings.push(quote! { 23 | (stringify!(#name), <#ty as blade_graphics::derive::HasShaderBinding>::TYPE) 24 | }); 25 | assignments.push(quote! { 26 | self.#name.bind_to(&mut ctx, #index); 27 | }); 28 | } 29 | 30 | let mut generics = Vec::new(); 31 | for param in item_struct.generics.params { 32 | match param { 33 | syn::GenericParam::Lifetime(lt) => { 34 | generics.push(lt.lifetime); 35 | } 36 | syn::GenericParam::Type(_) | syn::GenericParam::Const(_) => { 37 | return Err(syn::Error::new( 38 | item_struct.struct_token.span, 39 | "Unsupported generic parameters", 40 | )) 41 | } 42 | } 43 | } 44 | 45 | let struct_name = item_struct.ident; 46 | Ok(quote! { 47 | impl<#(#generics),*> blade_graphics::ShaderData for #struct_name<#(#generics),*> { 48 | fn layout() -> blade_graphics::ShaderDataLayout { 49 | blade_graphics::ShaderDataLayout { 50 | bindings: vec![#(#bindings),*], 51 | } 52 | } 53 | fn fill(&self, mut ctx: blade_graphics::PipelineContext) { 54 | use blade_graphics::ShaderBindable as _; 55 | #(#assignments)* 56 | } 57 | } 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /blade-macros/src/vertex.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | 4 | pub fn generate(input_stream: TokenStream) -> syn::Result { 5 | let item_struct = syn::parse::(input_stream)?; 6 | let fields = match item_struct.fields { 7 | syn::Fields::Named(ref fields) => fields, 8 | _ => { 9 | return Err(syn::Error::new( 10 | item_struct.struct_token.span, 11 | "Structure fields must be named", 12 | )) 13 | } 14 | }; 15 | 16 | let struct_name = item_struct.ident; 17 | let mut generics = Vec::new(); 18 | for param in item_struct.generics.params { 19 | match param { 20 | syn::GenericParam::Lifetime(lt) => { 21 | generics.push(lt.lifetime); 22 | } 23 | syn::GenericParam::Type(_) | syn::GenericParam::Const(_) => { 24 | return Err(syn::Error::new( 25 | item_struct.struct_token.span, 26 | "Unsupported generic parameters", 27 | )) 28 | } 29 | } 30 | } 31 | let full_struct_name = quote!(#struct_name<#(#generics),*>); 32 | 33 | let mut attributes = Vec::new(); 34 | for field in fields.named.iter() { 35 | let name = field.ident.as_ref().unwrap(); 36 | let ty = &field.ty; 37 | //TODO: use this when MSRV gets to 1.77 38 | // `std::mem::offset_of!(#full_struct_name, #name) 39 | attributes.push(quote! { 40 | (stringify!(#name), blade_graphics::VertexAttribute { 41 | offset: unsafe { 42 | (&(*base_ptr).#name as *const _ as *const u8).offset_from(base_ptr as *const u8) as u32 43 | }, 44 | format: <#ty as blade_graphics::derive::HasVertexAttribute>::FORMAT, 45 | }) 46 | }); 47 | } 48 | 49 | Ok(quote! { 50 | impl<#(#generics),*> blade_graphics::Vertex for #full_struct_name { 51 | fn layout() -> blade_graphics::VertexLayout { 52 | let uninit = >::uninit(); 53 | let base_ptr = uninit.as_ptr(); 54 | blade_graphics::VertexLayout { 55 | attributes: vec![#(#attributes),*], 56 | stride: core::mem::size_of::() as u32, 57 | } 58 | } 59 | } 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /blade-macros/tests/main.rs: -------------------------------------------------------------------------------- 1 | #[repr(C)] 2 | #[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)] 3 | struct Globals { 4 | mvp_transform: [[f32; 4]; 4], 5 | } 6 | 7 | #[derive(blade_macros::ShaderData)] 8 | struct ShaderParams { 9 | globals: Globals, 10 | sprite_texture: blade_graphics::TextureView, 11 | sprite_sampler: blade_graphics::Sampler, 12 | } 13 | 14 | #[derive(blade_macros::Flat, PartialEq, Debug)] 15 | struct FlatData<'a> { 16 | array: [u32; 2], 17 | single: f32, 18 | slice: &'a [u16], 19 | } 20 | 21 | #[test] 22 | fn test_flat_struct() { 23 | use blade_asset::Flat; 24 | 25 | let data = FlatData { 26 | array: [1, 2], 27 | single: 3.0, 28 | slice: &[4, 5, 6], 29 | }; 30 | let mut vec = vec![0u8; data.size()]; 31 | unsafe { data.write(vec.as_mut_ptr()) }; 32 | let other = unsafe { Flat::read(vec.as_ptr()) }; 33 | assert_eq!(data, other); 34 | } 35 | 36 | #[derive(Clone, Copy, Debug, PartialEq)] 37 | #[repr(u32)] 38 | #[non_exhaustive] 39 | enum Foo { 40 | #[allow(dead_code)] 41 | A, 42 | B, 43 | } 44 | 45 | #[derive(blade_macros::Flat, Clone, Copy, Debug, PartialEq)] 46 | #[repr(transparent)] 47 | struct FooWrap(Foo); 48 | 49 | #[test] 50 | fn test_flat_wrap() { 51 | use blade_asset::Flat; 52 | 53 | let foo = FooWrap(Foo::B); 54 | let mut vec = vec![0u8; foo.size()]; 55 | unsafe { foo.write(vec.as_mut_ptr()) }; 56 | let other = unsafe { Flat::read(vec.as_ptr()) }; 57 | assert_eq!(foo, other); 58 | } 59 | -------------------------------------------------------------------------------- /blade-render/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blade-render" 3 | version = "0.4.0" 4 | edition = "2021" 5 | description = "Renderer built on Blade" 6 | keywords = ["graphics", "engine"] 7 | license = "MIT" 8 | repository = "https://github.com/kvark/blade" 9 | 10 | [lib] 11 | 12 | [features] 13 | default = ["asset"] 14 | asset = [ 15 | "gltf", 16 | "base64", 17 | "exr", 18 | "mikktspace", 19 | "slab", 20 | "texpresso", 21 | "zune-core", 22 | "zune-jpeg", 23 | "zune-png", 24 | "zune-hdr", 25 | "zune-imageprocs", 26 | ] 27 | 28 | [dependencies] 29 | base64 = { workspace = true, optional = true } 30 | bitflags = { workspace = true } 31 | blade-graphics = { version = "0.6", path = "../blade-graphics" } 32 | blade-asset = { version = "0.2", path = "../blade-asset" } 33 | blade-macros = { version = "0.3", path = "../blade-macros" } 34 | bytemuck = { workspace = true } 35 | choir = { workspace = true } 36 | exr = { version = "1.6", optional = true } 37 | gltf = { workspace = true, features = ["names", "utils"], optional = true } 38 | glam = { workspace = true } 39 | log = { workspace = true } 40 | mikktspace = { package = "bevy_mikktspace", version = "0.15.0-rc.3", optional = true } 41 | mint = { workspace = true } 42 | profiling = { workspace = true } 43 | slab = { workspace = true, optional = true } 44 | strum = { workspace = true } 45 | texpresso = { version = "2.0", optional = true } 46 | zune-core = { version = "0.4", optional = true } 47 | zune-jpeg = { version = "0.4", optional = true } 48 | zune-png = { version = "0.4", optional = true } 49 | zune-hdr = { version = "0.4", optional = true } 50 | zune-imageprocs = { version = "0.4", optional = true } 51 | 52 | [package.metadata.cargo_check_external_types] 53 | allowed_external_types = [ 54 | "bitflags::*", 55 | "blade_asset::*", 56 | "blade_graphics::*", 57 | "bytemuck::*", 58 | "choir::*", 59 | "epaint::*", 60 | "mint::*", 61 | "strum::*", 62 | ] 63 | 64 | [lints.rust] 65 | unexpected_cfgs = { level = "allow", check-cfg = ['cfg(gles)'] } 66 | -------------------------------------------------------------------------------- /blade-render/README.md: -------------------------------------------------------------------------------- 1 | # Blade Render 2 | 3 | [![Docs](https://docs.rs/blade-render/badge.svg)](https://docs.rs/blade-render) 4 | [![Crates.io](https://img.shields.io/crates/v/blade-render.svg?maxAge=2592000)](https://crates.io/crates/blade-render) 5 | 6 | Ray-traced renderer based on [blade-graphics](https://crates.io/crates/blade-graphics) and [blade-asset](https://crates.io/crates/blade-asset). 7 | 8 | ![sponza scene](etc/sponza.jpg) 9 | 10 | ## Platforms 11 | 12 | Only Vulkan with hardware Ray Tracing is currently supported. 13 | In the future, we should be able to run on Metal as well. 14 | -------------------------------------------------------------------------------- /blade-render/code/a-trous.wgsl: -------------------------------------------------------------------------------- 1 | #include "camera.inc.wgsl" 2 | #include "gbuf.inc.wgsl" 3 | #include "quaternion.inc.wgsl" 4 | #include "surface.inc.wgsl" 5 | 6 | // Spatio-temporal variance-guided filtering 7 | // https://research.nvidia.com/sites/default/files/pubs/2017-07_Spatiotemporal-Variance-Guided-Filtering%3A//svgf_preprint.pdf 8 | 9 | // Note: using "ilm" in place of "illumination and the 2nd moment of its luminance" 10 | 11 | struct Params { 12 | extent: vec2, 13 | temporal_weight: f32, 14 | iteration: u32, 15 | use_motion_vectors: u32, 16 | } 17 | 18 | var camera: CameraParams; 19 | var prev_camera: CameraParams; 20 | var params: Params; 21 | var t_depth: texture_2d; 22 | var t_prev_depth: texture_2d; 23 | var t_flat_normal: texture_2d; 24 | var t_prev_flat_normal: texture_2d; 25 | var t_motion: texture_2d; 26 | var input: texture_2d; 27 | var output: texture_storage_2d; 28 | 29 | const LUMA: vec3 = vec3(0.2126, 0.7152, 0.0722); 30 | const MIN_WEIGHT: f32 = 0.01; 31 | 32 | fn read_surface(pixel: vec2) -> Surface { 33 | var surface = Surface(); 34 | surface.flat_normal = normalize(textureLoad(t_flat_normal, pixel, 0).xyz); 35 | surface.depth = textureLoad(t_depth, pixel, 0).x; 36 | return surface; 37 | } 38 | fn read_prev_surface(pixel: vec2) -> Surface { 39 | var surface = Surface(); 40 | surface.flat_normal = normalize(textureLoad(t_prev_flat_normal, pixel, 0).xyz); 41 | surface.depth = textureLoad(t_prev_depth, pixel, 0).x; 42 | return surface; 43 | } 44 | 45 | fn get_prev_pixel(pixel: vec2, pos_world: vec3) -> vec2 { 46 | if (USE_MOTION_VECTORS && params.use_motion_vectors != 0u) { 47 | let motion = textureLoad(t_motion, pixel, 0).xy / MOTION_SCALE; 48 | return vec2(pixel) + 0.5 + motion; 49 | } else { 50 | return get_projected_pixel_float(prev_camera, pos_world); 51 | } 52 | } 53 | 54 | @compute @workgroup_size(8, 8) 55 | fn temporal_accum(@builtin(global_invocation_id) global_id: vec3) { 56 | let pixel = vec2(global_id.xy); 57 | if (any(pixel >= params.extent)) { 58 | return; 59 | } 60 | 61 | let surface = read_surface(pixel); 62 | let pos_world = camera.position + surface.depth * get_ray_direction(camera, pixel); 63 | // considering all samples in 2x2 quad, to help with edges 64 | var center_pixel = get_prev_pixel(pixel, pos_world); 65 | var prev_pixels = array, 4>( 66 | vec2(vec2(center_pixel.x - 0.5, center_pixel.y - 0.5)), 67 | vec2(vec2(center_pixel.x + 0.5, center_pixel.y - 0.5)), 68 | vec2(vec2(center_pixel.x + 0.5, center_pixel.y + 0.5)), 69 | vec2(vec2(center_pixel.x - 0.5, center_pixel.y + 0.5)), 70 | ); 71 | //Note: careful about the pixel center when there is a perfect match 72 | let w_bot_right = fract(center_pixel + vec2(0.5)); 73 | var prev_weights = vec4( 74 | (1.0 - w_bot_right.x) * (1.0 - w_bot_right.y), 75 | w_bot_right.x * (1.0 - w_bot_right.y), 76 | w_bot_right.x * w_bot_right.y, 77 | (1.0 - w_bot_right.x) * w_bot_right.y, 78 | ); 79 | 80 | var sum_weight = 0.0; 81 | var sum_ilm = vec4(0.0); 82 | if (params.temporal_weight != 1.0) { 83 | //TODO: optimize depth load with a gather operation 84 | for (var i = 0; i < 4; i += 1) { 85 | let prev_pixel = prev_pixels[i]; 86 | if (all(prev_pixel >= vec2(0)) && all(prev_pixel < params.extent)) { 87 | let prev_surface = read_prev_surface(prev_pixel); 88 | if (compare_flat_normals(surface.flat_normal, prev_surface.flat_normal) < 0.5) { 89 | continue; 90 | } 91 | let projected_distance = length(pos_world - prev_camera.position); 92 | if (compare_depths(prev_surface.depth, projected_distance) < 0.5) { 93 | continue; 94 | } 95 | let w = prev_weights[i]; 96 | sum_weight += w; 97 | let illumination = w * textureLoad(input, prev_pixel, 0).xyz; 98 | let luminocity = dot(illumination, LUMA); 99 | sum_ilm += vec4(illumination, luminocity * luminocity); 100 | } 101 | } 102 | } 103 | 104 | let cur_illumination = textureLoad(output, pixel).xyz; 105 | let cur_luminocity = dot(cur_illumination, LUMA); 106 | var mixed_ilm = vec4(cur_illumination, cur_luminocity * cur_luminocity); 107 | if (sum_weight > MIN_WEIGHT) { 108 | let prev_ilm = sum_ilm / vec4(vec3(sum_weight), max(0.001, sum_weight*sum_weight)); 109 | mixed_ilm = mix(mixed_ilm, prev_ilm, sum_weight * (1.0 - params.temporal_weight)); 110 | } 111 | //Note: could also use HW blending for this 112 | textureStore(output, pixel, mixed_ilm); 113 | } 114 | 115 | const GAUSSIAN_WEIGHTS = vec2(0.44198, 0.27901); 116 | const SIGMA_L: f32 = 4.0; 117 | const EPSILON: f32 = 0.001; 118 | 119 | fn compare_luminance(a_lum: f32, b_lum: f32, variance: f32) -> f32 { 120 | return exp(-abs(a_lum - b_lum) / (SIGMA_L * variance + EPSILON)); 121 | } 122 | 123 | fn w4(w: f32) -> vec4 { 124 | return vec4(vec3(w), w * w); 125 | } 126 | 127 | @compute @workgroup_size(8, 8) 128 | fn atrous3x3(@builtin(global_invocation_id) global_id: vec3) { 129 | let center = vec2(global_id.xy); 130 | if (any(center >= params.extent)) { 131 | return; 132 | } 133 | 134 | let center_ilm = textureLoad(input, center, 0); 135 | let center_luma = dot(center_ilm.xyz, LUMA); 136 | let variance = sqrt(center_ilm.w); 137 | let center_suf = read_surface(center); 138 | var sum_weight = GAUSSIAN_WEIGHTS[0] * GAUSSIAN_WEIGHTS[0]; 139 | var sum_ilm = w4(sum_weight) * center_ilm; 140 | 141 | for (var yy=-1; yy<=1; yy+=1) { 142 | for (var xx=-1; xx<=1; xx+=1) { 143 | let p = center + vec2(xx, yy) * (1i << params.iteration); 144 | if (all(p == center) || any(p < vec2(0)) || any(p >= params.extent)) { 145 | continue; 146 | } 147 | 148 | //TODO: store in group-shared memory 149 | let surface = read_surface(p); 150 | var weight = GAUSSIAN_WEIGHTS[abs(xx)] * GAUSSIAN_WEIGHTS[abs(yy)]; 151 | //TODO: make it stricter on higher iterations 152 | weight *= compare_flat_normals(surface.flat_normal, center_suf.flat_normal); 153 | //Note: should we use a projected depth instead of the surface one? 154 | weight *= compare_depths(surface.depth, center_suf.depth); 155 | let other_ilm = textureLoad(input, p, 0); 156 | weight *= compare_luminance(center_luma, dot(other_ilm.xyz, LUMA), variance); 157 | sum_ilm += w4(weight) * other_ilm; 158 | sum_weight += weight; 159 | } 160 | } 161 | 162 | let filtered_ilm = select(center_ilm, sum_ilm / w4(sum_weight), sum_weight > MIN_WEIGHT); 163 | textureStore(output, global_id.xy, filtered_ilm); 164 | } 165 | -------------------------------------------------------------------------------- /blade-render/code/camera.inc.wgsl: -------------------------------------------------------------------------------- 1 | struct CameraParams { 2 | position: vec3, 3 | depth: f32, 4 | orientation: vec4, 5 | fov: vec2, 6 | target_size: vec2, 7 | } 8 | 9 | const VFLIP: vec2 = vec2(1.0, -1.0); 10 | 11 | fn get_ray_direction(cp: CameraParams, pixel: vec2) -> vec3 { 12 | let half_size = 0.5 * vec2(cp.target_size); 13 | let ndc = (vec2(pixel) + vec2(0.5) - half_size) / half_size; 14 | // Right-handed coordinate system with X=right, Y=up, and Z=towards the camera 15 | let local_dir = vec3(VFLIP * ndc * tan(0.5 * cp.fov), -1.0); 16 | return normalize(qrot(cp.orientation, local_dir)); 17 | } 18 | 19 | fn get_projected_pixel_float(cp: CameraParams, point: vec3) -> vec2 { 20 | let local_dir = qrot(qinv(cp.orientation), point - cp.position); 21 | if local_dir.z >= 0.0 { 22 | return vec2(-1.0); 23 | } 24 | let ndc = local_dir.xy / (-local_dir.z * tan(0.5 * cp.fov)); 25 | let half_size = 0.5 * vec2(cp.target_size); 26 | return (VFLIP * ndc + vec2(1.0)) * half_size; 27 | } 28 | 29 | fn get_projected_pixel(cp: CameraParams, point: vec3) -> vec2 { 30 | return vec2(get_projected_pixel_float(cp, point)); 31 | } 32 | -------------------------------------------------------------------------------- /blade-render/code/debug-blit.wgsl: -------------------------------------------------------------------------------- 1 | struct DebugBlitParams { 2 | target_offset: vec2, 3 | target_size: vec2, 4 | mip_level: f32, 5 | } 6 | var params: DebugBlitParams; 7 | var input: texture_2d; 8 | var samp: sampler; 9 | 10 | struct VertexOutput { 11 | @builtin(position) clip_pos: vec4, 12 | @location(0) tc: vec2, 13 | } 14 | 15 | @vertex 16 | fn blit_vs(@builtin(vertex_index) vi: u32) -> VertexOutput { 17 | let tc = vec2(vec2(vi & 1u, (vi & 2u) >> 1u)); 18 | let transformed = params.target_offset + params.target_size * vec2(tc.x, 1.0 - tc.y); 19 | var vo: VertexOutput; 20 | vo.tc = tc; 21 | vo.clip_pos = vec4(2.0 * transformed - 1.0, 0.0, 1.0); 22 | return vo; 23 | } 24 | 25 | @fragment 26 | fn blit_fs(vo: VertexOutput) -> @location(0) vec4 { 27 | return textureSampleLevel(input, samp, vo.tc, params.mip_level); 28 | } 29 | -------------------------------------------------------------------------------- /blade-render/code/debug-draw.wgsl: -------------------------------------------------------------------------------- 1 | #include "quaternion.inc.wgsl" 2 | #include "debug.inc.wgsl" 3 | #include "camera.inc.wgsl" 4 | 5 | var camera: CameraParams; 6 | var debug_lines: array; 7 | 8 | struct DebugVarying { 9 | @builtin(position) pos: vec4, 10 | @location(0) color: vec4, 11 | @location(1) dir: vec3, 12 | } 13 | 14 | @vertex 15 | fn debug_vs(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> DebugVarying { 16 | let line = debug_lines[instance_id]; 17 | var point = line.a; 18 | if (vertex_id != 0u) { 19 | point = line.b; 20 | } 21 | 22 | let world_dir = point.pos - camera.position; 23 | let local_dir = qrot(qinv(camera.orientation), world_dir); 24 | let ndc = local_dir.xy / tan(0.5 * camera.fov); 25 | 26 | var out: DebugVarying; 27 | out.pos = vec4(ndc, 0.0, -local_dir.z); 28 | out.color = unpack4x8unorm(point.color); 29 | out.dir = world_dir; 30 | return out; 31 | } 32 | 33 | var depth: texture_2d; 34 | 35 | @fragment 36 | fn debug_fs(in: DebugVarying) -> @location(0) vec4 { 37 | let geo_dim = textureDimensions(depth); 38 | let depth_itc = vec2(i32(in.pos.x), i32(geo_dim.y) - i32(in.pos.y)); 39 | let depth = textureLoad(depth, depth_itc, 0).x; 40 | let alpha = select(0.8, 0.2, depth != 0.0 && dot(in.dir, in.dir) > depth*depth); 41 | return vec4(in.color.xyz, alpha); 42 | } 43 | -------------------------------------------------------------------------------- /blade-render/code/debug-param.inc.wgsl: -------------------------------------------------------------------------------- 1 | #use DebugMode 2 | #use DebugDrawFlags 3 | #use DebugTextureFlags 4 | 5 | struct DebugParams { 6 | view_mode: u32, 7 | draw_flags: u32, 8 | texture_flags: u32, 9 | pad: u32, 10 | mouse_pos: vec2, 11 | }; 12 | -------------------------------------------------------------------------------- /blade-render/code/debug.inc.wgsl: -------------------------------------------------------------------------------- 1 | struct DebugPoint { 2 | pos: vec3, 3 | color: u32, 4 | } 5 | struct DebugLine { 6 | a: DebugPoint, 7 | b: DebugPoint, 8 | } 9 | struct DebugVariance { 10 | color_sum: vec3, 11 | color2_sum: vec3, 12 | count: u32, 13 | } 14 | struct DebugEntry { 15 | custom_index: u32, 16 | depth: f32, 17 | tex_coords: vec2, 18 | base_color_texture: u32, 19 | normal_texture: u32, 20 | pad: vec2, 21 | position: vec3, 22 | flat_normal: vec3, 23 | } 24 | struct DebugBuffer { 25 | vertex_count: u32, 26 | instance_count: atomic, 27 | first_vertex: u32, 28 | first_instance: u32, 29 | capacity: u32, 30 | open: u32, 31 | variance: DebugVariance, 32 | entry: DebugEntry, 33 | lines: array, 34 | } 35 | 36 | var debug_buf: DebugBuffer; 37 | 38 | fn debug_line(a: vec3, b: vec3, color: u32) { 39 | if (debug_buf.open != 0u) { 40 | let index = atomicAdd(&debug_buf.instance_count, 1u); 41 | if (index < debug_buf.capacity) { 42 | debug_buf.lines[index] = DebugLine(DebugPoint(a, color), DebugPoint(b, color)); 43 | } else { 44 | // ensure the final value is never above the capacity 45 | atomicSub(&debug_buf.instance_count, 1u); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /blade-render/code/env-importance.inc.wgsl: -------------------------------------------------------------------------------- 1 | var env_weights: texture_2d; 2 | 3 | struct EnvImportantSample { 4 | pixel: vec2, 5 | pdf: f32, 6 | } 7 | 8 | // Returns the range of values proportional to the area, given the texel Y 9 | fn compute_latitude_area_bounds(texel_y: i32, dim: u32) -> vec2 { 10 | return cos(vec2(vec2(texel_y, texel_y + 1)) / f32(dim) * PI); 11 | } 12 | 13 | fn compute_texel_solid_angle(itc: vec2, dim: vec2) -> f32 { 14 | //Note: this has to agree with `map_equirect_uv_to_dir` 15 | let meridian_solid_angle = 4.0 * PI / f32(dim.x); 16 | let bounds = compute_latitude_area_bounds(itc.y, dim.y); 17 | let meridian_part = 0.5 * (bounds.x - bounds.y); 18 | return meridian_solid_angle * meridian_part; 19 | } 20 | 21 | fn generate_environment_sample(rng: ptr, dim: vec2) -> EnvImportantSample { 22 | var es = EnvImportantSample(); 23 | es.pdf = 1.0; 24 | var mip = i32(textureNumLevels(env_weights)); 25 | var itc = vec2(0); 26 | // descend through the mip chain to find a concrete pixel 27 | while (mip != 0) { 28 | mip -= 1; 29 | let weights = textureLoad(env_weights, itc, mip); 30 | let sum = dot(vec4(1.0), weights); 31 | let r = random_gen(rng) * sum; 32 | var weight: f32; 33 | itc *= 2; 34 | if (r >= weights.x+weights.y) { 35 | itc.y += 1; 36 | if (r >= weights.x+weights.y+weights.z) { 37 | weight = weights.w; 38 | itc.x += 1; 39 | } else { 40 | weight = weights.z; 41 | } 42 | } else { 43 | if (r >= weights.x) { 44 | weight = weights.y; 45 | itc.x += 1; 46 | } else { 47 | weight = weights.x; 48 | } 49 | } 50 | es.pdf *= weight / sum; 51 | } 52 | 53 | // adjust for the texel's solid angle 54 | es.pdf /= compute_texel_solid_angle(itc, dim); 55 | es.pixel = itc; 56 | return es; 57 | } 58 | 59 | fn compute_environment_sample_pdf(pixel: vec2, dim: vec2) -> f32 { 60 | var itc = pixel; 61 | var pdf = 1.0 / compute_texel_solid_angle(itc, dim); 62 | let mip_count = i32(textureNumLevels(env_weights)); 63 | for (var mip = 0; mip < mip_count; mip += 1) { 64 | let rem = itc & vec2(1); 65 | itc >>= vec2(1u); 66 | let weights = textureLoad(env_weights, itc, mip); 67 | let sum = dot(vec4(1.0), weights); 68 | let w2 = select(weights.xy, weights.zw, rem.y != 0); 69 | let weight = select(w2.x, w2.y, rem.x != 0); 70 | pdf *= weight / sum; 71 | } 72 | return pdf; 73 | } 74 | -------------------------------------------------------------------------------- /blade-render/code/env-prepare.wgsl: -------------------------------------------------------------------------------- 1 | var source: texture_2d; 2 | var destination: texture_storage_2d; 3 | struct EnvPreprocParams { 4 | target_level: u32, 5 | } 6 | var params: EnvPreprocParams; 7 | 8 | const PI: f32 = 3.1415926; 9 | const LUMA: vec3 = vec3(0.299, 0.587, 0.114); 10 | const MAX_FP16: f32 = 65504.0; 11 | const SUM: vec4 = vec4(0.25, 0.25, 0.25, 0.25); 12 | 13 | // Returns the weight of a pixel at the specified coordinates. 14 | // The weight is the area of a pixel multiplied by its luminance, not normalized. 15 | fn get_pixel_weight(pixel: vec2, src_size: vec2) -> f32 { 16 | if (any(pixel >= src_size)) { 17 | return 0.0; 18 | } 19 | let color = textureLoad(source, vec2(pixel), 0); 20 | if (params.target_level == 0u) { 21 | let luma = max(0.0, dot(LUMA, color.xyz)); 22 | let elevation = ((f32(pixel.y) + 0.5) / f32(src_size.y) - 0.5) * PI; 23 | let relative_solid_angle = cos(elevation); 24 | return clamp(luma * relative_solid_angle, 0.0, MAX_FP16); 25 | } else { 26 | return dot(SUM, color); 27 | } 28 | } 29 | 30 | @compute 31 | @workgroup_size(8, 8) 32 | fn downsample(@builtin(global_invocation_id) global_id: vec3) { 33 | let dst_size = textureDimensions(destination); 34 | if (any(global_id.xy >= dst_size)) { 35 | return; 36 | } 37 | 38 | let src_size = textureDimensions(source); 39 | let value = vec4( 40 | get_pixel_weight(global_id.xy * 2u + vec2(0u, 0u), src_size), 41 | get_pixel_weight(global_id.xy * 2u + vec2(1u, 0u), src_size), 42 | get_pixel_weight(global_id.xy * 2u + vec2(0u, 1u), src_size), 43 | get_pixel_weight(global_id.xy * 2u + vec2(1u, 1u), src_size) 44 | ); 45 | 46 | textureStore(destination, vec2(global_id.xy), value); 47 | } 48 | -------------------------------------------------------------------------------- /blade-render/code/gbuf.inc.wgsl: -------------------------------------------------------------------------------- 1 | #use DEBUG_MODE 2 | 3 | const MOTION_SCALE: f32 = 0.02; 4 | const USE_MOTION_VECTORS: bool = true; 5 | const WRITE_DEBUG_IMAGE: bool = DEBUG_MODE; 6 | -------------------------------------------------------------------------------- /blade-render/code/post-proc.wgsl: -------------------------------------------------------------------------------- 1 | #include "debug.inc.wgsl" 2 | #include "debug-param.inc.wgsl" 3 | 4 | struct ToneMapParams { 5 | enabled: u32, 6 | average_lum: f32, 7 | key_value: f32, 8 | // minimum value of the pixels mapped to white brightness 9 | white_level: f32, 10 | } 11 | 12 | var t_albedo: texture_2d; 13 | var light_diffuse: texture_2d; 14 | var t_debug: texture_2d; 15 | var tone_map_params: ToneMapParams; 16 | var debug_params: DebugParams; 17 | 18 | struct VertexOutput { 19 | @builtin(position) clip_pos: vec4, 20 | @location(0) @interpolate(flat) input_size: vec2, 21 | } 22 | 23 | @vertex 24 | fn postfx_vs(@builtin(vertex_index) vi: u32) -> VertexOutput { 25 | var vo: VertexOutput; 26 | vo.clip_pos = vec4(f32(vi & 1u) * 4.0 - 1.0, f32(vi & 2u) * 2.0 - 1.0, 0.0, 1.0); 27 | vo.input_size = textureDimensions(light_diffuse, 0); 28 | return vo; 29 | } 30 | 31 | @fragment 32 | fn postfx_fs(vo: VertexOutput) -> @location(0) vec4 { 33 | let tc = vec2(i32(vo.clip_pos.x), i32(vo.clip_pos.y)); 34 | let illumunation = textureLoad(light_diffuse, tc, 0); 35 | if (debug_params.view_mode == DebugMode_Final) { 36 | let albedo = textureLoad(t_albedo, tc, 0).xyz; 37 | let color = albedo.xyz * illumunation.xyz; 38 | if (tone_map_params.enabled != 0u) { 39 | // Following https://blog.en.uwa4d.com/2022/07/19/physically-based-renderingg-hdr-tone-mapping/ 40 | let l_adjusted = tone_map_params.key_value / tone_map_params.average_lum * color; 41 | let l_white = tone_map_params.white_level; 42 | let l_ldr = l_adjusted * (1.0 + l_adjusted / (l_white*l_white)) / (1.0 + l_adjusted); 43 | return vec4(l_ldr, 1.0); 44 | } else { 45 | return vec4(color, 1.0); 46 | } 47 | } else if (debug_params.view_mode == DebugMode_Variance) { 48 | return vec4(illumunation.w); 49 | } else { 50 | return textureLoad(t_debug, tc, 0); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /blade-render/code/quaternion.inc.wgsl: -------------------------------------------------------------------------------- 1 | fn qrot(q: vec4, v: vec3) -> vec3 { 2 | return v + 2.0*cross(q.xyz, cross(q.xyz,v) + q.w*v); 3 | } 4 | fn qinv(q: vec4) -> vec4 { 5 | return vec4(-q.xyz,q.w); 6 | } 7 | 8 | // Based on "quaternionRotationMatrix" in: 9 | // https://github.com/microsoft/DirectXMath 10 | fn make_quat(m: mat3x3) -> vec4 { 11 | var q: vec4; 12 | if (m[2].z < 0.0) { 13 | // x^2 + y^2 >= z^2 + w^2 14 | let dif10 = m[1].y - m[0].x; 15 | let omm22 = 1.0 - m[2].z; 16 | if (dif10 < 0.0) { 17 | // x^2 >= y^2 18 | q = vec4(omm22 - dif10, m[0].y + m[1].x, m[0].z + m[2].x, m[1].z - m[2].y); 19 | } else { 20 | // y^2 >= x^2 21 | q = vec4(m[0].y + m[1].x, omm22 + dif10, m[1].z + m[2].y, m[2].x - m[0].z); 22 | } 23 | } else { 24 | // z^2 + w^2 >= x^2 + y^2 25 | let sum10 = m[1].y + m[0].x; 26 | let opm22 = 1.0 + m[2].z; 27 | if (sum10 < 0.0) { 28 | // z^2 >= w^2 29 | q = vec4(m[0].z + m[2].x, m[1].z + m[2].y, opm22 - sum10, m[0].y - m[1].x); 30 | } else { 31 | // w^2 >= z^2 32 | q = vec4(m[1].z - m[2].y, m[2].x - m[0].z, m[0].y - m[1].x, opm22 + sum10); 33 | } 34 | } 35 | return normalize(q); 36 | } 37 | 38 | // Find a quaternion that turns vector 'a' into vector 'b' in a shortest arc. 39 | // https://stackoverflow.com/questions/1171849/finding-quaternion-representing-the-rotation-from-one-vector-to-another 40 | fn shortest_arc_quat(a: vec3, b: vec3) -> vec4 { 41 | if (dot(a, b) < -0.99999) { 42 | // Choose the axis of rotation that doesn't align with the vectors 43 | return select(vec4(1.0, 0.0, 0.0, 0.0), vec4(0.0, 1.0, 0.0, 0.0), abs(a.x) > abs(a.y)); 44 | } else { 45 | return normalize(vec4(cross(a, b), 1.0 + dot(a, b))); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /blade-render/code/random.inc.wgsl: -------------------------------------------------------------------------------- 1 | struct RandomState { 2 | seed: u32, 3 | index: u32, 4 | } 5 | 6 | // 32 bit Jenkins hash 7 | fn hash_jenkins(value: u32) -> u32 { 8 | var a = value; 9 | // http://burtleburtle.net/bob/hash/integer.html 10 | a = (a + 0x7ed55d16u) + (a << 12u); 11 | a = (a ^ 0xc761c23cu) ^ (a >> 19u); 12 | a = (a + 0x165667b1u) + (a << 5u); 13 | a = (a + 0xd3a2646cu) ^ (a << 9u); 14 | a = (a + 0xfd7046c5u) + (a << 3u); 15 | a = (a ^ 0xb55a4f09u) ^ (a >> 16u); 16 | return a; 17 | } 18 | 19 | fn random_init(pixel_index: u32, frame_index: u32) -> RandomState { 20 | var rs: RandomState; 21 | rs.seed = hash_jenkins(pixel_index) + frame_index; 22 | rs.index = 0u; 23 | return rs; 24 | } 25 | 26 | fn rot32(x: u32, bits: u32) -> u32 { 27 | return (x << bits) | (x >> (32u - bits)); 28 | } 29 | 30 | // https://en.wikipedia.org/wiki/MurmurHash 31 | fn murmur3(rng: ptr) -> u32 { 32 | let c1 = 0xcc9e2d51u; 33 | let c2 = 0x1b873593u; 34 | let r1 = 15u; 35 | let r2 = 13u; 36 | let m = 5u; 37 | let n = 0xe6546b64u; 38 | 39 | var hash = (*rng).seed; 40 | (*rng).index += 1u; 41 | var k = (*rng).index; 42 | k *= c1; 43 | k = rot32(k, r1); 44 | k *= c2; 45 | 46 | hash ^= k; 47 | hash = rot32(hash, r2) * m + n; 48 | 49 | hash ^= 4u; 50 | hash ^= (hash >> 16u); 51 | hash *= 0x85ebca6bu; 52 | hash ^= (hash >> 13u); 53 | hash *= 0xc2b2ae35u; 54 | hash ^= (hash >> 16u); 55 | 56 | return hash; 57 | } 58 | 59 | fn random_gen(rng: ptr) -> f32 { 60 | let v = murmur3(rng); 61 | let one = bitcast(1.0); 62 | let mask = (1u << 23u) - 1u; 63 | return bitcast((mask & v) | one) - 1.0; 64 | } 65 | -------------------------------------------------------------------------------- /blade-render/code/surface.inc.wgsl: -------------------------------------------------------------------------------- 1 | struct Surface { 2 | basis: vec4, 3 | flat_normal: vec3, 4 | depth: f32, 5 | } 6 | 7 | const SIGMA_N: f32 = 4.0; 8 | 9 | fn compare_flat_normals(a: vec3, b: vec3) -> f32 { 10 | return pow(max(0.0, dot(a, b)), SIGMA_N); 11 | } 12 | 13 | fn compare_depths(a: f32, b: f32) -> f32 { 14 | return 1.0 - smoothstep(0.0, 100.0, abs(a - b)); 15 | } 16 | 17 | // Return the compatibility rating, where 18 | // 1.0 means fully compatible, and 19 | // 0.0 means totally incompatible. 20 | fn compare_surfaces(a: Surface, b: Surface) -> f32 { 21 | let r_normal = compare_flat_normals(a.flat_normal, b.flat_normal); 22 | let r_depth = compare_depths(a.depth, b.depth); 23 | return r_normal * r_depth; 24 | } 25 | -------------------------------------------------------------------------------- /blade-render/etc/sponza.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/blade/e0ec4e720957edd51b945b64dd85605ea54bcfe5/blade-render/etc/sponza.jpg -------------------------------------------------------------------------------- /blade-render/src/asset_hub.rs: -------------------------------------------------------------------------------- 1 | use blade_asset::AssetManager; 2 | use std::{path::Path, sync::Arc}; 3 | 4 | /// A single hub to manage all assets. 5 | pub struct AssetHub { 6 | pub textures: Arc>, 7 | pub models: AssetManager, 8 | pub shaders: AssetManager, 9 | } 10 | 11 | pub struct LoadContext<'a> { 12 | asset_hub: &'a AssetHub, 13 | base_path: &'a Path, 14 | finish_task: choir::IdleTask, 15 | } 16 | 17 | impl AssetHub { 18 | /// Create a new hub. 19 | pub fn new( 20 | target: &Path, 21 | choir: &Arc, 22 | gpu_context: &Arc, 23 | ) -> Self { 24 | let _ = std::fs::create_dir_all(target); 25 | let textures = Arc::new(AssetManager::new( 26 | target, 27 | choir, 28 | crate::texture::Baker::new(gpu_context), 29 | )); 30 | let models = AssetManager::new( 31 | target, 32 | choir, 33 | crate::model::Baker::new(gpu_context, &textures), 34 | ); 35 | 36 | let mut sh_baker = crate::shader::Baker::new(gpu_context); 37 | sh_baker.register_bool("DEBUG_MODE", cfg!(debug_assertions)); 38 | sh_baker.register_enum::(); 39 | sh_baker.register_bitflags::(); 40 | sh_baker.register_bitflags::(); 41 | let shaders = AssetManager::new(target, choir, sh_baker); 42 | 43 | Self { 44 | textures, 45 | models, 46 | shaders, 47 | } 48 | } 49 | 50 | /// Flush the GPU state updates into the specified command encoder. 51 | /// 52 | /// Populates the list of temporary buffers that can be freed when the 53 | /// relevant submission is completely retired. 54 | #[profiling::function] 55 | pub fn flush( 56 | &self, 57 | command_encoder: &mut blade_graphics::CommandEncoder, 58 | temp_buffers: &mut Vec, 59 | ) { 60 | self.textures.baker.flush(command_encoder, temp_buffers); 61 | self.models.baker.flush(command_encoder, temp_buffers); 62 | } 63 | 64 | /// Destroy the hub contents. 65 | pub fn destroy(&mut self) { 66 | self.textures.clear(); 67 | self.models.clear(); 68 | self.shaders.clear(); 69 | } 70 | 71 | pub fn open_context<'a, N: Into>( 72 | &'a self, 73 | base_path: &'a Path, 74 | name: N, 75 | ) -> LoadContext<'a> { 76 | LoadContext { 77 | asset_hub: self, 78 | base_path, 79 | finish_task: self.shaders.choir.spawn(name).init_dummy(), 80 | } 81 | } 82 | 83 | #[profiling::function] 84 | pub fn list_running_tasks(&self) -> Vec { 85 | let mut list = Vec::new(); 86 | self.textures.list_running_tasks(&mut list); 87 | self.models.list_running_tasks(&mut list); 88 | self.shaders.list_running_tasks(&mut list); 89 | list 90 | } 91 | } 92 | 93 | impl LoadContext<'_> { 94 | pub fn load_shader(&mut self, path: &str) -> blade_asset::Handle { 95 | let (handle, task) = self 96 | .asset_hub 97 | .shaders 98 | .load(self.base_path.join(path), crate::shader::Meta); 99 | self.finish_task.depend_on(task); 100 | handle 101 | } 102 | 103 | pub fn close(self) -> choir::RunningTask { 104 | self.finish_task.run() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /blade-render/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(any(gles, target_arch = "wasm32")))] 2 | #![allow(irrefutable_let_patterns, clippy::new_without_default)] 3 | #![warn( 4 | trivial_casts, 5 | trivial_numeric_casts, 6 | unused_extern_crates, 7 | //TODO: re-enable. Currently doesn't like "mem::size_of" on newer Rust 8 | //unused_qualifications, 9 | // We don't match on a reference, unless required. 10 | clippy::pattern_type_mismatch, 11 | )] 12 | 13 | mod asset_hub; 14 | pub mod model; 15 | mod render; 16 | pub mod shader; 17 | pub mod texture; 18 | pub mod util; 19 | 20 | pub use asset_hub::*; 21 | pub use model::Model; 22 | pub use render::*; 23 | pub use shader::Shader; 24 | pub use texture::Texture; 25 | 26 | // Has to match the `Vertex` in shaders 27 | #[repr(C)] 28 | #[derive(Clone, Copy, Debug, Default, bytemuck::Zeroable, bytemuck::Pod)] 29 | pub struct Vertex { 30 | pub position: [f32; 3], 31 | pub bitangent_sign: f32, 32 | pub tex_coords: [f32; 2], 33 | pub normal: u32, 34 | pub tangent: u32, 35 | } 36 | 37 | #[derive(Clone, Copy, Debug)] 38 | pub struct Camera { 39 | pub pos: mint::Vector3, 40 | pub rot: mint::Quaternion, 41 | pub fov_y: f32, 42 | pub depth: f32, 43 | } 44 | 45 | pub struct Object { 46 | pub model: blade_asset::Handle, 47 | pub transform: blade_graphics::Transform, 48 | pub prev_transform: blade_graphics::Transform, 49 | } 50 | 51 | impl From> for Object { 52 | fn from(model: blade_asset::Handle) -> Self { 53 | Self { 54 | model, 55 | transform: blade_graphics::IDENTITY_TRANSFORM, 56 | prev_transform: blade_graphics::IDENTITY_TRANSFORM, 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /blade-render/src/render/dummy.rs: -------------------------------------------------------------------------------- 1 | use std::ptr; 2 | 3 | pub struct DummyResources { 4 | pub size: blade_graphics::Extent, 5 | pub white_texture: blade_graphics::Texture, 6 | pub white_view: blade_graphics::TextureView, 7 | pub black_texture: blade_graphics::Texture, 8 | pub black_view: blade_graphics::TextureView, 9 | pub red_texture: blade_graphics::Texture, 10 | pub red_view: blade_graphics::TextureView, 11 | staging_buf: blade_graphics::Buffer, 12 | } 13 | 14 | impl DummyResources { 15 | pub fn new( 16 | command_encoder: &mut blade_graphics::CommandEncoder, 17 | gpu: &blade_graphics::Context, 18 | ) -> Self { 19 | let size = blade_graphics::Extent { 20 | width: 1, 21 | height: 1, 22 | depth: 1, 23 | }; 24 | let white_texture = gpu.create_texture(blade_graphics::TextureDesc { 25 | name: "dummy/white", 26 | format: blade_graphics::TextureFormat::Rgba8Unorm, 27 | size, 28 | array_layer_count: 1, 29 | mip_level_count: 1, 30 | dimension: blade_graphics::TextureDimension::D2, 31 | usage: blade_graphics::TextureUsage::COPY | blade_graphics::TextureUsage::RESOURCE, 32 | sample_count: 1, 33 | external: None, 34 | }); 35 | let white_view = gpu.create_texture_view( 36 | white_texture, 37 | blade_graphics::TextureViewDesc { 38 | name: "dummy/white", 39 | format: blade_graphics::TextureFormat::Rgba8Unorm, 40 | dimension: blade_graphics::ViewDimension::D2, 41 | subresources: &blade_graphics::TextureSubresources::default(), 42 | }, 43 | ); 44 | let black_texture = gpu.create_texture(blade_graphics::TextureDesc { 45 | name: "dummy/black", 46 | format: blade_graphics::TextureFormat::Rgba8Unorm, 47 | size, 48 | array_layer_count: 1, 49 | mip_level_count: 1, 50 | dimension: blade_graphics::TextureDimension::D2, 51 | usage: blade_graphics::TextureUsage::COPY | blade_graphics::TextureUsage::RESOURCE, 52 | sample_count: 1, 53 | external: None, 54 | }); 55 | let black_view = gpu.create_texture_view( 56 | black_texture, 57 | blade_graphics::TextureViewDesc { 58 | name: "dummy/black", 59 | format: blade_graphics::TextureFormat::Rgba8Unorm, 60 | dimension: blade_graphics::ViewDimension::D2, 61 | subresources: &blade_graphics::TextureSubresources::default(), 62 | }, 63 | ); 64 | let red_texture = gpu.create_texture(blade_graphics::TextureDesc { 65 | name: "dummy/red", 66 | format: blade_graphics::TextureFormat::Rgba8Unorm, 67 | size, 68 | array_layer_count: 1, 69 | mip_level_count: 1, 70 | dimension: blade_graphics::TextureDimension::D2, 71 | usage: blade_graphics::TextureUsage::COPY | blade_graphics::TextureUsage::RESOURCE, 72 | sample_count: 1, 73 | external: None, 74 | }); 75 | let red_view = gpu.create_texture_view( 76 | red_texture, 77 | blade_graphics::TextureViewDesc { 78 | name: "dummy/red", 79 | format: blade_graphics::TextureFormat::Rgba8Unorm, 80 | dimension: blade_graphics::ViewDimension::D2, 81 | subresources: &blade_graphics::TextureSubresources::default(), 82 | }, 83 | ); 84 | 85 | command_encoder.init_texture(white_texture); 86 | command_encoder.init_texture(black_texture); 87 | command_encoder.init_texture(red_texture); 88 | let mut transfers = command_encoder.transfer("init dummy"); 89 | let staging_buf = gpu.create_buffer(blade_graphics::BufferDesc { 90 | name: "dummy/staging", 91 | size: 4 * 3, 92 | memory: blade_graphics::Memory::Upload, 93 | }); 94 | unsafe { 95 | ptr::write( 96 | staging_buf.data() as *mut _, 97 | [!0u8, !0, !0, !0, 0, 0, 0, 0, !0, 0, 0, 0], 98 | ); 99 | } 100 | transfers.copy_buffer_to_texture(staging_buf.at(0), 4, white_texture.into(), size); 101 | transfers.copy_buffer_to_texture(staging_buf.at(4), 4, black_texture.into(), size); 102 | transfers.copy_buffer_to_texture(staging_buf.at(8), 4, red_texture.into(), size); 103 | 104 | Self { 105 | size, 106 | white_texture, 107 | white_view, 108 | black_texture, 109 | black_view, 110 | red_texture, 111 | red_view, 112 | staging_buf, 113 | } 114 | } 115 | 116 | pub fn destroy(&mut self, gpu: &blade_graphics::Context) { 117 | gpu.destroy_texture_view(self.white_view); 118 | gpu.destroy_texture(self.white_texture); 119 | gpu.destroy_texture_view(self.black_view); 120 | gpu.destroy_texture(self.black_texture); 121 | gpu.destroy_texture_view(self.red_view); 122 | gpu.destroy_texture(self.red_texture); 123 | gpu.destroy_buffer(self.staging_buf); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /blade-render/src/render/env_map.rs: -------------------------------------------------------------------------------- 1 | use crate::DummyResources; 2 | use std::num::NonZeroU32; 3 | 4 | #[repr(C)] 5 | #[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)] 6 | struct EnvPreprocParams { 7 | target_level: u32, 8 | } 9 | 10 | #[derive(blade_macros::ShaderData)] 11 | struct EnvPreprocData { 12 | source: blade_graphics::TextureView, 13 | destination: blade_graphics::TextureView, 14 | params: EnvPreprocParams, 15 | } 16 | 17 | pub struct EnvironmentMap { 18 | pub main_view: blade_graphics::TextureView, 19 | pub size: blade_graphics::Extent, 20 | pub weight_texture: blade_graphics::Texture, 21 | pub weight_view: blade_graphics::TextureView, 22 | pub weight_mips: Vec, 23 | pub prepare_pipeline: blade_graphics::ComputePipeline, 24 | } 25 | 26 | impl EnvironmentMap { 27 | pub fn init_pipeline( 28 | shader: &blade_graphics::Shader, 29 | gpu: &blade_graphics::Context, 30 | ) -> Result { 31 | let layout = ::layout(); 32 | shader.check_struct_size::(); 33 | 34 | Ok( 35 | gpu.create_compute_pipeline(blade_graphics::ComputePipelineDesc { 36 | name: "env-prepare", 37 | data_layouts: &[&layout], 38 | compute: shader.at("downsample"), 39 | }), 40 | ) 41 | } 42 | 43 | pub fn with_pipeline( 44 | dummy: &DummyResources, 45 | prepare_pipeline: blade_graphics::ComputePipeline, 46 | ) -> Self { 47 | Self { 48 | main_view: dummy.white_view, 49 | size: blade_graphics::Extent::default(), 50 | weight_texture: blade_graphics::Texture::default(), 51 | weight_view: dummy.red_view, 52 | weight_mips: Vec::new(), 53 | prepare_pipeline, 54 | } 55 | } 56 | 57 | pub fn new( 58 | shader: &blade_graphics::Shader, 59 | dummy: &DummyResources, 60 | gpu: &blade_graphics::Context, 61 | ) -> Self { 62 | Self::with_pipeline(dummy, Self::init_pipeline(shader, gpu).unwrap()) 63 | } 64 | 65 | fn weight_size(&self) -> blade_graphics::Extent { 66 | // The weight texture has to include all of the edge pixels, starting at mip 1 67 | blade_graphics::Extent { 68 | width: self.size.width.next_power_of_two() / 2, 69 | height: self.size.height.next_power_of_two() / 2, 70 | depth: 1, 71 | } 72 | } 73 | 74 | pub fn destroy(&mut self, gpu: &blade_graphics::Context) { 75 | if self.weight_texture != blade_graphics::Texture::default() { 76 | gpu.destroy_texture(self.weight_texture); 77 | gpu.destroy_texture_view(self.weight_view); 78 | } 79 | for view in self.weight_mips.drain(..) { 80 | gpu.destroy_texture_view(view); 81 | } 82 | gpu.destroy_compute_pipeline(&mut self.prepare_pipeline); 83 | } 84 | 85 | pub fn assign( 86 | &mut self, 87 | view: blade_graphics::TextureView, 88 | extent: blade_graphics::Extent, 89 | encoder: &mut blade_graphics::CommandEncoder, 90 | gpu: &blade_graphics::Context, 91 | ) { 92 | if self.main_view == view { 93 | return; 94 | } 95 | self.main_view = view; 96 | self.size = extent; 97 | self.destroy(gpu); 98 | 99 | let mip_level_count = extent 100 | .width 101 | .max(extent.height) 102 | .next_power_of_two() 103 | .trailing_zeros(); 104 | let weight_extent = self.weight_size(); 105 | let format = blade_graphics::TextureFormat::Rgba16Float; 106 | self.weight_texture = gpu.create_texture(blade_graphics::TextureDesc { 107 | name: "env-weight", 108 | format, 109 | size: weight_extent, 110 | dimension: blade_graphics::TextureDimension::D2, 111 | array_layer_count: 1, 112 | mip_level_count, 113 | usage: blade_graphics::TextureUsage::RESOURCE | blade_graphics::TextureUsage::STORAGE, 114 | sample_count: 1, 115 | external: None, 116 | }); 117 | self.weight_view = gpu.create_texture_view( 118 | self.weight_texture, 119 | blade_graphics::TextureViewDesc { 120 | name: "env-weight", 121 | format, 122 | dimension: blade_graphics::ViewDimension::D2, 123 | subresources: &Default::default(), 124 | }, 125 | ); 126 | for base_mip_level in 0..mip_level_count { 127 | let view = gpu.create_texture_view( 128 | self.weight_texture, 129 | blade_graphics::TextureViewDesc { 130 | name: &format!("env-weight-mip{}", base_mip_level), 131 | format, 132 | dimension: blade_graphics::ViewDimension::D2, 133 | subresources: &blade_graphics::TextureSubresources { 134 | base_mip_level, 135 | mip_level_count: NonZeroU32::new(1), 136 | ..Default::default() 137 | }, 138 | }, 139 | ); 140 | self.weight_mips.push(view); 141 | } 142 | 143 | encoder.init_texture(self.weight_texture); 144 | for target_level in 0..mip_level_count { 145 | let groups = self 146 | .prepare_pipeline 147 | .get_dispatch_for(weight_extent.at_mip_level(target_level)); 148 | let mut compute = encoder.compute("pre-process env map"); 149 | let mut pass = compute.with(&self.prepare_pipeline); 150 | pass.bind( 151 | 0, 152 | &EnvPreprocData { 153 | source: if target_level == 0 { 154 | view 155 | } else { 156 | self.weight_mips[target_level as usize - 1] 157 | }, 158 | destination: self.weight_mips[target_level as usize], 159 | params: EnvPreprocParams { target_level }, 160 | }, 161 | ); 162 | pass.dispatch(groups); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /blade-render/src/shader.rs: -------------------------------------------------------------------------------- 1 | use std::{any, collections::HashMap, fmt, fs, path::Path, str, sync::Arc}; 2 | 3 | const FAILURE_DUMP_NAME: &str = "_failure.wgsl"; 4 | 5 | #[derive(blade_macros::Flat)] 6 | pub struct CookedShader<'a> { 7 | data: &'a [u8], 8 | } 9 | 10 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 11 | pub struct Meta; 12 | impl fmt::Display for Meta { 13 | fn fmt(&self, _f: &mut fmt::Formatter) -> fmt::Result { 14 | Ok(()) 15 | } 16 | } 17 | 18 | pub struct Shader { 19 | pub raw: Result, 20 | } 21 | 22 | pub enum Expansion { 23 | Values(HashMap), 24 | Bool(bool), 25 | } 26 | impl Expansion { 27 | pub fn from_enum>() -> Self { 28 | Self::Values( 29 | E::iter() 30 | .map(|variant| (format!("{variant:?}"), variant.into())) 31 | .collect(), 32 | ) 33 | } 34 | pub fn from_bitflags>() -> Self { 35 | Self::Values( 36 | F::FLAGS 37 | .iter() 38 | .map(|flag| (flag.name().to_string(), flag.value().bits())) 39 | .collect(), 40 | ) 41 | } 42 | } 43 | 44 | pub struct Baker { 45 | gpu_context: Arc, 46 | expansions: HashMap, 47 | } 48 | 49 | impl Baker { 50 | pub fn new(gpu_context: &Arc) -> Self { 51 | Self { 52 | gpu_context: Arc::clone(gpu_context), 53 | expansions: HashMap::default(), 54 | } 55 | } 56 | 57 | fn register(&mut self, expansion: Expansion) { 58 | let full_name = any::type_name::(); 59 | let short_name = full_name.split("::").last().unwrap().to_string(); 60 | self.expansions.insert(short_name, expansion); 61 | } 62 | 63 | pub fn register_enum>(&mut self) { 64 | self.register::(Expansion::from_enum::()); 65 | } 66 | 67 | pub fn register_bitflags>(&mut self) { 68 | self.register::(Expansion::from_bitflags::()); 69 | } 70 | 71 | pub fn register_bool(&mut self, name: &str, value: bool) { 72 | self.expansions 73 | .insert(name.to_string(), Expansion::Bool(value)); 74 | } 75 | } 76 | 77 | fn parse_impl( 78 | text_raw: &[u8], 79 | base_path: &Path, 80 | text_out: &mut String, 81 | cooker: &blade_asset::Cooker, 82 | expansions: &HashMap, 83 | ) { 84 | use std::fmt::Write as _; 85 | 86 | let text_in = str::from_utf8(text_raw).unwrap(); 87 | for line in text_in.lines() { 88 | if line.starts_with("#include") { 89 | let include_path = match line.split('"').nth(1) { 90 | Some(include) => base_path.join(include), 91 | None => panic!("Unable to extract the include path from: {line}"), 92 | }; 93 | let include = cooker.add_dependency(&include_path); 94 | writeln!(text_out, "//{}", line).unwrap(); 95 | parse_impl( 96 | &include, 97 | include_path.parent().unwrap(), 98 | text_out, 99 | cooker, 100 | expansions, 101 | ); 102 | } else if line.starts_with("#use") { 103 | let type_name = line.split_whitespace().last().unwrap(); 104 | match expansions[type_name] { 105 | Expansion::Values(ref map) => { 106 | for (key, value) in map.iter() { 107 | writeln!(text_out, "const {}_{}: u32 = {}u;", type_name, key, value) 108 | .unwrap(); 109 | } 110 | } 111 | Expansion::Bool(value) => { 112 | writeln!(text_out, "const {}: bool = {};", type_name, value).unwrap(); 113 | } 114 | } 115 | } else { 116 | *text_out += line; 117 | } 118 | *text_out += "\n"; 119 | } 120 | } 121 | 122 | pub fn parse_shader( 123 | text_raw: &[u8], 124 | cooker: &blade_asset::Cooker, 125 | expansions: &HashMap, 126 | ) -> String { 127 | let mut text_out = String::new(); 128 | parse_impl(text_raw, ".".as_ref(), &mut text_out, cooker, expansions); 129 | text_out 130 | } 131 | 132 | impl blade_asset::Baker for Baker { 133 | type Meta = Meta; 134 | type Data<'a> = CookedShader<'a>; 135 | type Output = Shader; 136 | fn cook( 137 | &self, 138 | source: &[u8], 139 | extension: &str, 140 | _meta: Meta, 141 | cooker: Arc>, 142 | _exe_context: &choir::ExecutionContext, 143 | ) { 144 | assert_eq!(extension, "wgsl"); 145 | let text_out = parse_shader(source, &cooker, &self.expansions); 146 | cooker.finish(CookedShader { 147 | data: text_out.as_bytes(), 148 | }); 149 | } 150 | fn serve(&self, cooked: CookedShader, _exe_context: &choir::ExecutionContext) -> Shader { 151 | let source = str::from_utf8(cooked.data).unwrap(); 152 | let raw = self 153 | .gpu_context 154 | .try_create_shader(blade_graphics::ShaderDesc { source }); 155 | if let Err(e) = raw { 156 | let _ = fs::write(FAILURE_DUMP_NAME, source); 157 | log::warn!("Shader compilation failed: {e:?}, source dumped as '{FAILURE_DUMP_NAME}'.") 158 | } 159 | Shader { raw } 160 | } 161 | fn delete(&self, _output: Shader) {} 162 | } 163 | -------------------------------------------------------------------------------- /blade-render/src/util/frame_pacer.rs: -------------------------------------------------------------------------------- 1 | use crate::render::FrameResources; 2 | use std::mem; 3 | 4 | /// Utility object that encapsulates the logic 5 | /// of always rendering 1 frame at a time, and 6 | /// cleaning up the temporary resources. 7 | pub struct FramePacer { 8 | frame_index: usize, 9 | prev_resources: FrameResources, 10 | prev_sync_point: Option, 11 | command_encoder: blade_graphics::CommandEncoder, 12 | next_resources: FrameResources, 13 | } 14 | 15 | impl FramePacer { 16 | pub fn new(context: &blade_graphics::Context) -> Self { 17 | let encoder = context.create_command_encoder(blade_graphics::CommandEncoderDesc { 18 | name: "main", 19 | buffer_count: 2, 20 | }); 21 | Self { 22 | frame_index: 0, 23 | prev_resources: FrameResources::default(), 24 | prev_sync_point: None, 25 | command_encoder: encoder, 26 | next_resources: FrameResources::default(), 27 | } 28 | } 29 | 30 | #[profiling::function] 31 | pub fn wait_for_previous_frame(&mut self, context: &blade_graphics::Context) { 32 | if let Some(sp) = self.prev_sync_point.take() { 33 | context.wait_for(&sp, !0); 34 | } 35 | for buffer in self.prev_resources.buffers.drain(..) { 36 | context.destroy_buffer(buffer); 37 | } 38 | for accel_structure in self.prev_resources.acceleration_structures.drain(..) { 39 | context.destroy_acceleration_structure(accel_structure); 40 | } 41 | } 42 | 43 | pub fn last_sync_point(&self) -> Option<&blade_graphics::SyncPoint> { 44 | self.prev_sync_point.as_ref() 45 | } 46 | 47 | pub fn destroy(&mut self, context: &blade_graphics::Context) { 48 | self.wait_for_previous_frame(context); 49 | context.destroy_command_encoder(&mut self.command_encoder); 50 | } 51 | 52 | pub fn begin_frame(&mut self) -> (&mut blade_graphics::CommandEncoder, &mut FrameResources) { 53 | self.command_encoder.start(); 54 | (&mut self.command_encoder, &mut self.next_resources) 55 | } 56 | 57 | pub fn end_frame(&mut self, context: &blade_graphics::Context) -> &blade_graphics::SyncPoint { 58 | let sync_point = context.submit(&mut self.command_encoder); 59 | self.frame_index += 1; 60 | // Wait for the previous frame immediately - this ensures that we are 61 | // only processing one frame at a time, and yet not stalling. 62 | self.wait_for_previous_frame(context); 63 | self.prev_sync_point = Some(sync_point); 64 | mem::swap(&mut self.prev_resources, &mut self.next_resources); 65 | self.prev_sync_point.as_ref().unwrap() 66 | } 67 | 68 | pub fn timings(&self) -> &blade_graphics::Timings { 69 | self.command_encoder.timings() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /blade-render/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | mod frame_pacer; 2 | 3 | pub use self::frame_pacer::*; 4 | 5 | pub fn align_to(offset: u64, alignment: u64) -> u64 { 6 | let rem = offset & (alignment - 1); 7 | if rem == 0 { 8 | offset 9 | } else { 10 | offset - rem + alignment 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /blade-util/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blade-util" 3 | version = "0.2.0" 4 | edition = "2021" 5 | description = "Utility logic for Blade applications" 6 | keywords = ["graphics"] 7 | license = "MIT" 8 | repository = "https://github.com/kvark/blade" 9 | 10 | [lib] 11 | 12 | [dependencies] 13 | blade-graphics = { version = "0.6", path = "../blade-graphics" } 14 | bytemuck = { workspace = true } 15 | log = { workspace = true } 16 | profiling = { workspace = true } 17 | 18 | [package.metadata.cargo_check_external_types] 19 | allowed_external_types = ["blade_graphics::*"] 20 | -------------------------------------------------------------------------------- /blade-util/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/blade/e0ec4e720957edd51b945b64dd85605ea54bcfe5/blade-util/README.md -------------------------------------------------------------------------------- /blade-util/src/belt.rs: -------------------------------------------------------------------------------- 1 | use blade_graphics as gpu; 2 | use std::mem; 3 | 4 | struct ReusableBuffer { 5 | raw: gpu::Buffer, 6 | size: u64, 7 | } 8 | 9 | /// Configuration of the Blade belt. 10 | pub struct BufferBeltDescriptor { 11 | /// Kind of memory to allocate from. 12 | pub memory: gpu::Memory, 13 | pub min_chunk_size: u64, 14 | pub alignment: u64, 15 | } 16 | 17 | /// A belt of reusable buffer space. 18 | /// Could be useful for temporary data, such as texture staging areas. 19 | pub struct BufferBelt { 20 | desc: BufferBeltDescriptor, 21 | buffers: Vec<(ReusableBuffer, gpu::SyncPoint)>, 22 | active: Vec<(ReusableBuffer, u64)>, 23 | } 24 | 25 | impl BufferBelt { 26 | /// Create a new belt. 27 | pub fn new(desc: BufferBeltDescriptor) -> Self { 28 | assert_ne!(desc.alignment, 0); 29 | Self { 30 | desc, 31 | buffers: Vec::new(), 32 | active: Vec::new(), 33 | } 34 | } 35 | 36 | /// Destroy this belt. 37 | pub fn destroy(&mut self, gpu: &gpu::Context) { 38 | for (buffer, _) in self.buffers.drain(..) { 39 | gpu.destroy_buffer(buffer.raw); 40 | } 41 | for (buffer, _) in self.active.drain(..) { 42 | gpu.destroy_buffer(buffer.raw); 43 | } 44 | } 45 | 46 | /// Allocate a region of `size` bytes. 47 | #[profiling::function] 48 | pub fn alloc(&mut self, size: u64, gpu: &gpu::Context) -> gpu::BufferPiece { 49 | for &mut (ref rb, ref mut offset) in self.active.iter_mut() { 50 | let aligned = offset.next_multiple_of(self.desc.alignment); 51 | if aligned + size <= rb.size { 52 | let piece = rb.raw.at(aligned); 53 | *offset = aligned + size; 54 | return piece; 55 | } 56 | } 57 | 58 | let index_maybe = self 59 | .buffers 60 | .iter() 61 | .position(|(rb, sp)| size <= rb.size && gpu.wait_for(sp, 0)); 62 | if let Some(index) = index_maybe { 63 | let (rb, _) = self.buffers.remove(index); 64 | let piece = rb.raw.into(); 65 | self.active.push((rb, size)); 66 | return piece; 67 | } 68 | 69 | let chunk_index = self.buffers.len() + self.active.len(); 70 | let chunk_size = size.max(self.desc.min_chunk_size); 71 | let chunk = gpu.create_buffer(gpu::BufferDesc { 72 | name: &format!("chunk-{}", chunk_index), 73 | size: chunk_size, 74 | memory: self.desc.memory, 75 | }); 76 | let rb = ReusableBuffer { 77 | raw: chunk, 78 | size: chunk_size, 79 | }; 80 | self.active.push((rb, size)); 81 | chunk.into() 82 | } 83 | 84 | /// Allocate a region to hold the byte `data` slice contents. 85 | pub fn alloc_bytes(&mut self, data: &[u8], gpu: &gpu::Context) -> gpu::BufferPiece { 86 | assert!(!data.is_empty()); 87 | let bp = self.alloc(data.len() as u64, gpu); 88 | unsafe { 89 | std::ptr::copy_nonoverlapping(data.as_ptr(), bp.data(), data.len()); 90 | } 91 | bp 92 | } 93 | 94 | // SAFETY: T should be zeroable and ordinary data, no references, pointers, cells or other complicated data type. 95 | /// Allocate a region to hold the typed `data` slice contents. 96 | pub unsafe fn alloc_typed(&mut self, data: &[T], gpu: &gpu::Context) -> gpu::BufferPiece { 97 | assert!(!data.is_empty()); 98 | let type_alignment = mem::align_of::() as u64; 99 | debug_assert_eq!( 100 | self.desc.alignment % type_alignment, 101 | 0, 102 | "Type alignment {} is too big", 103 | type_alignment 104 | ); 105 | let total_bytes = std::mem::size_of_val(data); 106 | let bp = self.alloc(total_bytes as u64, gpu); 107 | unsafe { 108 | std::ptr::copy_nonoverlapping(data.as_ptr() as *const u8, bp.data(), total_bytes); 109 | } 110 | bp 111 | } 112 | 113 | /// Allocate a region to hold the POD `data` slice contents. 114 | pub fn alloc_pod( 115 | &mut self, 116 | data: &[T], 117 | gpu: &gpu::Context, 118 | ) -> gpu::BufferPiece { 119 | unsafe { self.alloc_typed(data, gpu) } 120 | } 121 | 122 | /// Mark the actively used buffers as used by GPU with a given sync point. 123 | pub fn flush(&mut self, sp: &gpu::SyncPoint) { 124 | self.buffers 125 | .extend(self.active.drain(..).map(|(rb, _)| (rb, sp.clone()))); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /blade-util/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod belt; 2 | 3 | pub use belt::{BufferBelt, BufferBeltDescriptor}; 4 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog for *Blade* project 2 | 3 | ## blade-graphics-0.6, blade-util-0.2, blade-egui-0.6, blade-render-0.4, blade-0.3 (21 Dec 2024) 4 | 5 | - graphics: 6 | - API for surface creation 7 | - allows multiple windows used by the same context 8 | - multi-sampling support 9 | - API for destruction of pipelines 10 | - return detailed initialization errors 11 | - every pass now takes a label 12 | - automatic GPU pass markers 13 | - ability to capture pass GPU timings 14 | - ability to force the use of a specific GPU 15 | - ability to set viewport 16 | - fragment shader is optional 17 | - support more texture formats 18 | - Metal: 19 | - migrate to "objc2" 20 | - support for workgroup memory 21 | - concurrent compute dispatches 22 | - Egl: 23 | - destroy old surface on resize 24 | - Vulkan: 25 | - support unused bind groups 26 | - egui: 27 | - fix blending color space 28 | 29 | ## blade-egui-0.5 (09 Nov 2024) 30 | 31 | - update egui to 0.29 32 | 33 | ## blade-graphics-0.5, blade-macros-0.3, blade-egui-0.4, blade-util-0.1 (27 Aug 2024) 34 | 35 | - crate: `blade-util` for helper utilities 36 | - graphics: 37 | - vertex buffers support 38 | - surface configuration: 39 | - transparency support 40 | - option to disable exclusive fullscreen 41 | - VK: using linear sRGB color space if available 42 | - exposed initialization errors 43 | - exposed device information 44 | - Vk: 45 | - fixed initial RAM consumption 46 | - worked around Intel descriptor memory allocation bug 47 | - fixed coherent memory requirements 48 | - rudimentary cleanup on destruction 49 | - GLES: 50 | - support for storage buffer and compute 51 | - scissor rects, able to run "particle" example 52 | - blending and draw masks 53 | - fixed texture uploads 54 | - examples: "move" 55 | - window API switched to raw-window-handle-0.6 56 | 57 | ## blade-graphics-0.4, blade-render-0.3, blade-0.2 (22 Mar 2024) 58 | 59 | - crate: `blade` for high-level engine 60 | - built-in physics via Rapier3D 61 | - examples: "vehicle" 62 | - render: 63 | - support object motion 64 | - support clockwise mesh winding 65 | - fixed mipmap generation 66 | - update to egui-0.26 and winit-0.29 67 | - graphics: 68 | - display sync configuration 69 | - color space configuration 70 | - work around Intel+Nvidia presentation bug 71 | - overlay support 72 | 73 | ## blade-graphics-0.3, blade-render-0.2 (17 Nov 2023) 74 | 75 | - tangent space generation 76 | - spatio-temporal resampling 77 | - SVGF de-noising 78 | - environment map importance sampling 79 | - shaders as assets 80 | - with includes, enums, and bitflags 81 | - with hot reloading 82 | - load textures: `exr`, `hdr` 83 | - utility: `FramePacer` 84 | - examples: scene editing in "scene" 85 | - using egui-gizmo for manipulation 86 | 87 | ## blade-graphics-0.2, blade-render-0.1 (31 May 2023) 88 | 89 | - ray tracing support 90 | - examples: "ray-query", "scene" 91 | - crate: `blade-egui` for egui integration 92 | - crate: `blade-asset` for asset pipeline 93 | - crate: `blade-render` for ray-traced renderer 94 | - load models: `gltf` 95 | - load textures: `png`, `jpg` 96 | 97 | ## blade-graphics-0.1 (25 Jan 2023) 98 | 99 | - backends: Vulkan, Metal, OpenGL ES + WebGL2 100 | - examples: "mini", "bunnymark", "particle" 101 | - crate: `blade-graphics` for GPU abstracting GPU operations 102 | - crate: `blade-macros` for `ShaderData` derivation 103 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Blade 2 | 3 | [![Matrix](https://img.shields.io/static/v1?label=dev&message=%23blade&color=blueviolet&logo=matrix)](https://matrix.to/#/#blade-dev:matrix.org) 4 | [![Build Status](https://github.com/kvark/blade/workflows/check/badge.svg)](https://github.com/kvark/blade/actions) 5 | [![Docs](https://docs.rs/blade/badge.svg)](https://docs.rs/blade) 6 | [![Crates.io](https://img.shields.io/crates/v/blade.svg?label=blade)](https://crates.io/crates/blade) 7 | [![Crates.io](https://img.shields.io/crates/v/blade-graphics.svg?label=blade-graphics)](https://crates.io/crates/blade-graphics) 8 | [![Crates.io](https://img.shields.io/crates/v/blade-render.svg?label=blade-render)](https://crates.io/crates/blade-render) 9 | 10 | ![](logo.png) 11 | 12 | Blade is an innovative rendering solution for Rust. It starts with a lean low-level GPU abstraction focused at ergonomics and fun. It then grows into a high-level rendering library that utilizes hardware ray-tracing. It's accompanied by a [task-parallel asset pipeline](https://youtu.be/1DiA3OYqvqU) together with [egui](https://www.egui.rs/) support, turning into a minimal rendering engine. Finally, the top-level Blade engine combines all of this with Rapier3D-based physics and hides them behind a concise API. Talks: 13 | - [In GPU we Rust](https://youtu.be/92mwRCXvMVk) (Rust AI meetup, 2024) 14 | - [Blade - lean and mean graphics library](https://youtu.be/63dnzjw4azI?t=623) (Rust Graphics meetup, 2023) 15 | - [Blade asset pipeline](https://youtu.be/1DiA3OYqvqU) (Rust Gamedev meetup, 2023) 16 | - [Blade scene editor](https://www.youtube.com/watch?v=Q5IUOvuXoC8) (Rust Gamedev meetup, 2023) 17 | 18 | ![architecture](https://raw.githubusercontent.com/kvark/blade/main/docs/architecture2.png) 19 | 20 | ## Examples 21 | 22 | ![scene editor](../blade-egui/etc/scene-editor.jpg) 23 | ![particle example](../blade-graphics/etc/particles.png) 24 | ![vehicle example](vehicle-colliders.jpg) 25 | ![sponza scene](../blade-render/etc/sponza.jpg) 26 | 27 | ## Instructions 28 | 29 | Just the usual :crab: workflow. E.g. to run the bunny-mark benchmark run: 30 | 31 | ```bash 32 | cargo run --release --example bunnymark 33 | ``` 34 | 35 | ## Platforms 36 | 37 | The full-stack Blade Engine can only run on Vulkan with hardware Ray Tracing support. 38 | However, on secondary platforms, such as Metal and GLES/WebGL2, one can still use Blde-Graphics and Blade-Egui. 39 | -------------------------------------------------------------------------------- /docs/architecture2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/blade/e0ec4e720957edd51b945b64dd85605ea54bcfe5/docs/architecture2.png -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/blade/e0ec4e720957edd51b945b64dd85605ea54bcfe5/docs/logo.png -------------------------------------------------------------------------------- /docs/vehicle-colliders.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/blade/e0ec4e720957edd51b945b64dd85605ea54bcfe5/docs/vehicle-colliders.jpg -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Blade Examples 2 | 3 | | Example | graphics | macros | util | egui | asset | render | helper | lib | 4 | | --------- | ----------- | ------ | ------ | ------ | ------ | ------ | ------ | ------ | 5 | | mini | :star: | | | | | | | | 6 | | init | :star: | :star: | | | | | | | 7 | | ray-query | :star: (RT) | :star: | | | | | | | 8 | | particle | :star: | :star: | | :star: | | | | | 9 | | scene | :star: (RT) | :star: | | :star: | :star: | :star: | :star: | | 10 | | vehicle | | | | | | | | :star: | 11 | | move | | | | | | | :star: | :star: | 12 | -------------------------------------------------------------------------------- /examples/bunnymark/shader.wgsl: -------------------------------------------------------------------------------- 1 | struct Globals { 2 | mvp_transform: mat4x4, 3 | sprite_size: vec2, 4 | }; 5 | var globals: Globals; 6 | 7 | struct Locals { 8 | position: vec2, 9 | velocity: vec2, 10 | color: u32, 11 | }; 12 | var locals: Locals; 13 | 14 | struct Vertex { 15 | pos: vec2, 16 | }; 17 | 18 | var sprite_texture: texture_2d; 19 | var sprite_sampler: sampler; 20 | 21 | struct VertexOutput { 22 | @builtin(position) position: vec4, 23 | @location(0) tex_coords: vec2, 24 | @location(1) color: vec4, 25 | } 26 | 27 | fn unpack_color(raw: u32) -> vec4 { 28 | //TODO: https://github.com/gfx-rs/naga/issues/2188 29 | //return unpack4x8unorm(raw); 30 | return vec4((vec4(raw) >> vec4(0u, 8u, 16u, 24u)) & vec4(0xFFu)) / 255.0; 31 | } 32 | 33 | @vertex 34 | fn vs_main(vertex: Vertex) -> VertexOutput { 35 | let tc = vertex.pos; 36 | let offset = tc * globals.sprite_size; 37 | let pos = globals.mvp_transform * vec4(locals.position + offset, 0.0, 1.0); 38 | let color = unpack_color(locals.color); 39 | return VertexOutput(pos, tc, color); 40 | } 41 | 42 | @fragment 43 | fn fs_main(vertex: VertexOutput) -> @location(0) vec4 { 44 | return vertex.color * textureSampleLevel(sprite_texture, sprite_sampler, vertex.tex_coords, 0.0); 45 | } 46 | -------------------------------------------------------------------------------- /examples/init/env-sample.wgsl: -------------------------------------------------------------------------------- 1 | #include "../../blade-render/code/random.inc.wgsl" 2 | #include "../../blade-render/code/env-importance.inc.wgsl" 3 | 4 | const PI: f32 = 3.1415926; 5 | const BUMP: f32 = 0.025; 6 | 7 | var env_main: texture_2d; 8 | 9 | @vertex 10 | fn vs_accum(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4 { 11 | var rng = random_init(vi, 0u); 12 | let dim = textureDimensions(env_main); 13 | let es = generate_environment_sample(&rng, dim); 14 | let extent = textureDimensions(env_weights, 0); 15 | let relative = (vec2(es.pixel) + vec2(0.5)) / vec2(extent); 16 | return vec4(relative.x - 1.0, 1.0 - relative.y, 0.0, 1.0); 17 | } 18 | 19 | @fragment 20 | fn fs_accum() -> @location(0) vec4 { 21 | return vec4(BUMP); 22 | } 23 | 24 | fn map_equirect_dir_to_uv(dir: vec3) -> vec2 { 25 | //Note: Y axis is up 26 | let yaw = asin(dir.y); 27 | let pitch = atan2(dir.x, dir.z); 28 | return vec2(pitch + PI, -2.0 * yaw + PI) / (2.0 * PI); 29 | } 30 | fn map_equirect_uv_to_dir(uv: vec2) -> vec3 { 31 | let yaw = PI * (0.5 - uv.y); 32 | let pitch = 2.0 * PI * (uv.x - 0.5); 33 | return vec3(cos(yaw) * sin(pitch), sin(yaw), cos(yaw) * cos(pitch)); 34 | } 35 | 36 | struct UvOutput { 37 | @builtin(position) position: vec4, 38 | @location(0) uv: vec2, 39 | } 40 | 41 | @vertex 42 | fn vs_init(@builtin(vertex_index) vi: u32) -> UvOutput { 43 | var vo: UvOutput; 44 | let uv = vec2(2.0 * f32(vi & 1u), f32(vi & 2u)); 45 | vo.position = vec4(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, 0.0, 1.0); 46 | vo.uv = uv; 47 | return vo; 48 | } 49 | 50 | @fragment 51 | fn fs_init(input: UvOutput) -> @location(0) vec4 { 52 | let dir = map_equirect_uv_to_dir(input.uv); 53 | let uv = map_equirect_dir_to_uv(dir); 54 | let dim = textureDimensions(env_main); 55 | let pixel = vec2(uv * vec2(dim)); 56 | let pdf = compute_environment_sample_pdf(pixel, dim); 57 | return vec4(0.0, pdf, length(uv - input.uv), 0.0); 58 | } 59 | -------------------------------------------------------------------------------- /examples/mini/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(irrefutable_let_patterns)] 2 | 3 | use blade_graphics as gpu; 4 | use std::{num::NonZeroU32, slice}; 5 | 6 | struct Globals { 7 | modulator: [f32; 4], 8 | input: gpu::TextureView, 9 | output: gpu::TextureView, 10 | } 11 | 12 | // Using a manual implementation of the trait 13 | // to show what's generated by the derive macro. 14 | impl gpu::ShaderData for Globals { 15 | fn layout() -> gpu::ShaderDataLayout { 16 | gpu::ShaderDataLayout { 17 | bindings: vec![ 18 | ("modulator", gpu::ShaderBinding::Plain { size: 16 }), 19 | ("input", gpu::ShaderBinding::Texture), 20 | ("output", gpu::ShaderBinding::Texture), 21 | ], 22 | } 23 | } 24 | fn fill(&self, mut ctx: gpu::PipelineContext) { 25 | use gpu::ShaderBindable as _; 26 | self.modulator.bind_to(&mut ctx, 0); 27 | self.input.bind_to(&mut ctx, 1); 28 | self.output.bind_to(&mut ctx, 2); 29 | } 30 | } 31 | 32 | fn main() { 33 | env_logger::init(); 34 | let context = unsafe { gpu::Context::init(gpu::ContextDesc::default()).unwrap() }; 35 | 36 | let global_layout = ::layout(); 37 | let shader_source = std::fs::read_to_string("examples/mini/shader.wgsl").unwrap(); 38 | let shader = context.create_shader(gpu::ShaderDesc { 39 | source: &shader_source, 40 | }); 41 | 42 | let pipeline = context.create_compute_pipeline(gpu::ComputePipelineDesc { 43 | name: "main", 44 | data_layouts: &[&global_layout], 45 | compute: shader.at("main"), 46 | }); 47 | 48 | let extent = gpu::Extent { 49 | width: 16, 50 | height: 16, 51 | depth: 1, 52 | }; 53 | let mip_level_count = extent.max_mip_levels(); 54 | let texture = context.create_texture(gpu::TextureDesc { 55 | name: "input", 56 | format: gpu::TextureFormat::Rgba8Unorm, 57 | size: extent, 58 | dimension: gpu::TextureDimension::D2, 59 | array_layer_count: 1, 60 | mip_level_count, 61 | usage: gpu::TextureUsage::RESOURCE | gpu::TextureUsage::STORAGE | gpu::TextureUsage::COPY, 62 | sample_count: 1, 63 | external: None, 64 | }); 65 | let views = (0..mip_level_count) 66 | .map(|i| { 67 | context.create_texture_view( 68 | texture, 69 | gpu::TextureViewDesc { 70 | name: &format!("mip-{}", i), 71 | format: gpu::TextureFormat::Rgba8Unorm, 72 | dimension: gpu::ViewDimension::D2, 73 | subresources: &gpu::TextureSubresources { 74 | base_mip_level: i, 75 | mip_level_count: NonZeroU32::new(1), 76 | base_array_layer: 0, 77 | array_layer_count: None, 78 | }, 79 | }, 80 | ) 81 | }) 82 | .collect::>(); 83 | 84 | let result_buffer = context.create_buffer(gpu::BufferDesc { 85 | name: "result", 86 | size: 4, 87 | memory: gpu::Memory::Shared, 88 | }); 89 | 90 | let upload_buffer = context.create_buffer(gpu::BufferDesc { 91 | name: "staging", 92 | size: (extent.width * extent.height) as u64 * 4, 93 | memory: gpu::Memory::Upload, 94 | }); 95 | { 96 | let data = unsafe { 97 | slice::from_raw_parts_mut( 98 | upload_buffer.data() as *mut u32, 99 | (extent.width * extent.height) as usize, 100 | ) 101 | }; 102 | for y in 0..extent.height { 103 | for x in 0..extent.width { 104 | data[(y * extent.width + x) as usize] = y * x; 105 | } 106 | } 107 | } 108 | 109 | let mut command_encoder = context.create_command_encoder(gpu::CommandEncoderDesc { 110 | name: "main", 111 | buffer_count: 1, 112 | }); 113 | command_encoder.start(); 114 | command_encoder.init_texture(texture); 115 | 116 | if let mut transfer = command_encoder.transfer("gen-mips") { 117 | transfer.copy_buffer_to_texture( 118 | upload_buffer.into(), 119 | extent.width * 4, 120 | texture.into(), 121 | extent, 122 | ); 123 | } 124 | for i in 1..mip_level_count { 125 | if let mut compute = command_encoder.compute("generate mips") { 126 | if let mut pc = compute.with(&pipeline) { 127 | let groups = pipeline.get_dispatch_for(extent.at_mip_level(i)); 128 | pc.bind( 129 | 0, 130 | &Globals { 131 | modulator: if i == 1 { 132 | [0.2, 0.4, 0.3, 0.0] 133 | } else { 134 | [1.0; 4] 135 | }, 136 | input: views[i as usize - 1], 137 | output: views[i as usize], 138 | }, 139 | ); 140 | pc.dispatch(groups); 141 | } 142 | } 143 | } 144 | if let mut tranfer = command_encoder.transfer("init 1x2 texture") { 145 | tranfer.copy_texture_to_buffer( 146 | gpu::TexturePiece { 147 | texture, 148 | mip_level: mip_level_count - 1, 149 | array_layer: 0, 150 | origin: Default::default(), 151 | }, 152 | result_buffer.into(), 153 | 4, 154 | gpu::Extent { 155 | width: 1, 156 | height: 1, 157 | depth: 1, 158 | }, 159 | ); 160 | } 161 | let sync_point = context.submit(&mut command_encoder); 162 | 163 | let ok = context.wait_for(&sync_point, 1000); 164 | assert!(ok); 165 | let answer = unsafe { *(result_buffer.data() as *mut u32) }; 166 | println!("Output: 0x{:x}", answer); 167 | 168 | context.destroy_command_encoder(&mut command_encoder); 169 | context.destroy_buffer(result_buffer); 170 | context.destroy_buffer(upload_buffer); 171 | for view in views { 172 | context.destroy_texture_view(view); 173 | } 174 | context.destroy_texture(texture); 175 | } 176 | -------------------------------------------------------------------------------- /examples/mini/shader.wgsl: -------------------------------------------------------------------------------- 1 | var input: texture_2d; 2 | var output: texture_storage_2d; 3 | 4 | var modulator: vec4; 5 | 6 | @compute 7 | @workgroup_size(8, 8) 8 | fn main(@builtin(global_invocation_id) global_id: vec3) { 9 | let src_dim = vec2(textureDimensions(input).xy); 10 | if (any(global_id.xy * 2u >= src_dim)) { 11 | return; 12 | } 13 | let tc = vec2(global_id.xy); 14 | var sum = textureLoad(input, tc * 2, 0); 15 | var count = 1.0; 16 | if (global_id.x * 2u + 1u < src_dim.x) { 17 | sum += textureLoad(input, tc * 2 + vec2(1, 0), 0); 18 | count += 1.0; 19 | } 20 | if (global_id.y * 2u + 1u < src_dim.y) { 21 | sum += textureLoad(input, tc * 2 + vec2(0, 1), 0); 22 | count += 1.0; 23 | } 24 | if (all(global_id.xy * 2u + 1u < src_dim)) { 25 | sum += textureLoad(input, tc * 2 + vec2(1, 1), 0); 26 | count += 1.0; 27 | } 28 | textureStore(output, tc, sum * modulator / count); 29 | } 30 | -------------------------------------------------------------------------------- /examples/move/data/plane.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/blade/e0ec4e720957edd51b945b64dd85605ea54bcfe5/examples/move/data/plane.glb -------------------------------------------------------------------------------- /examples/move/data/sphere.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/blade/e0ec4e720957edd51b945b64dd85605ea54bcfe5/examples/move/data/sphere.glb -------------------------------------------------------------------------------- /examples/particle/particle.wgsl: -------------------------------------------------------------------------------- 1 | struct Particle { 2 | pos: vec2, 3 | rot: f32, 4 | scale: f32, 5 | color: u32, 6 | pos_vel: vec2, 7 | rot_vel: f32, 8 | life: f32, 9 | generation: u32, 10 | } 11 | var particles: array; 12 | 13 | struct FreeList { 14 | count: atomic, 15 | data: array, 16 | } 17 | var free_list: FreeList; 18 | 19 | @compute @workgroup_size(64, 1, 1) 20 | fn reset( 21 | @builtin(global_invocation_id) global_id: vec3, 22 | @builtin(num_workgroups) num_groups: vec3, 23 | ) { 24 | let total = num_groups.x * 64u; 25 | // reversing the order because it works like a stack 26 | free_list.data[global_id.x] = total - 1u - global_id.x; 27 | var p: Particle; 28 | particles[global_id.x] = p; 29 | if (global_id.x == 0u) { 30 | atomicStore(&free_list.count, i32(total)); 31 | } 32 | } 33 | 34 | struct Parameters { 35 | life: f32, 36 | velocity: f32, 37 | scale: f32, 38 | } 39 | var parameters: Parameters; 40 | struct UpdateParams { 41 | time_delta: f32, 42 | } 43 | var update_params: UpdateParams; 44 | 45 | var emit_end: i32; 46 | 47 | @compute @workgroup_size(64, 1, 1) 48 | fn emit(@builtin(local_invocation_index) local_index: u32) { 49 | if (local_index == 0u) { 50 | emit_end = atomicSub(&free_list.count, 64); 51 | if (emit_end < 64) { 52 | atomicAdd(&free_list.count, 64 - max(0, emit_end)); 53 | } 54 | } 55 | workgroupBarrier(); 56 | 57 | let list_index = emit_end - 1 - i32(local_index); 58 | if (list_index >= 0) { 59 | var p: Particle; 60 | let p_index = free_list.data[list_index]; 61 | p.generation += 1u; 62 | let random = i32(p_index * p.generation); 63 | p.life = max(1.0, parameters.life * f32((random + 17) % 20) / 20.0); 64 | p.scale = max(0.1, parameters.scale * f32((random + 13) % 10) / 10.0); 65 | p.color = ((p_index * 12345678u + p.generation * 912123u) << 8u) | 0xFFu; 66 | let angle = f32(random) * 0.3; 67 | let a_sin = sin(angle); 68 | let a_cos = cos(angle); 69 | p.pos_vel = parameters.velocity * vec2(a_cos, a_sin); 70 | p.rot_vel = f32((list_index + 189) % 10); 71 | particles[p_index] = p; 72 | } 73 | } 74 | 75 | @compute @workgroup_size(64, 1, 1) 76 | fn update(@builtin(global_invocation_id) global_id: vec3) { 77 | let p = &particles[global_id.x]; 78 | if ((*p).scale != 0.0) { 79 | (*p).pos += (*p).pos_vel * update_params.time_delta; 80 | (*p).rot += (*p).rot_vel * update_params.time_delta; 81 | (*p).life -= update_params.time_delta; 82 | if ((*p).life < 0.0) { 83 | let list_index = atomicAdd(&free_list.count, 1); 84 | free_list.data[list_index] = global_id.x; 85 | (*p).scale = 0.0; 86 | } 87 | } 88 | } 89 | 90 | var draw_particles: array; 91 | 92 | struct Transform2D { 93 | pos: vec2, 94 | scale: f32, 95 | rot: f32, 96 | } 97 | 98 | struct DrawParams { 99 | t_emitter: Transform2D, 100 | screen_center: vec2, 101 | screen_extent: vec2, 102 | } 103 | var draw_params: DrawParams; 104 | 105 | struct VertexOutput { 106 | @builtin(position) proj_pos: vec4, 107 | @location(0) color: u32, 108 | } 109 | 110 | fn transform(t: Transform2D, pos: vec2) -> vec2 { 111 | let rc = cos(t.rot); 112 | let rs = sin(t.rot); 113 | return t.scale * vec2(rc*pos.x - rs*pos.y, rs*pos.x + rc*pos.y) + t.pos; 114 | } 115 | 116 | @vertex 117 | fn draw_vs( 118 | @builtin(vertex_index) vertex_index: u32, 119 | @builtin(instance_index) instance_index: u32, 120 | ) -> VertexOutput { 121 | let particle = draw_particles[instance_index]; 122 | var out: VertexOutput; 123 | let zero_one_pos = vec2(vec2(vertex_index&1u, vertex_index>>1u)); 124 | let pt = Transform2D(particle.pos, particle.scale, particle.rot); 125 | let emitter_pos = transform(pt, 2.0 * zero_one_pos - vec2(1.0)); 126 | let world_pos = transform(draw_params.t_emitter, emitter_pos); 127 | out.proj_pos = vec4((world_pos - draw_params.screen_center) / draw_params.screen_extent, 0.0, 1.0); 128 | out.color = particle.color; 129 | return out; 130 | } 131 | 132 | @fragment 133 | fn draw_fs(in: VertexOutput) -> @location(0) vec4 { 134 | //TODO: texture fetch 135 | return unpack4x8unorm(in.color); 136 | } 137 | -------------------------------------------------------------------------------- /examples/ray-query/shader.wgsl: -------------------------------------------------------------------------------- 1 | const MAX_BOUNCES: i32 = 3; 2 | 3 | struct Parameters { 4 | cam_position: vec3, 5 | depth: f32, 6 | cam_orientation: vec4, 7 | fov: vec2, 8 | torus_radius: f32, 9 | rotation_angle: f32, 10 | }; 11 | 12 | var parameters: Parameters; 13 | var acc_struct: acceleration_structure; 14 | var output: texture_storage_2d; 15 | 16 | fn qmake(axis: vec3, angle: f32) -> vec4 { 17 | return vec4(axis * sin(angle), cos(angle)); 18 | } 19 | fn qrot(q: vec4, v: vec3) -> vec3 { 20 | return v + 2.0*cross(q.xyz, cross(q.xyz,v) + q.w*v); 21 | } 22 | 23 | fn get_miss_color(dir: vec3) -> vec4 { 24 | var colors = array, 4>( 25 | vec4(1.0), 26 | vec4(0.6, 0.9, 0.3, 1.0), 27 | vec4(0.3, 0.6, 0.9, 1.0), 28 | vec4(0.0) 29 | ); 30 | var thresholds = array(-1.0, -0.3, 0.4, 1.0); 31 | var i = 0; 32 | loop { 33 | if (dir.y < thresholds[i]) { 34 | let t = (dir.y - thresholds[i - 1]) / (thresholds[i] - thresholds[i - 1]); 35 | return mix(colors[i - 1], colors[i], t); 36 | } 37 | i += 1; 38 | if (i >= 4) { 39 | break; 40 | } 41 | } 42 | return colors[3]; 43 | } 44 | 45 | fn get_torus_normal(world_point: vec3, intersection: RayIntersection) -> vec3 { 46 | //Note: generally we'd store normals with the mesh data, but for the sake of 47 | // simplicity of this example it's computed analytically instead. 48 | let local_point = intersection.world_to_object * vec4(world_point, 1.0); 49 | let point_on_guiding_line = normalize(local_point.xy) * parameters.torus_radius; 50 | let world_point_on_guiding_line = intersection.object_to_world * vec4(point_on_guiding_line, 0.0, 1.0); 51 | return normalize(world_point - world_point_on_guiding_line); 52 | } 53 | 54 | @compute @workgroup_size(8, 8) 55 | fn main(@builtin(global_invocation_id) global_id: vec3) { 56 | let target_size = textureDimensions(output); 57 | let half_size = vec2(target_size >> vec2(1u)); 58 | let ndc = (vec2(global_id.xy) - half_size) / half_size; 59 | if (any(global_id.xy > target_size)) { 60 | return; 61 | } 62 | 63 | let local_dir = vec3(ndc * tan(parameters.fov), 1.0); 64 | let world_dir = normalize(qrot(parameters.cam_orientation, local_dir)); 65 | let rotator = qmake(vec3(0.0, 1.0, 0.0), parameters.rotation_angle); 66 | 67 | var num_bounces = 0; 68 | var rq: ray_query; 69 | var ray_pos = qrot(rotator, parameters.cam_position); 70 | var ray_dir = qrot(rotator, world_dir); 71 | loop { 72 | rayQueryInitialize(&rq, acc_struct, RayDesc(RAY_FLAG_NONE, 0xFFu, 0.1, parameters.depth, ray_pos, ray_dir)); 73 | rayQueryProceed(&rq); 74 | let intersection = rayQueryGetCommittedIntersection(&rq); 75 | if (intersection.kind == RAY_QUERY_INTERSECTION_NONE) { 76 | break; 77 | } 78 | 79 | ray_pos += ray_dir * intersection.t; 80 | let normal = get_torus_normal(ray_pos, intersection); 81 | ray_dir -= 2.0 * dot(ray_dir, normal) * normal; 82 | 83 | num_bounces += 1; 84 | if (num_bounces > MAX_BOUNCES) { 85 | break; 86 | } 87 | } 88 | 89 | let color = get_miss_color(ray_dir); 90 | textureStore(output, global_id.xy, color); 91 | } 92 | 93 | struct VertexOutput { 94 | @location(0) out_pos: vec2, 95 | } 96 | 97 | @vertex 98 | fn draw_vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4 { 99 | return vec4(f32(vi & 1u) * 4.0 - 1.0, f32(vi & 2u) * 2.0 - 1.0, 0.0, 1.0); 100 | } 101 | 102 | var input: texture_2d; 103 | 104 | @fragment 105 | fn draw_fs(@builtin(position) frag_coord: vec4) -> @location(0) vec4 { 106 | return textureLoad(input, vec2(frag_coord.xy), 0); 107 | } 108 | -------------------------------------------------------------------------------- /examples/scene/data/monkey.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/blade/e0ec4e720957edd51b945b64dd85605ea54bcfe5/examples/scene/data/monkey.bin -------------------------------------------------------------------------------- /examples/scene/data/monkey.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset" : { 3 | "generator" : "Khronos glTF Blender I/O v3.4.50", 4 | "version" : "2.0" 5 | }, 6 | "scene" : 0, 7 | "scenes" : [ 8 | { 9 | "name" : "Scene", 10 | "nodes" : [ 11 | 0, 12 | 1 13 | ] 14 | } 15 | ], 16 | "nodes" : [ 17 | { 18 | "mesh" : 0, 19 | "name" : "Suzanne", 20 | "translation" : [ 21 | 0, 22 | 1.20753812789917, 23 | 0 24 | ] 25 | }, 26 | { 27 | "mesh" : 1, 28 | "name" : "Plane", 29 | "scale" : [ 30 | 4.332225799560547, 31 | 4.386910438537598, 32 | 4.386910438537598 33 | ] 34 | } 35 | ], 36 | "materials" : [ 37 | { 38 | "doubleSided" : true, 39 | "name" : "Material.001", 40 | "pbrMetallicRoughness" : { 41 | "baseColorFactor" : [ 42 | 0.800000011920929, 43 | 0.800000011920929, 44 | 0.800000011920929, 45 | 1 46 | ], 47 | "metallicFactor" : 0, 48 | "roughnessFactor" : 0.5 49 | } 50 | }, 51 | { 52 | "doubleSided" : true, 53 | "name" : "Material.002", 54 | "pbrMetallicRoughness" : { 55 | "baseColorFactor" : [ 56 | 0.800000011920929, 57 | 0.800000011920929, 58 | 0.800000011920929, 59 | 1 60 | ], 61 | "metallicFactor" : 0, 62 | "roughnessFactor" : 0.5 63 | } 64 | } 65 | ], 66 | "meshes" : [ 67 | { 68 | "name" : "Suzanne", 69 | "primitives" : [ 70 | { 71 | "attributes" : { 72 | "POSITION" : 0, 73 | "TEXCOORD_0" : 1, 74 | "NORMAL" : 2 75 | }, 76 | "indices" : 3, 77 | "material" : 0 78 | } 79 | ] 80 | }, 81 | { 82 | "name" : "Plane", 83 | "primitives" : [ 84 | { 85 | "attributes" : { 86 | "POSITION" : 4, 87 | "TEXCOORD_0" : 5, 88 | "NORMAL" : 6 89 | }, 90 | "indices" : 7, 91 | "material" : 1 92 | } 93 | ] 94 | } 95 | ], 96 | "accessors" : [ 97 | { 98 | "bufferView" : 0, 99 | "componentType" : 5126, 100 | "count" : 1966, 101 | "max" : [ 102 | 1.3671875, 103 | 0.984375, 104 | 0.8515625 105 | ], 106 | "min" : [ 107 | -1.3671875, 108 | -0.984375, 109 | -0.8515625 110 | ], 111 | "type" : "VEC3" 112 | }, 113 | { 114 | "bufferView" : 1, 115 | "componentType" : 5126, 116 | "count" : 1966, 117 | "type" : "VEC2" 118 | }, 119 | { 120 | "bufferView" : 2, 121 | "componentType" : 5126, 122 | "count" : 1966, 123 | "type" : "VEC3" 124 | }, 125 | { 126 | "bufferView" : 3, 127 | "componentType" : 5123, 128 | "count" : 2904, 129 | "type" : "SCALAR" 130 | }, 131 | { 132 | "bufferView" : 4, 133 | "componentType" : 5126, 134 | "count" : 4, 135 | "max" : [ 136 | 1, 137 | 0, 138 | 1 139 | ], 140 | "min" : [ 141 | -1, 142 | 0, 143 | -1 144 | ], 145 | "type" : "VEC3" 146 | }, 147 | { 148 | "bufferView" : 5, 149 | "componentType" : 5126, 150 | "count" : 4, 151 | "type" : "VEC2" 152 | }, 153 | { 154 | "bufferView" : 6, 155 | "componentType" : 5126, 156 | "count" : 4, 157 | "type" : "VEC3" 158 | }, 159 | { 160 | "bufferView" : 7, 161 | "componentType" : 5123, 162 | "count" : 6, 163 | "type" : "SCALAR" 164 | } 165 | ], 166 | "bufferViews" : [ 167 | { 168 | "buffer" : 0, 169 | "byteLength" : 23592, 170 | "byteOffset" : 0, 171 | "target" : 34962 172 | }, 173 | { 174 | "buffer" : 0, 175 | "byteLength" : 15728, 176 | "byteOffset" : 23592, 177 | "target" : 34962 178 | }, 179 | { 180 | "buffer" : 0, 181 | "byteLength" : 23592, 182 | "byteOffset" : 39320, 183 | "target" : 34962 184 | }, 185 | { 186 | "buffer" : 0, 187 | "byteLength" : 5808, 188 | "byteOffset" : 62912, 189 | "target" : 34963 190 | }, 191 | { 192 | "buffer" : 0, 193 | "byteLength" : 48, 194 | "byteOffset" : 68720, 195 | "target" : 34962 196 | }, 197 | { 198 | "buffer" : 0, 199 | "byteLength" : 32, 200 | "byteOffset" : 68768, 201 | "target" : 34962 202 | }, 203 | { 204 | "buffer" : 0, 205 | "byteLength" : 48, 206 | "byteOffset" : 68800, 207 | "target" : 34962 208 | }, 209 | { 210 | "buffer" : 0, 211 | "byteLength" : 12, 212 | "byteOffset" : 68848, 213 | "target" : 34963 214 | } 215 | ], 216 | "buffers" : [ 217 | { 218 | "byteLength" : 68860, 219 | "uri" : "monkey.bin" 220 | } 221 | ] 222 | } 223 | -------------------------------------------------------------------------------- /examples/scene/data/plane.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/blade/e0ec4e720957edd51b945b64dd85605ea54bcfe5/examples/scene/data/plane.glb -------------------------------------------------------------------------------- /examples/scene/data/scene.ron: -------------------------------------------------------------------------------- 1 | ( 2 | camera: ( 3 | position: (2.7, 1.6, 2.1), 4 | orientation: (-0.07, 0.36, 0.01, 0.93), 5 | fov_y: 1.0, 6 | max_depth: 100.0, 7 | speed: 1000.0, 8 | ), 9 | average_luminocity: 0.3, 10 | objects: [ 11 | ( 12 | path: "plane.glb", 13 | ), 14 | ], 15 | ) 16 | -------------------------------------------------------------------------------- /examples/vehicle/config.rs: -------------------------------------------------------------------------------- 1 | #[derive(serde::Deserialize)] 2 | pub struct Body { 3 | pub visual: blade::config::Visual, 4 | pub collider: blade::config::Collider, 5 | } 6 | 7 | #[derive(serde::Deserialize)] 8 | pub struct Wheel { 9 | pub visual: blade::config::Visual, 10 | pub collider: blade::config::Collider, 11 | } 12 | 13 | #[derive(serde::Deserialize)] 14 | pub struct Axle { 15 | /// Side offset for each wheel. 16 | pub x_wheels: Vec, 17 | /// Height offset from the body. 18 | pub y: f32, 19 | /// Forward offset from the body. 20 | pub z: f32, 21 | #[serde(default)] 22 | pub max_steering_angle: f32, 23 | #[serde(default)] 24 | pub max_suspension_offset: f32, 25 | #[serde(default)] 26 | pub suspension: blade::config::Motor, 27 | #[serde(default)] 28 | pub steering: blade::config::Motor, 29 | } 30 | 31 | fn default_additional_mass() -> blade::config::AdditionalMass { 32 | blade::config::AdditionalMass { 33 | density: 0.0, 34 | shape: blade::config::Shape::Ball { radius: 0.0 }, 35 | } 36 | } 37 | 38 | #[derive(serde::Deserialize)] 39 | pub struct Vehicle { 40 | pub body: Body, 41 | pub wheel: Wheel, 42 | #[serde(default = "default_additional_mass")] 43 | pub suspender: blade::config::AdditionalMass, 44 | pub drive_factor: f32, 45 | pub jump_impulse: f32, 46 | pub roll_impulse: f32, 47 | pub axles: Vec, 48 | } 49 | 50 | #[derive(serde::Deserialize)] 51 | pub struct Level { 52 | #[serde(default)] 53 | pub environment: String, 54 | pub gravity: f32, 55 | pub average_luminocity: f32, 56 | pub spawn_pos: [f32; 3], 57 | pub ground: blade::config::Object, 58 | } 59 | 60 | #[derive(serde::Deserialize)] 61 | pub struct Camera { 62 | pub azimuth: f32, 63 | pub altitude: f32, 64 | pub distance: f32, 65 | pub speed: f32, 66 | pub target: [f32; 3], 67 | pub fov: f32, 68 | } 69 | -------------------------------------------------------------------------------- /examples/vehicle/data/ground.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/blade/e0ec4e720957edd51b945b64dd85605ea54bcfe5/examples/vehicle/data/ground.bin -------------------------------------------------------------------------------- /examples/vehicle/data/ground.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset":{ 3 | "generator":"Khronos glTF Blender I/O v4.0.44", 4 | "version":"2.0" 5 | }, 6 | "scene":0, 7 | "scenes":[ 8 | { 9 | "name":"Scene", 10 | "nodes":[ 11 | 0, 12 | 1, 13 | 2, 14 | 3, 15 | 4 16 | ] 17 | } 18 | ], 19 | "nodes":[ 20 | { 21 | "mesh":0, 22 | "name":"Plane", 23 | "scale":[ 24 | 20, 25 | 20, 26 | 20 27 | ] 28 | }, 29 | { 30 | "mesh":1, 31 | "name":"Plane.001", 32 | "rotation":[ 33 | 0, 34 | 0, 35 | -0.7071068286895752, 36 | 0.7071068286895752 37 | ], 38 | "scale":[ 39 | 1, 40 | 1, 41 | 20 42 | ], 43 | "translation":[ 44 | 20, 45 | 1, 46 | 0 47 | ] 48 | }, 49 | { 50 | "mesh":2, 51 | "name":"Plane.002", 52 | "rotation":[ 53 | 0, 54 | 0, 55 | -0.7071068286895752, 56 | 0.7071068286895752 57 | ], 58 | "scale":[ 59 | 1, 60 | 1, 61 | 20 62 | ], 63 | "translation":[ 64 | -20, 65 | 1, 66 | 0 67 | ] 68 | }, 69 | { 70 | "mesh":3, 71 | "name":"Plane.003", 72 | "rotation":[ 73 | -0.5, 74 | 0.5, 75 | -0.5, 76 | 0.5 77 | ], 78 | "scale":[ 79 | 1, 80 | 1, 81 | 20 82 | ], 83 | "translation":[ 84 | 0, 85 | 1, 86 | -20 87 | ] 88 | }, 89 | { 90 | "mesh":4, 91 | "name":"Plane.004", 92 | "rotation":[ 93 | -0.5, 94 | 0.5, 95 | -0.5, 96 | 0.5 97 | ], 98 | "scale":[ 99 | 1, 100 | 1, 101 | 20 102 | ], 103 | "translation":[ 104 | 0, 105 | 1, 106 | 20 107 | ] 108 | } 109 | ], 110 | "materials":[ 111 | { 112 | "doubleSided":true, 113 | "name":"Material.001", 114 | "pbrMetallicRoughness":{ 115 | "baseColorTexture":{ 116 | "index":0 117 | }, 118 | "metallicFactor":0, 119 | "roughnessFactor":0.5 120 | } 121 | }, 122 | { 123 | "doubleSided":true, 124 | "name":"Material.002", 125 | "pbrMetallicRoughness":{ 126 | "baseColorFactor":[ 127 | 0.800000011920929, 128 | 0.800000011920929, 129 | 0.800000011920929, 130 | 1 131 | ], 132 | "metallicFactor":0, 133 | "roughnessFactor":0.5 134 | } 135 | } 136 | ], 137 | "meshes":[ 138 | { 139 | "name":"Plane", 140 | "primitives":[ 141 | { 142 | "attributes":{ 143 | "POSITION":0, 144 | "NORMAL":1, 145 | "TEXCOORD_0":2 146 | }, 147 | "indices":3, 148 | "material":0 149 | } 150 | ] 151 | }, 152 | { 153 | "name":"Plane.001", 154 | "primitives":[ 155 | { 156 | "attributes":{ 157 | "POSITION":4, 158 | "NORMAL":5, 159 | "TEXCOORD_0":6 160 | }, 161 | "indices":3, 162 | "material":1 163 | } 164 | ] 165 | }, 166 | { 167 | "name":"Plane.002", 168 | "primitives":[ 169 | { 170 | "attributes":{ 171 | "POSITION":7, 172 | "NORMAL":8, 173 | "TEXCOORD_0":9 174 | }, 175 | "indices":3, 176 | "material":1 177 | } 178 | ] 179 | }, 180 | { 181 | "name":"Plane.003", 182 | "primitives":[ 183 | { 184 | "attributes":{ 185 | "POSITION":10, 186 | "NORMAL":11, 187 | "TEXCOORD_0":12 188 | }, 189 | "indices":3, 190 | "material":1 191 | } 192 | ] 193 | }, 194 | { 195 | "name":"Plane.004", 196 | "primitives":[ 197 | { 198 | "attributes":{ 199 | "POSITION":13, 200 | "NORMAL":14, 201 | "TEXCOORD_0":15 202 | }, 203 | "indices":3, 204 | "material":1 205 | } 206 | ] 207 | } 208 | ], 209 | "textures":[ 210 | { 211 | "sampler":0, 212 | "source":0 213 | } 214 | ], 215 | "images":[ 216 | { 217 | "mimeType":"image/png", 218 | "name":"orange_light_grid", 219 | "uri":"orange_light_grid.png" 220 | } 221 | ], 222 | "accessors":[ 223 | { 224 | "bufferView":0, 225 | "componentType":5126, 226 | "count":4, 227 | "max":[ 228 | 1, 229 | 0, 230 | 1 231 | ], 232 | "min":[ 233 | -1, 234 | 0, 235 | -1 236 | ], 237 | "type":"VEC3" 238 | }, 239 | { 240 | "bufferView":1, 241 | "componentType":5126, 242 | "count":4, 243 | "type":"VEC3" 244 | }, 245 | { 246 | "bufferView":2, 247 | "componentType":5126, 248 | "count":4, 249 | "type":"VEC2" 250 | }, 251 | { 252 | "bufferView":3, 253 | "componentType":5123, 254 | "count":6, 255 | "type":"SCALAR" 256 | }, 257 | { 258 | "bufferView":4, 259 | "componentType":5126, 260 | "count":4, 261 | "max":[ 262 | 1, 263 | 0, 264 | 1 265 | ], 266 | "min":[ 267 | -1, 268 | 0, 269 | -1 270 | ], 271 | "type":"VEC3" 272 | }, 273 | { 274 | "bufferView":5, 275 | "componentType":5126, 276 | "count":4, 277 | "type":"VEC3" 278 | }, 279 | { 280 | "bufferView":6, 281 | "componentType":5126, 282 | "count":4, 283 | "type":"VEC2" 284 | }, 285 | { 286 | "bufferView":7, 287 | "componentType":5126, 288 | "count":4, 289 | "max":[ 290 | 1, 291 | 0, 292 | 1 293 | ], 294 | "min":[ 295 | -1, 296 | 0, 297 | -1 298 | ], 299 | "type":"VEC3" 300 | }, 301 | { 302 | "bufferView":8, 303 | "componentType":5126, 304 | "count":4, 305 | "type":"VEC3" 306 | }, 307 | { 308 | "bufferView":9, 309 | "componentType":5126, 310 | "count":4, 311 | "type":"VEC2" 312 | }, 313 | { 314 | "bufferView":10, 315 | "componentType":5126, 316 | "count":4, 317 | "max":[ 318 | 1, 319 | 0, 320 | 1 321 | ], 322 | "min":[ 323 | -1, 324 | 0, 325 | -1 326 | ], 327 | "type":"VEC3" 328 | }, 329 | { 330 | "bufferView":11, 331 | "componentType":5126, 332 | "count":4, 333 | "type":"VEC3" 334 | }, 335 | { 336 | "bufferView":12, 337 | "componentType":5126, 338 | "count":4, 339 | "type":"VEC2" 340 | }, 341 | { 342 | "bufferView":13, 343 | "componentType":5126, 344 | "count":4, 345 | "max":[ 346 | 1, 347 | 0, 348 | 1 349 | ], 350 | "min":[ 351 | -1, 352 | 0, 353 | -1 354 | ], 355 | "type":"VEC3" 356 | }, 357 | { 358 | "bufferView":14, 359 | "componentType":5126, 360 | "count":4, 361 | "type":"VEC3" 362 | }, 363 | { 364 | "bufferView":15, 365 | "componentType":5126, 366 | "count":4, 367 | "type":"VEC2" 368 | } 369 | ], 370 | "bufferViews":[ 371 | { 372 | "buffer":0, 373 | "byteLength":48, 374 | "byteOffset":0, 375 | "target":34962 376 | }, 377 | { 378 | "buffer":0, 379 | "byteLength":48, 380 | "byteOffset":48, 381 | "target":34962 382 | }, 383 | { 384 | "buffer":0, 385 | "byteLength":32, 386 | "byteOffset":96, 387 | "target":34962 388 | }, 389 | { 390 | "buffer":0, 391 | "byteLength":12, 392 | "byteOffset":128, 393 | "target":34963 394 | }, 395 | { 396 | "buffer":0, 397 | "byteLength":48, 398 | "byteOffset":140, 399 | "target":34962 400 | }, 401 | { 402 | "buffer":0, 403 | "byteLength":48, 404 | "byteOffset":188, 405 | "target":34962 406 | }, 407 | { 408 | "buffer":0, 409 | "byteLength":32, 410 | "byteOffset":236, 411 | "target":34962 412 | }, 413 | { 414 | "buffer":0, 415 | "byteLength":48, 416 | "byteOffset":268, 417 | "target":34962 418 | }, 419 | { 420 | "buffer":0, 421 | "byteLength":48, 422 | "byteOffset":316, 423 | "target":34962 424 | }, 425 | { 426 | "buffer":0, 427 | "byteLength":32, 428 | "byteOffset":364, 429 | "target":34962 430 | }, 431 | { 432 | "buffer":0, 433 | "byteLength":48, 434 | "byteOffset":396, 435 | "target":34962 436 | }, 437 | { 438 | "buffer":0, 439 | "byteLength":48, 440 | "byteOffset":444, 441 | "target":34962 442 | }, 443 | { 444 | "buffer":0, 445 | "byteLength":32, 446 | "byteOffset":492, 447 | "target":34962 448 | }, 449 | { 450 | "buffer":0, 451 | "byteLength":48, 452 | "byteOffset":524, 453 | "target":34962 454 | }, 455 | { 456 | "buffer":0, 457 | "byteLength":48, 458 | "byteOffset":572, 459 | "target":34962 460 | }, 461 | { 462 | "buffer":0, 463 | "byteLength":32, 464 | "byteOffset":620, 465 | "target":34962 466 | } 467 | ], 468 | "samplers":[ 469 | { 470 | "magFilter":9729, 471 | "minFilter":9987 472 | } 473 | ], 474 | "buffers":[ 475 | { 476 | "byteLength":652, 477 | "uri":"ground.bin" 478 | } 479 | ] 480 | } 481 | -------------------------------------------------------------------------------- /examples/vehicle/data/level.ron: -------------------------------------------------------------------------------- 1 | ( 2 | environment: "", 3 | gravity: 9.81, 4 | average_luminocity: 0.1, 5 | spawn_pos: (0, 2, 0), 6 | ground: ( 7 | name: "ground", 8 | visuals: [ 9 | ( 10 | model: "ground.gltf", 11 | pos: (0, 1, 0), 12 | ), 13 | ], 14 | colliders: [ 15 | ( 16 | density: 1.0, 17 | friction: 1.0, 18 | shape: Cuboid( 19 | half: (20, 1, 20), 20 | ), 21 | ), 22 | ( 23 | density: 1.0, 24 | friction: 1.0, 25 | shape: Cuboid( 26 | half: (20, 1, 1), 27 | ), 28 | pos: (21, 2, 0), 29 | rot: (0, 90, 0), 30 | ), 31 | ( 32 | density: 1.0, 33 | friction: 1.0, 34 | shape: Cuboid( 35 | half: (20, 1, 1), 36 | ), 37 | pos: (-21, 2, 0), 38 | rot: (0, 90, 0), 39 | ), 40 | ( 41 | density: 1.0, 42 | friction: 1.0, 43 | shape: Cuboid( 44 | half: (20, 1, 1), 45 | ), 46 | pos: (0, 2, 21), 47 | rot: (0, 0, 0), 48 | ), 49 | ( 50 | density: 1.0, 51 | friction: 1.0, 52 | shape: Cuboid( 53 | half: (20, 1, 1), 54 | ), 55 | pos: (0, 2, -21), 56 | rot: (0, 0, 0), 57 | ), 58 | ], 59 | ), 60 | ) 61 | -------------------------------------------------------------------------------- /examples/vehicle/data/orange_light_grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/blade/e0ec4e720957edd51b945b64dd85605ea54bcfe5/examples/vehicle/data/orange_light_grid.png -------------------------------------------------------------------------------- /examples/vehicle/data/raceFuture-body.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/blade/e0ec4e720957edd51b945b64dd85605ea54bcfe5/examples/vehicle/data/raceFuture-body.glb -------------------------------------------------------------------------------- /examples/vehicle/data/raceFuture.ron: -------------------------------------------------------------------------------- 1 | ( 2 | body: ( 3 | visual: ( 4 | model: "raceFuture-body.glb", 5 | pos: (0, -0.3, 0.1), 6 | rot: (0, 180, 0), 7 | ), 8 | collider: ( 9 | density: 100.0, 10 | shape: Cuboid( 11 | half: (0.65, 0.2, 1.2), 12 | ), 13 | ), 14 | ), 15 | wheel: ( 16 | visual: ( 17 | model: "wheelRacing.glb", 18 | ), 19 | collider: ( 20 | density: 100.0, 21 | friction: 1.0, 22 | rot: (0, 0, 90), 23 | shape: Cylinder( 24 | half_height: 0.1, 25 | radius: 0.28, 26 | ), 27 | ), 28 | ), 29 | suspender: ( 30 | density: 100.0, 31 | shape: Ball( 32 | radius: 0.28, 33 | ), 34 | ), 35 | drive_factor: 100.0, 36 | jump_impulse: 10, 37 | roll_impulse: 10, 38 | axles: [ 39 | ( 40 | x_wheels: [-0.5, 0.5], 41 | y: -0.1, 42 | z: 0.7, 43 | max_steering_angle: 30, 44 | max_suspension_offset: 0.02, 45 | steering: ( 46 | stiffness: 100000, 47 | damping: 10000, 48 | max_force: 1000, 49 | ), 50 | suspension: ( 51 | stiffness: 100000, 52 | damping: 10000, 53 | max_force: 1000, 54 | ), 55 | ), 56 | ( 57 | x_wheels: [-0.5, 0.5], 58 | y: -0.1, 59 | z: -0.8, 60 | max_suspension_offset: 0.02, 61 | suspension: ( 62 | stiffness: 100000, 63 | damping: 10000, 64 | max_force: 1000, 65 | ), 66 | ), 67 | ], 68 | ) 69 | -------------------------------------------------------------------------------- /examples/vehicle/data/wheelRacing.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kvark/blade/e0ec4e720957edd51b945b64dd85605ea54bcfe5/examples/vehicle/data/wheelRacing.glb -------------------------------------------------------------------------------- /run-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "run-wasm" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | cargo-run-wasm = "0.3.0" 9 | -------------------------------------------------------------------------------- /run-wasm/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | cargo_run_wasm::run_wasm_with_css("body { margin: 0px; }"); 3 | } 4 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | #[derive(serde::Deserialize)] 2 | pub enum FrontFace { 3 | Cw, 4 | Ccw, 5 | } 6 | impl Default for FrontFace { 7 | fn default() -> Self { 8 | Self::Ccw 9 | } 10 | } 11 | 12 | fn default_vec() -> mint::Vector3 { 13 | [0.0; 3].into() 14 | } 15 | fn default_scale() -> f32 { 16 | 1.0 17 | } 18 | 19 | #[derive(serde::Deserialize)] 20 | pub struct Visual { 21 | pub model: String, 22 | #[serde(default)] 23 | pub front_face: FrontFace, 24 | #[serde(default = "default_vec")] 25 | pub pos: mint::Vector3, 26 | #[serde(default = "default_vec")] 27 | pub rot: mint::Vector3, 28 | #[serde(default = "default_scale")] 29 | pub scale: f32, 30 | } 31 | impl Default for Visual { 32 | fn default() -> Self { 33 | Self { 34 | model: String::new(), 35 | front_face: FrontFace::default(), 36 | pos: default_vec(), 37 | rot: default_vec(), 38 | scale: default_scale(), 39 | } 40 | } 41 | } 42 | 43 | #[derive(serde::Deserialize)] 44 | pub enum Shape { 45 | Ball { 46 | radius: f32, 47 | }, 48 | Cylinder { 49 | half_height: f32, 50 | radius: f32, 51 | }, 52 | Cuboid { 53 | half: mint::Vector3, 54 | }, 55 | ConvexHull { 56 | points: Vec>, 57 | #[serde(default)] 58 | border_radius: f32, 59 | }, 60 | TriMesh { 61 | model: String, 62 | #[serde(default)] 63 | convex: bool, 64 | #[serde(default)] 65 | border_radius: f32, 66 | }, 67 | } 68 | 69 | fn default_friction() -> f32 { 70 | 1.0 71 | } 72 | fn default_restitution() -> f32 { 73 | 0.0 74 | } 75 | 76 | #[derive(serde::Deserialize)] 77 | pub struct Collider { 78 | pub density: f32, 79 | pub shape: Shape, 80 | #[serde(default = "default_friction")] 81 | pub friction: f32, 82 | #[serde(default = "default_restitution")] 83 | pub restitution: f32, 84 | #[serde(default = "default_vec")] 85 | pub pos: mint::Vector3, 86 | #[serde(default = "default_vec")] 87 | pub rot: mint::Vector3, 88 | } 89 | 90 | #[derive(serde::Deserialize)] 91 | pub struct AdditionalMass { 92 | pub density: f32, 93 | pub shape: Shape, 94 | } 95 | 96 | #[derive(serde::Deserialize)] 97 | pub struct Object { 98 | pub name: String, 99 | pub visuals: Vec, 100 | pub colliders: Vec, 101 | #[serde(default)] 102 | pub additional_mass: Option, 103 | } 104 | 105 | #[derive(Clone, Copy, Debug, Default, PartialEq, serde::Deserialize)] 106 | pub struct Motor { 107 | pub stiffness: f32, 108 | pub damping: f32, 109 | pub max_force: f32, 110 | } 111 | 112 | fn default_time_step() -> f32 { 113 | 0.01 114 | } 115 | 116 | #[derive(serde::Deserialize)] 117 | pub struct Engine { 118 | pub shader_path: String, 119 | pub data_path: String, 120 | #[serde(default = "default_time_step")] 121 | pub time_step: f32, 122 | } 123 | -------------------------------------------------------------------------------- /src/trimesh.rs: -------------------------------------------------------------------------------- 1 | use std::unimplemented; 2 | 3 | #[derive(Default)] 4 | pub struct TriMesh { 5 | pub points: Vec>, 6 | pub triangles: Vec<[u32; 3]>, 7 | } 8 | 9 | impl TriMesh { 10 | fn populate_from_gltf( 11 | &mut self, 12 | g_node: gltf::Node, 13 | parent_transform: nalgebra::Matrix4, 14 | data_buffers: &[Vec], 15 | ) { 16 | let name = g_node.name().unwrap_or(""); 17 | let transform = parent_transform * nalgebra::Matrix4::from(g_node.transform().matrix()); 18 | 19 | for child in g_node.children() { 20 | self.populate_from_gltf(child, transform, data_buffers); 21 | } 22 | 23 | let g_mesh = match g_node.mesh() { 24 | Some(mesh) => mesh, 25 | None => return, 26 | }; 27 | 28 | for (prim_index, g_primitive) in g_mesh.primitives().enumerate() { 29 | if g_primitive.mode() != gltf::mesh::Mode::Triangles { 30 | log::warn!( 31 | "Skipping primitive '{}'[{}] for having mesh mode {:?}", 32 | name, 33 | prim_index, 34 | g_primitive.mode() 35 | ); 36 | continue; 37 | } 38 | 39 | let reader = g_primitive.reader(|buffer| Some(&data_buffers[buffer.index()])); 40 | 41 | // Read the vertices into memory 42 | profiling::scope!("Read data"); 43 | let base_vertex = self.points.len() as u32; 44 | 45 | match reader.read_indices() { 46 | Some(read) => { 47 | let mut read_u32 = read.into_u32(); 48 | let tri_count = read_u32.len() / 3; 49 | for _ in 0..tri_count { 50 | let mut tri = [0u32; 3]; 51 | for index in tri.iter_mut() { 52 | *index = base_vertex + read_u32.next().unwrap(); 53 | } 54 | self.triangles.push(tri); 55 | } 56 | } 57 | None => { 58 | log::warn!("Missing index buffer for '{name}'"); 59 | continue; 60 | } 61 | } 62 | 63 | for pos in reader.read_positions().unwrap() { 64 | let point = transform.transform_point(&pos.into()); 65 | self.points.push(point); 66 | } 67 | } 68 | } 69 | } 70 | 71 | pub fn load(path: &str) -> TriMesh { 72 | use base64::engine::{general_purpose::URL_SAFE as ENCODING_ENGINE, Engine as _}; 73 | 74 | let gltf::Gltf { document, mut blob } = gltf::Gltf::open(path).unwrap(); 75 | // extract buffers 76 | let mut data_buffers = Vec::new(); 77 | for buffer in document.buffers() { 78 | let mut data = match buffer.source() { 79 | gltf::buffer::Source::Uri(uri) => { 80 | if let Some(rest) = uri.strip_prefix("data:") { 81 | let (_before, after) = rest.split_once(";base64,").unwrap(); 82 | ENCODING_ENGINE.decode(after).unwrap() 83 | } else { 84 | unimplemented!("Unexpected reference to external file: {uri}"); 85 | } 86 | } 87 | gltf::buffer::Source::Bin => blob.take().unwrap(), 88 | }; 89 | assert!(data.len() >= buffer.length()); 90 | while data.len() % 4 != 0 { 91 | data.push(0); 92 | } 93 | data_buffers.push(data); 94 | } 95 | 96 | let scene = document.scenes().next().expect("Document has no scenes?"); 97 | let mut trimesh = TriMesh::default(); 98 | for g_node in scene.nodes() { 99 | trimesh.populate_from_gltf(g_node, nalgebra::Matrix4::identity(), &data_buffers); 100 | } 101 | trimesh 102 | } 103 | -------------------------------------------------------------------------------- /tests/parse_shaders.rs: -------------------------------------------------------------------------------- 1 | use naga::{front::wgsl, valid::Validator}; 2 | use std::{collections::HashMap, fs, path::PathBuf}; 3 | 4 | /// Runs through all pass shaders and ensures they are valid WGSL. 5 | #[test] 6 | fn parse_wgsl() { 7 | let mut expansions = HashMap::default(); 8 | expansions.insert( 9 | "DebugMode".to_string(), 10 | blade_render::shader::Expansion::from_enum::(), 11 | ); 12 | 13 | let read_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) 14 | .join("examples") 15 | .read_dir() 16 | .unwrap(); 17 | 18 | for sub_entry in read_dir { 19 | let example = match sub_entry { 20 | Ok(entry) => entry.path(), 21 | Err(e) => { 22 | println!("Skipping non-example: {:?}", e); 23 | continue; 24 | } 25 | }; 26 | let dir = match example.read_dir() { 27 | Ok(dir) => dir, 28 | Err(_) => continue, 29 | }; 30 | 31 | for file in dir { 32 | let path = match file { 33 | Ok(entry) => entry.path(), 34 | Err(e) => { 35 | println!("Skipping file: {:?}", e); 36 | continue; 37 | } 38 | }; 39 | let shader_raw = match path.extension() { 40 | Some(ostr) if &*ostr == "wgsl" => { 41 | println!("Validating {:?}", path); 42 | fs::read(&path).unwrap_or_default() 43 | } 44 | _ => continue, 45 | }; 46 | 47 | let cooker = blade_asset::Cooker::new(&example, Default::default()); 48 | let text_out = blade_render::shader::parse_shader(&shader_raw, &cooker, &expansions); 49 | 50 | let module = match wgsl::parse_str(&text_out) { 51 | Ok(module) => module, 52 | Err(e) => panic!("{}", e.emit_to_string(&text_out)), 53 | }; 54 | //TODO: re-use the validator 55 | Validator::new( 56 | naga::valid::ValidationFlags::all() ^ naga::valid::ValidationFlags::BINDINGS, 57 | naga::valid::Capabilities::RAY_QUERY, 58 | ) 59 | .validate(&module) 60 | .unwrap_or_else(|e| { 61 | blade_graphics::util::emit_annotated_error(&e, "", &text_out); 62 | blade_graphics::util::print_err(&e); 63 | panic!("Shader validation failed"); 64 | }); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /vk_layer_settings.txt: -------------------------------------------------------------------------------- 1 | khronos_validation.debug_action = VK_DBG_LAYER_ACTION_LOG_MSG 2 | khronos_validation.enables = VK_VALIDATION_FEATURE_ENABLE_SYNCHRONIZATION_VALIDATION_EXT,VALIDATION_CHECK_ENABLE_SYNCHRONIZATION_VALIDATION_QUEUE_SUBMIT 3 | khronos_validation.disables = 4 | # 0x1ed8505a: WARNING-VkDescriptorSetAllocateInfo-descriptorCount - we are intentionally trying to allocate from the pool and not assuming the outcome. 5 | khronos_validation.message_id_filter = 0x1ed8505a 6 | --------------------------------------------------------------------------------