diff --git a/CHANGELOG.md b/CHANGELOG.md index ef3b86f8..6e9bcc2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.0] - 2022-12-28 + +### Added + +- Shader hot-reload feature for compute, graphic and ray-trace pipelines (see examples) +- `Buffer` objects may be created with an alignment specified in `BufferInfo` (useful for shader + binding tables) + +### Changed + +- `ComputePipeline::create` now takes three arguments: the device, info, and shader +- `ComputePipelineInfo` no longer contains shader information; use `Shader::new_compute` for that + instead + ## [0.7.1] - 2022-12-17 ### Fixed @@ -294,4 +308,5 @@ _See [#25](https://github.com/attackgoat/screen-13/pull/25) for migration detail [0.6.4]: https://crates.io/crates/screen-13/0.6.4 [0.6.5]: https://crates.io/crates/screen-13/0.6.5 [0.7.0]: https://crates.io/crates/screen-13/0.7.0 -[0.7.1]: https://crates.io/crates/screen-13/0.7.1 \ No newline at end of file +[0.7.1]: https://crates.io/crates/screen-13/0.7.1 +[0.8.0]: https://crates.io/crates/screen-13/0.8.0 \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 51a5c95a..962145a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "screen-13" -version = "0.7.1" +version = "0.8.0" authors = ["John Wells "] edition = "2021" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index 08f2285a..30819d08 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ _[QBasic](https://en.wikipedia.org/wiki/QBasic)_. ```toml [dependencies] -screen-13 = "0.7" +screen-13 = "0.8" ``` ## Overview @@ -41,6 +41,7 @@ Features of the render graph: - Automatic Vulkan management (render passes, subpasses, descriptors, pools, _etc._) - Automatic render pass scheduling, re-ordering, merging, with resource aliasing - Interoperable with existing Vulkan code + - Optional [shader hot-reload](contrib/screen-13-hot/README.md) from disk ```rust render_graph diff --git a/contrib/README.md b/contrib/README.md index 9a71d12b..53d4696d 100644 --- a/contrib/README.md +++ b/contrib/README.md @@ -18,17 +18,21 @@ debugging. A script which exercises all test cases and build conditions which must succeed prior to merging new code into the main branch. -### [`screen-13-egui/`](screen-13-egui/) +### [`screen-13-egui/`](screen-13-egui/README.md) Renderer for [egui](https://github.com/emilk/egui); a simple, fast, and highly portable immediate mode GUI library. -### [`screen-13-fx/`](screen-13-fx/) +### [`screen-13-fx/`](screen-13-fx/README.md) Pre-defined effects and tools built using _Screen 13_ features. Generally anything that requires shaders or other physical data which shouldn't be part of the main library. -### [`screen-13-imgui/`](screen-13-imgui/) +### [`screen-13-hot/`](screen-13-hot/README.md) + +Adds a hot-reload feature to compute, graphic and ray-trace shader pipelines. + +### [`screen-13-imgui/`](screen-13-imgui/README.md) Renderer for [Dear ImGui](https://github.com/imgui-rs/imgui-rs). Provides a graphical user interface useful for debug purposes. \ No newline at end of file diff --git a/contrib/rel-mgmt/check b/contrib/rel-mgmt/check index 872385a9..e7889e5f 100755 --- a/contrib/rel-mgmt/check +++ b/contrib/rel-mgmt/check @@ -18,6 +18,7 @@ diff || fail "Uncommitted changes" cargo fmt && diff || fail "Unformatted rust code" cargo fmt --manifest-path contrib/screen-13-egui/Cargo.toml && diff || fail "Unformatted rust code (screen-13-egui)" cargo fmt --manifest-path contrib/screen-13-fx/Cargo.toml && diff || fail "Unformatted rust code (screen-13-fx)" +cargo fmt --manifest-path contrib/screen-13-hot/Cargo.toml && diff || fail "Unformatted rust code (screen-13-hot)" cargo fmt --manifest-path contrib/screen-13-imgui/Cargo.toml && diff || fail "Unformatted rust code (screen-13-imgui)" cargo fmt --manifest-path examples/shader-toy/Cargo.toml && diff || fail "Unformatted rust code (shader-toy)" cargo fmt --manifest-path examples/skeletal-anim/Cargo.toml && diff || fail "Unformatted rust code (skeletal-anim)" @@ -26,6 +27,7 @@ cargo fmt --manifest-path examples/skeletal-anim/Cargo.toml && diff || fail "Unf cargo check --all-targets --all-features cargo check --manifest-path contrib/screen-13-egui/Cargo.toml --all-targets --all-features cargo check --manifest-path contrib/screen-13-fx/Cargo.toml --all-targets --all-features +cargo check --manifest-path contrib/screen-13-hot/Cargo.toml --all-targets --all-features cargo check --manifest-path contrib/screen-13-imgui/Cargo.toml --all-targets --all-features cargo check --manifest-path examples/shader-toy/Cargo.toml --all-targets --all-features cargo check --manifest-path examples/skeletal-anim/Cargo.toml --all-targets --all-features @@ -34,6 +36,7 @@ cargo check --manifest-path examples/skeletal-anim/Cargo.toml --all-targets --al cargo clippy --all-targets --all-features cargo clippy --manifest-path contrib/screen-13-egui/Cargo.toml --all-targets --all-features cargo clippy --manifest-path contrib/screen-13-fx/Cargo.toml --all-targets --all-features +cargo clippy --manifest-path contrib/screen-13-hot/Cargo.toml --all-targets --all-features cargo clippy --manifest-path contrib/screen-13-imgui/Cargo.toml --all-targets --all-features cargo clippy --manifest-path examples/shader-toy/Cargo.toml --all-targets --all-features cargo clippy --manifest-path examples/skeletal-anim/Cargo.toml --all-targets --all-features diff --git a/contrib/rel-mgmt/run-all-examples b/contrib/rel-mgmt/run-all-examples index 8bb47972..80413e60 100755 --- a/contrib/rel-mgmt/run-all-examples +++ b/contrib/rel-mgmt/run-all-examples @@ -24,5 +24,9 @@ cargo run --example transitions cargo run --example vsm_omni cargo run --manifest-path examples/skeletal-anim/Cargo.toml +# Hot-reload examples +cargo run --manifest-path contrib/screen-13-hot/Cargo.toml --example glsl +cargo run --manifest-path contrib/screen-13-hot/Cargo.toml --example hlsl + # Run this one in release mode cargo run --manifest-path examples/shader-toy/Cargo.toml --release \ No newline at end of file diff --git a/contrib/screen-13-fx/src/bitmap_font.rs b/contrib/screen-13-fx/src/bitmap_font.rs index 67beda2b..96f39c37 100644 --- a/contrib/screen-13-fx/src/bitmap_font.rs +++ b/contrib/screen-13-fx/src/bitmap_font.rs @@ -163,7 +163,10 @@ impl BitmapFont { let vertex_buf_len = 120 * text.chars().count() as vk::DeviceSize; let mut vertex_buf = self .cache - .lease(BufferInfo::new_mappable(vertex_buf_len, vk::BufferUsageFlags::VERTEX_BUFFER)) + .lease(BufferInfo::new_mappable( + vertex_buf_len, + vk::BufferUsageFlags::VERTEX_BUFFER, + )) .unwrap(); let mut vertex_count = 0; diff --git a/contrib/screen-13-fx/src/image_loader.rs b/contrib/screen-13-fx/src/image_loader.rs index e84ee2cd..e80cb374 100644 --- a/contrib/screen-13-fx/src/image_loader.rs +++ b/contrib/screen-13-fx/src/image_loader.rs @@ -41,11 +41,18 @@ impl ImageLoader { pool: HashPool::new(device), _decode_r_rg: Arc::new(ComputePipeline::create( device, - include_spirv!("res/shader/compute/decode_bitmap_r_rg.comp", comp).as_slice(), + ComputePipelineInfo::default(), + Shader::new_compute( + include_spirv!("res/shader/compute/decode_bitmap_r_rg.comp", comp).as_slice(), + ), )?), decode_rgb_rgba: Arc::new(ComputePipeline::create( device, - include_spirv!("res/shader/compute/decode_bitmap_rgb_rgba.comp", comp).as_slice(), + ComputePipelineInfo::default(), + Shader::new_compute( + include_spirv!("res/shader/compute/decode_bitmap_rgb_rgba.comp", comp) + .as_slice(), + ), )?), device: Arc::clone(device), }) diff --git a/contrib/screen-13-fx/src/presenter.rs b/contrib/screen-13-fx/src/presenter.rs index dbb14c43..9d3779cd 100644 --- a/contrib/screen-13-fx/src/presenter.rs +++ b/contrib/screen-13-fx/src/presenter.rs @@ -12,11 +12,17 @@ impl ComputePresenter { pub fn new(device: &Arc) -> Result { let pipeline1 = Arc::new(ComputePipeline::create( device, - include_spirv!("res/shader/compute/present1.comp", comp).as_slice(), + ComputePipelineInfo::default(), + Shader::new_compute( + include_spirv!("res/shader/compute/present1.comp", comp).as_slice(), + ), )?); let pipeline2 = Arc::new(ComputePipeline::create( device, - include_spirv!("res/shader/compute/present2.comp", comp).as_slice(), + ComputePipelineInfo::default(), + Shader::new_compute( + include_spirv!("res/shader/compute/present2.comp", comp).as_slice(), + ), )?); Ok(Self([pipeline1, pipeline2])) diff --git a/contrib/screen-13-fx/src/transition.rs b/contrib/screen-13-fx/src/transition.rs index 8652dcda..9222a037 100644 --- a/contrib/screen-13-fx/src/transition.rs +++ b/contrib/screen-13-fx/src/transition.rs @@ -482,7 +482,8 @@ impl TransitionPipeline { Arc::new( ComputePipeline::create( &self.device, - match transition_ty { + ComputePipelineInfo::default(), + Shader::new_compute(match transition_ty { TransitionType::Angular => { include_spirv!("res/shader/transition/angular.comp", comp).as_slice() } @@ -774,7 +775,7 @@ impl TransitionPipeline { include_spirv!("res/shader/transition/zoom_right_wipe.comp", comp) .as_slice() } - }, + }), ) .unwrap(), ) diff --git a/contrib/screen-13-hot/.github/img/noise.png b/contrib/screen-13-hot/.github/img/noise.png new file mode 100644 index 00000000..4f096886 Binary files /dev/null and b/contrib/screen-13-hot/.github/img/noise.png differ diff --git a/contrib/screen-13-hot/Cargo.toml b/contrib/screen-13-hot/Cargo.toml new file mode 100644 index 00000000..ef31110b --- /dev/null +++ b/contrib/screen-13-hot/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "screen-13-hot" +version = "0.1.0" +authors = ["John Wells "] +edition = "2021" +license = "MIT OR Apache-2.0" +readme = "README.md" +repository = "https://github.com/attackgoat/screen-13" +homepage = "https://github.com/attackgoat/screen-13/contrib/screen-13-hot" +keywords = ["gamedev", "vulkan"] +categories = ["game-development", "multimedia::images", "rendering::engine"] +description = "Hot-reloading shader pipelines for Screen-13" + +[dependencies] +anyhow = "1.0" +derive_builder = "0.12" +lazy_static = "1.4" +notify = "5.0" +screen-13 = { path = "../.."} +shader-prepper = "0.3.0-pre.3" +shaderc = "0.8" + +[dev-dependencies] +pretty_env_logger = "0.4" \ No newline at end of file diff --git a/contrib/screen-13-hot/README.md b/contrib/screen-13-hot/README.md new file mode 100644 index 00000000..a42cd39b --- /dev/null +++ b/contrib/screen-13-hot/README.md @@ -0,0 +1,31 @@ +# Screen 13 Hot + +Hot-reloading shader pipelines for _Screen 13_. Supports compute, graphic, and ray-trace shader +pipelines. + +Based on shaderc. Feel free to submit PRs for other compilers. + +## Quick Start + +See the [example code](examples/README.md), + +## Basic usage + +See the [GLSL](examples/glsl.rs) and [HLSL](examples/hlsl.rs) examples for usage - the hot pipelines +are drop-in replacements for the regular shader pipelines offered by _Screen 13_. + +After creating a pipeline two functions are available, `hot` or `cold`. The result of each may be +bound to a render graph for any sort of regular use. + +- `hot()`: Returns the pipeline instance which includes any changes found on disk. +- `cold()`: Returns the most recent successful compilation without watching for changes. + +## Advanced usage + +There are a few options available when creating a `HotShader` instance, which is a wrapper around +regular `Shader` instances. These options allow you to set compilation settings such as optimization +level and warnings-as-errors, among other things. + +## More infomation + +Run `cargo doc --open` to view detailed API documentation and find available compilation options. \ No newline at end of file diff --git a/contrib/screen-13-hot/examples/README.md b/contrib/screen-13-hot/examples/README.md new file mode 100644 index 00000000..92dd719a --- /dev/null +++ b/contrib/screen-13-hot/examples/README.md @@ -0,0 +1,15 @@ +# _Screen 13 Hot_ Example Code + +## Getting Started + +Hot-reloading shader pipelines are drop-in replacements for regular shader pipelines. Reference the +below code to get started. + +See the [README](../README.md) for more information. + +## Example Code + +Example | Instructions | Preview + --- | --- | :---: +[glsl.rs](glsl.rs) |
cargo run --example glsl
| Preview +[hlsl.rs](hlsl.rs) |
cargo run --example hlsl
| Preview \ No newline at end of file diff --git a/contrib/screen-13-hot/examples/glsl.rs b/contrib/screen-13-hot/examples/glsl.rs new file mode 100644 index 00000000..56dd5d42 --- /dev/null +++ b/contrib/screen-13-hot/examples/glsl.rs @@ -0,0 +1,44 @@ +use { + lazy_static::lazy_static, screen_13::prelude::*, screen_13_hot::prelude::*, std::path::PathBuf, +}; + +lazy_static! { + static ref CARGO_MANIFEST_DIR: PathBuf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); +} + +/// This program draws a noise signal to the swapchain - make changes to fill_image.comp or the +/// noise.glsl file it includes to see those changes update while the program is still running. +/// +/// Run with RUST_LOG=info to get notification of shader compilations. +fn main() -> Result<(), DisplayError> { + pretty_env_logger::init(); + + let event_loop = EventLoop::new().build()?; + + // Create a compute pipeline - the same as normal except for "Hot" prefixes and we provide the + // shader source code path instead of the shader source code bytes + let mut pipeline = HotComputePipeline::create( + &event_loop.device, + ComputePipelineInfo::default(), + HotShader::new_compute(CARGO_MANIFEST_DIR.join("examples/res/fill_image.comp")), + )?; + + let mut frame_index: u32 = 0; + + event_loop.run(|frame| { + frame + .render_graph + .begin_pass("make some noise") + .bind_pipeline(pipeline.hot()) + .write_descriptor(0, frame.swapchain_image) + .record_compute(move |compute, _| { + compute.push_constants(&frame_index.to_ne_bytes()).dispatch( + frame.width, + frame.height, + 1, + ); + }); + + frame_index += 1; + }) +} diff --git a/contrib/screen-13-hot/examples/hlsl.rs b/contrib/screen-13-hot/examples/hlsl.rs new file mode 100644 index 00000000..09cd339d --- /dev/null +++ b/contrib/screen-13-hot/examples/hlsl.rs @@ -0,0 +1,49 @@ +use { + lazy_static::lazy_static, screen_13::prelude::*, screen_13_hot::prelude::*, std::path::PathBuf, +}; + +lazy_static! { + static ref CARGO_MANIFEST_DIR: PathBuf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); +} + +/// This program draws a noise signal to the swapchain - make changes to fill_image.hlsl or the +/// noise.hlsl file it includes to see those changes update while the program is still running. +/// +/// Run with RUST_LOG=info to get notification of shader compilations. +fn main() -> Result<(), DisplayError> { + pretty_env_logger::init(); + + let event_loop = EventLoop::new().build()?; + + // Create a graphic pipeline - the same as normal except for "Hot" prefixes and we provide the + // shader source code path instead of the shader source code bytes + let fill_image_path = CARGO_MANIFEST_DIR.join("examples/res/fill_image.hlsl"); + let mut pipeline = HotGraphicPipeline::create( + &event_loop.device, + GraphicPipelineInfo::default(), + [ + HotShader::new_vertex(&fill_image_path).entry_name("vertex_main".to_string()), + HotShader::new_fragment(&fill_image_path).entry_name("fragment_main".to_string()), + ], + )?; + + let mut frame_index: u32 = 0; + + event_loop.run(|frame| { + frame + .render_graph + .begin_pass("make some noise") + .bind_pipeline(pipeline.hot()) + .clear_color(0, frame.swapchain_image) + .store_color(0, frame.swapchain_image) + .record_subpass(move |subpass, _| { + subpass + .push_constants_offset(0, &frame_index.to_ne_bytes()) + .push_constants_offset(4, &frame.width.to_ne_bytes()) + .push_constants_offset(8, &frame.height.to_ne_bytes()) + .draw(3, 1, 0, 0); + }); + + frame_index += 1; + }) +} diff --git a/contrib/screen-13-hot/examples/res/fill_image.comp b/contrib/screen-13-hot/examples/res/fill_image.comp new file mode 100644 index 00000000..b4b02949 --- /dev/null +++ b/contrib/screen-13-hot/examples/res/fill_image.comp @@ -0,0 +1,18 @@ +#version 460 core + +#include "noise.glsl" + +layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; + +layout(push_constant) uniform PushConstants { + layout(offset = 0) uint frame_index; +} push_const; + +layout(set = 0, binding = 0, rgba32f) restrict writeonly uniform image2D image; + +void main() { + uvec3 data = uvec3(gl_GlobalInvocationID.xy, push_const.frame_index); + vec4 color = vec4(hash(data), 1.0); + + imageStore(image, ivec2(gl_GlobalInvocationID.xy), color); +} \ No newline at end of file diff --git a/contrib/screen-13-hot/examples/res/fill_image.hlsl b/contrib/screen-13-hot/examples/res/fill_image.hlsl new file mode 100644 index 00000000..299bf34e --- /dev/null +++ b/contrib/screen-13-hot/examples/res/fill_image.hlsl @@ -0,0 +1,35 @@ +#include "noise.hlsl" + +struct PushConst { + uint frame_index; + uint frame_width; + uint frame_height; +}; + +[[vk::push_constant]] +cbuffer { + PushConst push_const; +}; + +struct Vertex { + float4 position: SV_POSITION; + [[vk::location(0)]] float2 tex_coord: TEXCOORD0; +}; + +Vertex vertex_main(uint vertex_id: SV_VERTEXID) { + Vertex vertex; + + vertex.tex_coord = float2((vertex_id << 1) & 2, vertex_id & 2); + vertex.position = float4(vertex.tex_coord * float2(2, -2) + float2(-1, 1), 0, 1); + + return vertex; +} + +float4 fragment_main(Vertex vertex): SV_TARGET { + uint3 coord; + coord.x = uint(vertex.tex_coord.x * float(push_const.frame_width)); + coord.y = uint(vertex.tex_coord.y * float(push_const.frame_height)); + coord.z = push_const.frame_index; + + return float4(hash(coord), 1.0); +} diff --git a/contrib/screen-13-hot/examples/res/noise.glsl b/contrib/screen-13-hot/examples/res/noise.glsl new file mode 100644 index 00000000..45cfc1d3 --- /dev/null +++ b/contrib/screen-13-hot/examples/res/noise.glsl @@ -0,0 +1,9 @@ +const uint k = 1103515245u; + +vec3 hash(uvec3 x) { + x = ((x >> 8u) ^ x.yzx) * k; + x = ((x >> 8u) ^ x.yzx) * k; + x = ((x >> 8u) ^ x.yzx) * k; + + return vec3(x) * (1.0 / float(0xffffffffu)); +} diff --git a/contrib/screen-13-hot/examples/res/noise.hlsl b/contrib/screen-13-hot/examples/res/noise.hlsl new file mode 100644 index 00000000..9f855922 --- /dev/null +++ b/contrib/screen-13-hot/examples/res/noise.hlsl @@ -0,0 +1,9 @@ +const uint k = 1103515245u; + +float3 hash(uint3 x) { + x = ((x >> 8u) ^ x.yzx) * k; + x = ((x >> 8u) ^ x.yzx) * k; + x = ((x >> 8u) ^ x.yzx) * k; + + return float3(x) * (1.0 / float(0xffffffffu)); +} diff --git a/contrib/screen-13-hot/src/compute.rs b/contrib/screen-13-hot/src/compute.rs new file mode 100644 index 00000000..caf0fcf8 --- /dev/null +++ b/contrib/screen-13-hot/src/compute.rs @@ -0,0 +1,72 @@ +use { + super::{compile_shader_and_watch, create_watcher, shader::HotShader}, + notify::INotifyWatcher, + screen_13::prelude::*, + std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; + +#[derive(Debug)] +pub struct HotComputePipeline { + device: Arc, + has_changes: Arc, + instance: Arc, + shader: HotShader, + watcher: INotifyWatcher, +} + +impl HotComputePipeline { + pub fn create( + device: &Arc, + info: impl Into, + shader: impl Into, + ) -> Result { + let shader = shader.into(); + + let (mut watcher, has_changes) = create_watcher(); + let compiled_shader = compile_shader_and_watch(&shader, &mut watcher)?; + + let instance = Arc::new(ComputePipeline::create(device, info, compiled_shader)?); + + let device = Arc::clone(device); + + Ok(Self { + device, + has_changes, + instance, + shader, + watcher, + }) + } + + /// Returns the most recent compilation without checking for changes or re-compiling the shader + /// source code. + pub fn cold(&self) -> &Arc { + &self.instance + } + + /// Returns the most recent compilation after checking for changes, and if needed re-compiling + /// the shader source code. + pub fn hot(&mut self) -> &Arc { + let has_changes = self.has_changes.swap(false, Ordering::Relaxed); + + if has_changes { + let (mut watcher, has_changes) = create_watcher(); + if let Ok(compiled_shader) = compile_shader_and_watch(&self.shader, &mut watcher) { + if let Ok(instance) = ComputePipeline::create( + &self.device, + self.instance.info.clone(), + compiled_shader, + ) { + self.has_changes = has_changes; + self.watcher = watcher; + self.instance = Arc::new(instance); + } + } + } + + self.cold() + } +} diff --git a/contrib/screen-13-hot/src/graphic.rs b/contrib/screen-13-hot/src/graphic.rs new file mode 100644 index 00000000..46cad769 --- /dev/null +++ b/contrib/screen-13-hot/src/graphic.rs @@ -0,0 +1,86 @@ +use { + super::{compile_shader_and_watch, create_watcher, shader::HotShader}, + notify::INotifyWatcher, + screen_13::prelude::*, + std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; + +#[derive(Debug)] +pub struct HotGraphicPipeline { + device: Arc, + has_changes: Arc, + instance: Arc, + shaders: Box<[HotShader]>, + watcher: INotifyWatcher, +} + +impl HotGraphicPipeline { + pub fn create( + device: &Arc, + info: impl Into, + shaders: impl IntoIterator, + ) -> Result + where + S: Into, + { + let shaders = shaders + .into_iter() + .map(|shader| shader.into()) + .collect::>(); + + let (mut watcher, has_changes) = create_watcher(); + let compiled_shaders = shaders + .iter() + .map(|shader| compile_shader_and_watch(shader, &mut watcher)) + .collect::, _>>()?; + + let instance = Arc::new(GraphicPipeline::create(device, info, compiled_shaders)?); + + let device = Arc::clone(device); + + Ok(Self { + device, + has_changes, + instance, + shaders, + watcher, + }) + } + + /// Returns the most recent compilation without checking for changes or re-compiling the shader + /// source code. + pub fn cold(&self) -> &Arc { + &self.instance + } + + /// Returns the most recent compilation after checking for changes, and if needed re-compiling + /// the shader source code. + pub fn hot(&mut self) -> &Arc { + let has_changes = self.has_changes.swap(false, Ordering::Relaxed); + + if has_changes { + let (mut watcher, has_changes) = create_watcher(); + if let Ok(compiled_shaders) = self + .shaders + .iter() + .map(|shader| compile_shader_and_watch(shader, &mut watcher)) + .collect::, DriverError>>() + { + if let Ok(instance) = GraphicPipeline::create( + &self.device, + self.instance.info.clone(), + compiled_shaders, + ) { + self.has_changes = has_changes; + self.watcher = watcher; + self.instance = Arc::new(instance); + } + } + } + + self.cold() + } +} diff --git a/contrib/screen-13-hot/src/lib.rs b/contrib/screen-13-hot/src/lib.rs new file mode 100644 index 00000000..531c84f7 --- /dev/null +++ b/contrib/screen-13-hot/src/lib.rs @@ -0,0 +1,205 @@ +pub mod compute; +pub mod graphic; +pub mod ray_trace; +pub mod shader; + +pub mod prelude { + pub use super::{ + compute::HotComputePipeline, + graphic::HotGraphicPipeline, + ray_trace::HotRayTracePipeline, + shader::{HotShader, HotShaderBuilder, OptimizationLevel, SourceLanguage, SpirvVersion}, + }; +} + +use { + self::shader::HotShader, + lazy_static::lazy_static, + notify::{recommended_watcher, Event, EventKind, INotifyWatcher}, + screen_13::prelude::*, + shader_prepper::{ + process_file, BoxedIncludeProviderError, IncludeProvider, ResolvedInclude, + ResolvedIncludePath, + }, + shaderc::{CompileOptions, Compiler, ShaderKind, SourceLanguage}, + std::{ + collections::HashSet, + fs::read_to_string, + io::{Error, ErrorKind}, + path::{Path, PathBuf}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + }, +}; + +lazy_static! { + static ref COMPILER: Compiler = Compiler::new().expect("Unable to initialize shaderc"); +} + +struct CompiledShader { + files_included: HashSet, + spirv_code: Vec, +} + +fn compile_shader( + path: impl AsRef, + entry_name: &str, + shader_kind: Option, + additional_opts: Option<&CompileOptions<'_>>, +) -> anyhow::Result { + info!("Compiling: {}", path.as_ref().display()); + + let path = path.as_ref().to_path_buf(); + let shader_kind = shader_kind.unwrap_or_else(|| guess_shader_kind(&path)); + + #[derive(Default)] + struct FileIncludeProvider(HashSet); + + impl IncludeProvider for FileIncludeProvider { + type IncludeContext = PathBuf; + + fn get_include( + &mut self, + path: &ResolvedIncludePath, + ) -> Result { + self.0.insert(PathBuf::from(&path.0)); + + Ok(read_to_string(&path.0)?) + } + + fn resolve_path( + &self, + path: &str, + context: &Self::IncludeContext, + ) -> Result, BoxedIncludeProviderError> { + let path = context.join(path); + + Ok(ResolvedInclude { + resolved_path: ResolvedIncludePath(path.to_str().unwrap_or_default().to_string()), + context: path + .parent() + .map(|path| path.to_path_buf()) + .unwrap_or_else(PathBuf::new), + }) + } + } + + let mut file_include_provider = FileIncludeProvider::default(); + let source_code = process_file( + path.to_string_lossy().as_ref(), + &mut file_include_provider, + PathBuf::new(), + ) + .map_err(|err| { + error!("Unable to process shader file: {err}"); + + Error::new(ErrorKind::InvalidData, err) + })? + .iter() + .map(|chunk| chunk.source.as_str()) + .collect::(); + let files_included = file_include_provider.0; + + let spirv_code = COMPILER + .compile_into_spirv( + &source_code, + shader_kind, + &path.to_string_lossy(), + entry_name, + additional_opts, + ) + .map_err(|err| { + eprintln!("Shader: {}", path.display()); + + for (line_index, line) in source_code.split('\n').enumerate() { + let line_number = line_index + 1; + eprintln!("{line_number}: {line}"); + } + + err + })? + .as_binary_u8() + .to_vec(); + + Ok(CompiledShader { + files_included, + spirv_code, + }) +} + +fn compile_shader_and_watch( + shader: &HotShader, + watcher: &mut INotifyWatcher, +) -> Result { + let mut base_shader = Shader::new(shader.stage, shader.compile_and_watch(watcher)?.as_slice()); + + base_shader = base_shader.entry_name(shader.entry_name.clone()); + + if let Some(specialization_info) = &shader.specialization_info { + base_shader = base_shader.specialization_info(specialization_info.clone()); + } + + Ok(base_shader) +} + +fn create_watcher() -> (INotifyWatcher, Arc) { + let has_changes = Arc::new(AtomicBool::new(false)); + let has_changes_clone = Arc::clone(&has_changes); + let watcher = recommended_watcher(move |event: notify::Result| { + let event = event.unwrap_or_else(|_| Event::new(EventKind::Any)); + if matches!( + event.kind, + EventKind::Any | EventKind::Modify(_) | EventKind::Other + ) { + info!("Shader change detected"); + + has_changes_clone.store(true, Ordering::Relaxed); + } + }) + .unwrap(); + + (watcher, has_changes) +} + +fn guess_shader_kind(path: impl AsRef) -> ShaderKind { + match path + .as_ref() + .extension() + .map(|ext| ext.to_string_lossy().to_string()) + .unwrap_or_default() + .as_str() + { + "comp" => ShaderKind::Compute, + "task" => ShaderKind::Task, + "mesh" => ShaderKind::Mesh, + "vert" => ShaderKind::Vertex, + "geom" => ShaderKind::Geometry, + "tesc" => ShaderKind::TessControl, + "tese" => ShaderKind::TessEvaluation, + "frag" => ShaderKind::Fragment, + "rgen" => ShaderKind::RayGeneration, + "rahit" => ShaderKind::AnyHit, + "rchit" => ShaderKind::ClosestHit, + "rint" => ShaderKind::Intersection, + "rcall" => ShaderKind::Callable, + "rmiss" => ShaderKind::Miss, + _ => ShaderKind::InferFromSource, + } +} + +fn guess_shader_source_language(path: impl AsRef) -> Option { + match path + .as_ref() + .extension() + .map(|ext| ext.to_string_lossy().to_string()) + .unwrap_or_default() + .as_str() + { + "comp" | "task" | "mesh" | "vert" | "geom" | "tesc" | "tese" | "frag" | "rgen" + | "rahit" | "rchit" | "rint" | "rcall" | "rmiss" | "glsl" => Some(SourceLanguage::GLSL), + "hlsl" => Some(SourceLanguage::HLSL), + _ => None, + } +} diff --git a/contrib/screen-13-hot/src/ray_trace.rs b/contrib/screen-13-hot/src/ray_trace.rs new file mode 100644 index 00000000..a5809729 --- /dev/null +++ b/contrib/screen-13-hot/src/ray_trace.rs @@ -0,0 +1,96 @@ +use { + super::{compile_shader_and_watch, create_watcher, shader::HotShader}, + notify::INotifyWatcher, + screen_13::prelude::*, + std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; + +#[derive(Debug)] +pub struct HotRayTracePipeline { + device: Arc, + has_changes: Arc, + instance: Arc, + shader_groups: Box<[RayTraceShaderGroup]>, + shaders: Box<[HotShader]>, + watcher: INotifyWatcher, +} + +impl HotRayTracePipeline { + pub fn create( + device: &Arc, + info: impl Into, + shaders: impl IntoIterator, + shader_groups: impl IntoIterator, + ) -> Result + where + S: Into, + { + let shader_groups = shader_groups.into_iter().collect::>(); + let shaders = shaders + .into_iter() + .map(|shader| shader.into()) + .collect::>(); + + let (mut watcher, has_changes) = create_watcher(); + let compiled_shaders = shaders + .iter() + .map(|shader| compile_shader_and_watch(shader, &mut watcher)) + .collect::, _>>()?; + + let instance = Arc::new(RayTracePipeline::create( + device, + info, + compiled_shaders, + shader_groups.iter().copied(), + )?); + + let device = Arc::clone(device); + + Ok(Self { + device, + has_changes, + instance, + shader_groups, + shaders, + watcher, + }) + } + + /// Returns the most recent compilation without checking for changes or re-compiling the shader + /// source code. + pub fn cold(&self) -> &Arc { + &self.instance + } + + /// Returns the most recent compilation after checking for changes, and if needed re-compiling + /// the shader source code. + pub fn hot(&mut self) -> &Arc { + let has_changes = self.has_changes.swap(false, Ordering::Relaxed); + + if has_changes { + let (mut watcher, has_changes) = create_watcher(); + if let Ok(compiled_shaders) = self + .shaders + .iter() + .map(|shader| compile_shader_and_watch(shader, &mut watcher)) + .collect::, DriverError>>() + { + if let Ok(instance) = RayTracePipeline::create( + &self.device, + self.instance.info.clone(), + compiled_shaders, + self.shader_groups.iter().copied(), + ) { + self.has_changes = has_changes; + self.watcher = watcher; + self.instance = Arc::new(instance); + } + } + } + + self.cold() + } +} diff --git a/contrib/screen-13-hot/src/shader.rs b/contrib/screen-13-hot/src/shader.rs new file mode 100644 index 00000000..418944e0 --- /dev/null +++ b/contrib/screen-13-hot/src/shader.rs @@ -0,0 +1,334 @@ +pub use shaderc::{OptimizationLevel, SourceLanguage, SpirvVersion}; + +use { + super::{compile_shader, guess_shader_source_language}, + derive_builder::{Builder, UninitializedFieldError}, + notify::{INotifyWatcher, RecursiveMode, Watcher}, + screen_13::prelude::*, + shaderc::{CompileOptions, EnvVersion, ShaderKind, TargetEnv}, + std::path::{Path, PathBuf}, +}; + +/// Describes a shader program which runs on some pipeline stage. +#[allow(missing_docs)] +#[derive(Builder, Clone, Debug)] +#[builder( + build_fn(private, name = "fallible_build", error = "HotShaderBuilderError"), + pattern = "owned" +)] +pub struct HotShader { + /// The name of the entry point which will be executed by this shader. + /// + /// The default value is `main`. + #[builder(default = "\"main\".to_owned()")] + pub entry_name: String, + + /// Sets the optimization level. + #[builder(default, setter(strip_option))] + pub optimization_level: Option, + + /// Shader source code path. + pub path: PathBuf, + + /// Sets the source language. + #[builder(default, setter(strip_option))] + pub source_language: Option, + + /// Data about Vulkan specialization constants. + /// + /// # Examples + /// + /// Basic usage (GLSL): + /// + /// ``` + /// # inline_spirv::inline_spirv!(r#" + /// #version 460 core + /// + /// // Defaults to 6 if not set using HotShader specialization_info! + /// layout(constant_id = 0) const uint MY_COUNT = 6; + /// + /// layout(set = 0, binding = 0) uniform sampler2D my_samplers[MY_COUNT]; + /// + /// void main() + /// { + /// // Code uses MY_COUNT number of my_samplers here + /// } + /// # "#, comp); + /// ``` + /// + /// ```no_run + /// # use std::sync::Arc; + /// # use ash::vk; + /// # use screen_13::driver::{Device, DriverConfig, DriverError}; + /// # use screen_13::driver::shader::{SpecializationInfo}; + /// # use screen_13_hot::shader::HotShader; + /// # fn main() -> Result<(), DriverError> { + /// # let device = Arc::new(Device::new(DriverConfig::new().build())?); + /// # let my_shader_code = [0u8; 1]; + /// // We instead specify 42 for MY_COUNT: + /// let shader = HotShader::new_fragment(my_shader_code.as_slice()) + /// .specialization_info(SpecializationInfo::new( + /// [vk::SpecializationMapEntry { + /// constant_id: 0, + /// offset: 0, + /// size: 4, + /// }], + /// 42u32.to_ne_bytes() + /// )); + /// # Ok(()) } + /// ``` + #[builder(default, setter(strip_option))] + pub specialization_info: Option, + + /// The shader stage this structure applies to. + pub stage: vk::ShaderStageFlags, + + /// Sets the target SPIR-V version. + #[builder(default, setter(strip_option))] + pub target_spirv: Option, + + /// Sets the compiler mode to treat all warnings as errors. + #[builder(default)] + pub warnings_as_errors: bool, +} + +impl HotShader { + /// Specifies a shader with the given `stage` and shader code values. + #[allow(clippy::new_ret_no_self)] + pub fn new(stage: vk::ShaderStageFlags, path: impl AsRef) -> HotShaderBuilder { + HotShaderBuilder::new(stage, path) + } + + /// Creates a new ray trace shader. + /// + /// # Panics + /// + /// If the shader code is invalid. + pub fn new_any_hit(path: impl AsRef) -> HotShaderBuilder { + Self::new(vk::ShaderStageFlags::ANY_HIT_KHR, path) + } + + /// Creates a new ray trace shader. + /// + /// # Panics + /// + /// If the shader code is invalid. + pub fn new_callable(path: impl AsRef) -> HotShaderBuilder { + Self::new(vk::ShaderStageFlags::CALLABLE_KHR, path) + } + + /// Creates a new ray trace shader. + /// + /// # Panics + /// + /// If the shader code is invalid. + pub fn new_closest_hit(path: impl AsRef) -> HotShaderBuilder { + Self::new(vk::ShaderStageFlags::CLOSEST_HIT_KHR, path) + } + + /// Creates a new compute shader. + /// + /// # Panics + /// + /// If the shader code is invalid. + pub fn new_compute(path: impl AsRef) -> HotShaderBuilder { + Self::new(vk::ShaderStageFlags::COMPUTE, path) + } + + /// Creates a new fragment shader. + /// + /// # Panics + /// + /// If the shader code is invalid. + pub fn new_fragment(path: impl AsRef) -> HotShaderBuilder { + Self::new(vk::ShaderStageFlags::FRAGMENT, path) + } + + /// Creates a new geometry shader. + /// + /// # Panics + /// + /// If the shader code is invalid. + pub fn new_geometry(path: impl AsRef) -> HotShaderBuilder { + Self::new(vk::ShaderStageFlags::GEOMETRY, path) + } + + /// Creates a new ray trace shader. + /// + /// # Panics + /// + /// If the shader code is invalid. + pub fn new_intersection(path: impl AsRef) -> HotShaderBuilder { + Self::new(vk::ShaderStageFlags::INTERSECTION_KHR, path) + } + + /// Creates a new mesh shader. + /// + /// # Panics + /// + /// If the shader code is invalid. + pub fn new_mesh(path: impl AsRef) -> HotShaderBuilder { + Self::new(vk::ShaderStageFlags::MESH_EXT, path) + } + + /// Creates a new ray trace shader. + /// + /// # Panics + /// + /// If the shader code is invalid. + pub fn new_miss(path: impl AsRef) -> HotShaderBuilder { + Self::new(vk::ShaderStageFlags::MISS_KHR, path) + } + + /// Creates a new ray trace shader. + /// + /// # Panics + /// + /// If the shader code is invalid. + pub fn new_ray_gen(path: impl AsRef) -> HotShaderBuilder { + Self::new(vk::ShaderStageFlags::RAYGEN_KHR, path) + } + + /// Creates a new mesh task shader. + /// + /// # Panics + /// + /// If the shader code is invalid. + pub fn new_task(path: impl AsRef) -> HotShaderBuilder { + Self::new(vk::ShaderStageFlags::TASK_EXT, path) + } + + /// Creates a new tesselation control shader. + /// + /// # Panics + /// + /// If the shader code is invalid. + pub fn new_tesselation_ctrl(path: impl AsRef) -> HotShaderBuilder { + Self::new(vk::ShaderStageFlags::TESSELLATION_CONTROL, path) + } + + /// Creates a new tesselation evaluation shader. + /// + /// # Panics + /// + /// If the shader code is invalid. + pub fn new_tesselation_eval(spirv: impl AsRef) -> HotShaderBuilder { + Self::new(vk::ShaderStageFlags::TESSELLATION_EVALUATION, spirv) + } + + /// Creates a new vertex shader. + /// + /// # Panics + /// + /// If the shader code is invalid. + pub fn new_vertex(path: impl AsRef) -> HotShaderBuilder { + Self::new(vk::ShaderStageFlags::VERTEX, path) + } + + pub(super) fn compile_and_watch( + &self, + watcher: &mut INotifyWatcher, + ) -> Result, DriverError> { + let shader_kind = match self.stage { + vk::ShaderStageFlags::ANY_HIT_KHR => ShaderKind::AnyHit, + vk::ShaderStageFlags::CALLABLE_KHR => ShaderKind::Callable, + vk::ShaderStageFlags::CLOSEST_HIT_KHR => ShaderKind::ClosestHit, + vk::ShaderStageFlags::COMPUTE => ShaderKind::Compute, + vk::ShaderStageFlags::FRAGMENT => ShaderKind::Fragment, + vk::ShaderStageFlags::GEOMETRY => ShaderKind::Geometry, + vk::ShaderStageFlags::INTERSECTION_KHR => ShaderKind::Intersection, + vk::ShaderStageFlags::MISS_KHR => ShaderKind::Miss, + vk::ShaderStageFlags::RAYGEN_KHR => ShaderKind::RayGeneration, + vk::ShaderStageFlags::TASK_EXT => ShaderKind::Task, + vk::ShaderStageFlags::TESSELLATION_CONTROL => ShaderKind::TessControl, + vk::ShaderStageFlags::TESSELLATION_EVALUATION => ShaderKind::TessEvaluation, + vk::ShaderStageFlags::VERTEX => ShaderKind::Vertex, + _ => unimplemented!("{:?}", self.stage), + }; + + let mut additional_opts = CompileOptions::new().ok_or_else(|| { + warn!("Unable to initialize compiler options"); + + DriverError::Unsupported + })?; + + additional_opts.set_target_env(TargetEnv::Vulkan, EnvVersion::Vulkan1_2 as _); + + if let Some(language) = self.source_language.or_else(|| { + let language = guess_shader_source_language(&self.path); + + debug!("Guessed source language: {:?}", language); + + language + }) { + additional_opts.set_source_language(language); + } + + if let Some(target_spirv) = self.target_spirv { + additional_opts.set_target_spirv(target_spirv); + } + + if let Some(level) = self.optimization_level { + additional_opts.set_optimization_level(level); + } + + if self.warnings_as_errors { + additional_opts.set_warnings_as_errors(); + } + + let res = compile_shader( + &self.path, + &self.entry_name, + Some(shader_kind), + Some(&additional_opts), + ) + .map_err(|err| { + warn!("Unable to compile shader {}: {err}", self.path.display()); + + DriverError::InvalidData + })?; + + for path in res.files_included { + watcher + .watch(&path, RecursiveMode::NonRecursive) + .map_err(|err| { + warn!("Unable to watch file: {err}"); + + DriverError::Unsupported + })?; + } + + Ok(res.spirv_code) + } +} + +impl From for HotShader { + fn from(builder: HotShaderBuilder) -> HotShader { + builder.build() + } +} + +// HACK: https://github.com/colin-kiegel/rust-derive-builder/issues/56 +impl HotShaderBuilder { + /// Specifies a shader with the given `stage` and shader path values. + pub fn new(stage: vk::ShaderStageFlags, path: impl AsRef) -> Self { + Self::default() + .stage(stage) + .path(path.as_ref().to_path_buf()) + } + + /// Builds a new `HotShader`. + pub fn build(self) -> HotShader { + self.fallible_build() + .expect("All required fields set at initialization") + } +} + +#[derive(Debug)] +struct HotShaderBuilderError; + +impl From for HotShaderBuilderError { + fn from(_: UninitializedFieldError) -> Self { + Self + } +} diff --git a/contrib/screen-13-imgui/src/lib.rs b/contrib/screen-13-imgui/src/lib.rs index f158d86d..52ef434f 100644 --- a/contrib/screen-13-imgui/src/lib.rs +++ b/contrib/screen-13-imgui/src/lib.rs @@ -119,7 +119,10 @@ impl ImGui { let indices = cast_slice(draw_list.idx_buffer()); let mut index_buf = self .pool - .lease(BufferInfo::new_mappable(indices.len() as _, vk::BufferUsageFlags::INDEX_BUFFER)) + .lease(BufferInfo::new_mappable( + indices.len() as _, + vk::BufferUsageFlags::INDEX_BUFFER, + )) .unwrap(); { @@ -132,7 +135,10 @@ impl ImGui { let vertex_buf_len = vertices.len() * 20; let mut vertex_buf = self .pool - .lease(BufferInfo::new_mappable(vertex_buf_len as _, vk::BufferUsageFlags::VERTEX_BUFFER)) + .lease(BufferInfo::new_mappable( + vertex_buf_len as _, + vk::BufferUsageFlags::VERTEX_BUFFER, + )) .unwrap(); { @@ -260,7 +266,10 @@ impl ImGui { let temp_buf_len = texture.data.len(); let mut temp_buf = self .pool - .lease(BufferInfo::new_mappable(temp_buf_len as _, vk::BufferUsageFlags::TRANSFER_SRC)) + .lease(BufferInfo::new_mappable( + temp_buf_len as _, + vk::BufferUsageFlags::TRANSFER_SRC, + )) .unwrap(); { diff --git a/examples/README.md b/examples/README.md index 2e6685c5..73d6f20a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,6 +5,8 @@ A helpful [getting started](getting-started.md) guide is available which describes basic _Screen 13_ types and functions. +See the [README](../README.md) for more information. + ## Example Code Example | Instructions | Preview @@ -24,4 +26,10 @@ Example | Instructions | Preview [vsm_omni.rs](vsm_omni.rs) |
cargo run --example vsm_omni
Variance shadow mapping for omni/point lights | Preview [transitions.rs](transitions.rs) |
cargo run --example transitions
| Preview [skeletal-anim/](skeletal-anim/src/main.rs) |
cargo run --manifest-path examples/skeletal-anim/Cargo.toml
Skeletal mesh animation using GLTF | Preview -[shader-toy/](shader-toy/src/main.rs) |
cargo run --manifest-path examples/shader-toy/Cargo.toml
| Preview \ No newline at end of file +[shader-toy/](shader-toy/src/main.rs) |
cargo run --manifest-path examples/shader-toy/Cargo.toml
| Preview + +## Additional Examples + +The following packages offer examples for specific cases not listed here: + +- [contrib/screen-13-hot](../contrib/screen-13-hot/examples/README.md): Shader pipeline hot-reload \ No newline at end of file diff --git a/examples/debugger.rs b/examples/debugger.rs index 40e34627..4a26a03e 100644 --- a/examples/debugger.rs +++ b/examples/debugger.rs @@ -129,19 +129,22 @@ fn main() -> Result<(), screen_13::DisplayError> { let compute_pipeline = Arc::new( ComputePipeline::create( frame.device, - inline_spirv::inline_spirv!( - r#" - #version 460 core + ComputePipelineInfo::default(), + Shader::new_compute( + inline_spirv::inline_spirv!( + r#" + #version 460 core - layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; + layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; - layout(set = 0, binding = 42, rgba8) restrict readonly uniform image2D an_image; + layout(set = 0, binding = 42, rgba8) restrict readonly uniform image2D an_image; - void main() {/* TODO: 📈...💰! */} - "#, - comp - ) - .as_slice(), + void main() {/* TODO: 📈...💰! */} + "#, + comp + ) + .as_slice(), + ), ) .unwrap(), ); diff --git a/examples/font_bmp.rs b/examples/font_bmp.rs index 858769d8..416eaceb 100644 --- a/examples/font_bmp.rs +++ b/examples/font_bmp.rs @@ -41,6 +41,8 @@ fn main() -> anyhow::Result<()> { // A neato smoke effect just for fun let start_time = Instant::now(); let smoke_pipeline = Arc::new(ComputePipeline::create(&event_loop.device, + ComputePipelineInfo::default(), + Shader::new_compute( inline_spirv!( r#" // Derived from https://www.shadertoy.com/view/Xl2XWz @@ -100,7 +102,7 @@ fn main() -> anyhow::Result<()> { "#, comp ) - .as_slice(), + .as_slice()), )?); event_loop.run(|frame| { diff --git a/examples/fuzzer.rs b/examples/fuzzer.rs index 4413e85d..084f1a7e 100644 --- a/examples/fuzzer.rs +++ b/examples/fuzzer.rs @@ -311,7 +311,8 @@ fn record_compute_array_bind(frame: &mut FrameContext, pool: &mut HashPool) { let pipeline = compute_pipeline( "array_bind", frame.device, - ComputePipelineInfo::new( + ComputePipelineInfo::default(), + Shader::new_compute( inline_spirv!( r#" #version 460 core @@ -375,7 +376,7 @@ fn record_compute_array_bind(frame: &mut FrameContext, pool: &mut HashPool) { .clear_color_image(images[2]) .clear_color_image(images[3]) .clear_color_image(images[4]) - .begin_pass("no-op") + .begin_pass("array-bind") .bind_pipeline(&pipeline) .read_descriptor((0, [0]), images[0]) .read_descriptor((0, [1]), images[1]) @@ -393,7 +394,8 @@ fn record_compute_bindless(frame: &mut FrameContext, pool: &mut HashPool) { let pipeline = compute_pipeline( "bindless", frame.device, - ComputePipelineInfo::new( + ComputePipelineInfo::default(), + Shader::new_compute( inline_spirv!( r#" #version 460 core @@ -450,7 +452,7 @@ fn record_compute_bindless(frame: &mut FrameContext, pool: &mut HashPool) { frame .render_graph - .begin_pass("no-op") + .begin_pass("compute-bindless") .bind_pipeline(&pipeline) .write_descriptor((0, [0]), images[0]) .write_descriptor((0, [1]), images[1]) @@ -468,16 +470,19 @@ fn record_compute_no_op(frame: &mut FrameContext, _: &mut HashPool) { let pipeline = compute_pipeline( "no_op", frame.device, - inline_spirv!( - r#" - #version 460 core + ComputePipelineInfo::default(), + Shader::new_compute( + inline_spirv!( + r#" + #version 460 core - void main() { - } - "#, - comp - ) - .as_slice(), + void main() { + } + "#, + comp + ) + .as_slice(), + ), ); frame .render_graph @@ -572,7 +577,7 @@ fn record_graphic_bindless(frame: &mut FrameContext, pool: &mut HashPool) { .clear_color_image(images[2]) .clear_color_image(images[3]) .clear_color_image(images[4]) - .begin_pass("a") + .begin_pass("graphic-bindless") .bind_pipeline(&pipeline) .read_descriptor((0, [0]), images[0]) .read_descriptor((0, [1]), images[1]) @@ -1029,6 +1034,7 @@ fn compute_pipeline( key: &'static str, device: &Arc, info: impl Into, + shader: impl Into, ) -> Arc { use std::{cell::RefCell, collections::HashMap}; @@ -1038,9 +1044,9 @@ fn compute_pipeline( TLS.with(|tls| { Arc::clone( - tls.borrow_mut() - .entry(key) - .or_insert_with(|| Arc::new(ComputePipeline::create(device, info).unwrap())), + tls.borrow_mut().entry(key).or_insert_with(|| { + Arc::new(ComputePipeline::create(device, info, shader).unwrap()) + }), ) }) } diff --git a/examples/vsm_omni.rs b/examples/vsm_omni.rs index 5751ed73..e99b3948 100644 --- a/examples/vsm_omni.rs +++ b/examples/vsm_omni.rs @@ -451,27 +451,30 @@ fn create_blur_x_pipeline(device: &Arc) -> Result, comp ); - let info = - ComputePipelineInfo::new(comp.as_slice()).specialization_info(SpecializationInfo::new( - vec![ - vk::SpecializationMapEntry { - constant_id: 0, - offset: 0, - size: 4, - }, - vk::SpecializationMapEntry { - constant_id: 1, - offset: 4, - size: 4, - }, - ], - bytes_of(&Blur { - image_size: CUBEMAP_SIZE, - radius: BLUR_RADIUS, - }), - )); - - Ok(Arc::new(ComputePipeline::create(device, info)?)) + let shader = Shader::new_compute(comp.as_slice()).specialization_info(SpecializationInfo::new( + vec![ + vk::SpecializationMapEntry { + constant_id: 0, + offset: 0, + size: 4, + }, + vk::SpecializationMapEntry { + constant_id: 1, + offset: 4, + size: 4, + }, + ], + bytes_of(&Blur { + image_size: CUBEMAP_SIZE, + radius: BLUR_RADIUS, + }), + )); + + Ok(Arc::new(ComputePipeline::create( + device, + ComputePipelineInfo::default(), + shader, + )?)) } fn create_blur_y_pipeline(device: &Arc) -> Result, DriverError> { @@ -571,27 +574,30 @@ fn create_blur_y_pipeline(device: &Arc) -> Result, comp ); - let info = - ComputePipelineInfo::new(comp.as_slice()).specialization_info(SpecializationInfo::new( - vec![ - vk::SpecializationMapEntry { - constant_id: 0, - offset: 0, - size: 4, - }, - vk::SpecializationMapEntry { - constant_id: 1, - offset: 4, - size: 4, - }, - ], - bytes_of(&Blur { - image_size: CUBEMAP_SIZE, - radius: BLUR_RADIUS, - }), - )); - - Ok(Arc::new(ComputePipeline::create(device, info)?)) + let shader = Shader::new_compute(comp.as_slice()).specialization_info(SpecializationInfo::new( + vec![ + vk::SpecializationMapEntry { + constant_id: 0, + offset: 0, + size: 4, + }, + vk::SpecializationMapEntry { + constant_id: 1, + offset: 4, + size: 4, + }, + ], + bytes_of(&Blur { + image_size: CUBEMAP_SIZE, + radius: BLUR_RADIUS, + }), + )); + + Ok(Arc::new(ComputePipeline::create( + device, + ComputePipelineInfo::default(), + shader, + )?)) } fn create_debug_pipeline(device: &Arc) -> Result, DriverError> { diff --git a/src/driver/compute.rs b/src/driver/compute.rs index d5671189..7bba5851 100644 --- a/src/driver/compute.rs +++ b/src/driver/compute.rs @@ -2,9 +2,7 @@ use { super::{ - shader::{ - DescriptorBindingMap, PipelineDescriptorInfo, Shader, ShaderCode, SpecializationInfo, - }, + shader::{DescriptorBindingMap, PipelineDescriptorInfo, Shader}, Device, DriverError, }, ash::vk, @@ -54,20 +52,21 @@ impl ComputePipeline { /// # use ash::vk; /// # use screen_13::driver::{Device, DriverConfig, DriverError}; /// # use screen_13::driver::compute::{ComputePipeline, ComputePipelineInfo}; + /// # use screen_13::driver::shader::{Shader}; /// # fn main() -> Result<(), DriverError> { /// # let device = Arc::new(Device::new(DriverConfig::new().build())?); /// # let my_shader_code = [0u8; 1]; /// // my_shader_code is raw SPIR-V code as bytes - /// let info = ComputePipelineInfo::new(my_shader_code.as_slice()); - /// let pipeline = ComputePipeline::create(&device, info)?; + /// let shader = Shader::new_compute(my_shader_code.as_slice()); + /// let pipeline = ComputePipeline::create(&device, ComputePipelineInfo::default(), shader)?; /// /// assert_ne!(*pipeline, vk::Pipeline::null()); - /// assert_eq!(pipeline.info.entry_name.as_str(), "main"); /// # Ok(()) } /// ``` pub fn create( device: &Arc, info: impl Into, + shader: impl Into, ) -> Result { use std::slice::from_ref; @@ -75,7 +74,7 @@ impl ComputePipeline { let device = Arc::clone(device); let info: ComputePipelineInfo = info.into(); - let shader = info.clone().into_shader(); + let shader = shader.into(); // Use SPIR-V reflection to get the types and counts of all descriptors let mut descriptor_bindings = shader.descriptor_bindings(&device); @@ -94,8 +93,8 @@ impl ComputePipeline { unsafe { let shader_module_create_info = vk::ShaderModuleCreateInfo { - code_size: info.spirv.len(), - p_code: info.spirv.as_ptr() as *const u32, + code_size: shader.spirv.len(), + p_code: shader.spirv.as_ptr() as *const u32, ..Default::default() }; let shader_module = device @@ -105,12 +104,12 @@ impl ComputePipeline { DriverError::Unsupported })?; - let entry_name = CString::new(info.entry_name.as_bytes()).unwrap(); + let entry_name = CString::new(shader.entry_name.as_bytes()).unwrap(); let mut stage_create_info = vk::PipelineShaderStageCreateInfo::builder() .module(shader_module) .stage(shader.stage) .name(&entry_name); - let specialization_info = info.specialization_info.as_ref().map(|info| { + let specialization_info = shader.specialization_info.as_ref().map(|info| { vk::SpecializationInfo::builder() .map_entries(&info.map_entries) .data(&info.data) @@ -188,7 +187,7 @@ impl Drop for ComputePipeline { } /// Information used to create a [`ComputePipeline`] instance. -#[derive(Builder, Clone, Debug)] +#[derive(Builder, Clone, Debug, Default)] #[builder( pattern = "owned", build_fn( @@ -223,99 +222,9 @@ pub struct ComputePipelineInfo { #[builder(default = "8192")] pub bindless_descriptor_count: u32, - /// The GLSL or HLSL shader entry point name, or `main` by default. - #[builder(setter(strip_option), default = "String::from(\"main\")")] - pub entry_name: String, - /// A descriptive name used in debugging messages. #[builder(default, setter(strip_option))] pub name: Option, - - /// Data about Vulkan specialization constants. - /// - /// # Examples - /// - /// Basic usage (GLSL): - /// - /// ``` - /// # inline_spirv::inline_spirv!(r#" - /// #version 460 core - /// - /// // Defaults to 6 if not set using ComputePipelineInfo.specialization_info! - /// layout(constant_id = 0) const uint MY_COUNT = 6; - /// - /// layout(set = 0, binding = 0) uniform sampler2D my_samplers[MY_COUNT]; - /// - /// void main() - /// { - /// // Code uses MY_COUNT number of my_samplers here - /// } - /// # "#, comp); - /// ``` - /// - /// ```no_run - /// # use std::sync::Arc; - /// # use ash::vk; - /// # use screen_13::driver::{Device, DriverConfig, DriverError}; - /// # use screen_13::driver::compute::{ComputePipeline, ComputePipelineInfo}; - /// # use screen_13::driver::shader::{SpecializationInfo}; - /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::new(DriverConfig::new().build())?); - /// # let my_shader_code = [0u8; 1]; - /// // We instead specify 42 for MY_COUNT: - /// let info = ComputePipelineInfo::new(my_shader_code.as_slice()) - /// .specialization_info(SpecializationInfo::new( - /// [vk::SpecializationMapEntry { - /// constant_id: 0, - /// offset: 0, - /// size: 4, - /// }], - /// 42u32.to_ne_bytes() - /// )); - /// let pipeline = ComputePipeline::create(&device, info)?; - /// # Ok(()) } - /// ``` - #[builder(default, setter(strip_option))] - pub specialization_info: Option, - - /// Shader code. - /// - /// Although SPIR-V code is specified as `u32` values, this field uses `u8` in order to make - /// loading from file simpler. You should always have a SPIR-V code length which is a multiple - /// of four bytes, or a panic will happen during [`ComputePipeline::create`]. - pub spirv: Vec, -} - -impl ComputePipelineInfo { - /// Specifies a compute pipeline with the given shader code. - /// - /// # Panics - /// - /// If shader code is not a multiple of four bytes. - #[allow(clippy::new_ret_no_self)] - pub fn new(spirv: impl ShaderCode) -> ComputePipelineInfoBuilder { - ComputePipelineInfoBuilder::default().spirv(spirv.into_vec()) - } - - fn into_shader(self) -> Shader { - let mut shader = - Shader::new(vk::ShaderStageFlags::COMPUTE, self.spirv).entry_name(self.entry_name); - - if let Some(specialization_info) = self.specialization_info { - shader = shader.specialization_info(specialization_info); - } - - shader.build() - } -} - -impl From for ComputePipelineInfo -where - S: ShaderCode, -{ - fn from(spirv: S) -> Self { - Self::new(spirv).build() - } } impl From for ComputePipelineInfo { diff --git a/src/driver/ray_trace.rs b/src/driver/ray_trace.rs index e8890365..820b6f08 100644 --- a/src/driver/ray_trace.rs +++ b/src/driver/ray_trace.rs @@ -434,7 +434,7 @@ impl From for RayTracePipelineInfoBuilderError { /// /// See /// [VkRayTracingShaderGroupCreateInfoKHR](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#VkRayTracingShaderGroupCreateInfoKHR). -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub struct RayTraceShaderGroup { /// The optional index of the any-hit shader in the group if the shader group has type of /// [RayTraceShaderGroupType::TrianglesHitGroup] or @@ -542,7 +542,7 @@ impl From for vk::RayTracingShaderGroupCreateInfoKHR { /// Describes a type of ray tracing shader group, which is a collection of shaders which run in the /// specified mode. -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub enum RayTraceShaderGroupType { /// A shader group with a general shader. General, diff --git a/src/driver/shader.rs b/src/driver/shader.rs index 077fae45..d1afb219 100644 --- a/src/driver/shader.rs +++ b/src/driver/shader.rs @@ -258,7 +258,7 @@ impl PipelineDescriptorInfo { pattern = "owned" )] pub struct Shader { - /// The name of the entrypoint which will be executed by this shader. + /// The name of the entry point which will be executed by this shader. /// /// The default value is `main`. #[builder(default = "\"main\".to_owned()")] @@ -274,7 +274,7 @@ pub struct Shader { /// # inline_spirv::inline_spirv!(r#" /// #version 460 core /// - /// // Defaults to 6 if not set using ComputePipelineInfo.specialization_info! + /// // Defaults to 6 if not set using Shader specialization_info! /// layout(constant_id = 0) const uint MY_COUNT = 6; /// /// layout(set = 0, binding = 0) uniform sampler2D my_samplers[MY_COUNT]; @@ -394,6 +394,15 @@ impl Shader { Self::new(vk::ShaderStageFlags::INTERSECTION_KHR, spirv) } + /// Creates a new mesh shader. + /// + /// # Panics + /// + /// If the shader code is invalid. + pub fn new_mesh(spirv: impl ShaderCode) -> ShaderBuilder { + Self::new(vk::ShaderStageFlags::MESH_EXT, spirv) + } + /// Creates a new ray trace shader. /// /// # Panics @@ -412,9 +421,20 @@ impl Shader { Self::new(vk::ShaderStageFlags::RAYGEN_KHR, spirv) } + /// Creates a new mesh task shader. + /// + /// # Panics + /// + /// If the shader code is invalid. + pub fn new_task(spirv: impl ShaderCode) -> ShaderBuilder { + Self::new(vk::ShaderStageFlags::TASK_EXT, spirv) + } + /// Creates a new tesselation control shader. /// - /// _NOTE:_ May panic if the shader code is invalid. + /// # Panics + /// + /// If the shader code is invalid or not a multiple of four bytes in length. pub fn new_tesselation_ctrl(spirv: impl ShaderCode) -> ShaderBuilder { Self::new(vk::ShaderStageFlags::TESSELLATION_CONTROL, spirv) } @@ -841,16 +861,17 @@ impl ShaderBuilder { /// Builds a new `Shader`. pub fn build(mut self) -> Shader { + let entry_name = self.entry_name.as_deref().unwrap_or("main"); self.entry_point = Some( Shader::reflect_entry_point( - self.entry_name.as_deref().unwrap_or("main"), + entry_name, self.spirv.as_deref().unwrap(), self.specialization_info .as_ref() .map(|opt| opt.as_ref()) .unwrap_or_default(), ) - .expect("invalid shader code"), + .unwrap_or_else(|_| panic!("invalid shader code for entry name \'{entry_name}\'")), ); self.fallible_build() diff --git a/src/graph/pass_ref.rs b/src/graph/pass_ref.rs index d8b89d8f..4e2ce15b 100644 --- a/src/graph/pass_ref.rs +++ b/src/graph/pass_ref.rs @@ -496,12 +496,13 @@ impl<'a> Index for Bindings<'a> { /// # use ash::vk; /// # use screen_13::driver::{Device, DriverConfig, DriverError}; /// # use screen_13::driver::compute::{ComputePipeline, ComputePipelineInfo}; +/// # use screen_13::driver::shader::{Shader}; /// # use screen_13::graph::RenderGraph; /// # fn main() -> Result<(), DriverError> { /// # let device = Arc::new(Device::new(DriverConfig::new().build())?); -/// # let shader = [0u8; 1]; -/// # let info = ComputePipelineInfo::new(shader.as_slice()); -/// # let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, info)?); +/// # let info = ComputePipelineInfo::default(); +/// # let shader = Shader::new_compute([0u8; 1].as_slice()); +/// # let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, info, shader)?); /// # let mut my_graph = RenderGraph::new(); /// my_graph.begin_pass("my compute pass") /// .bind_pipeline(&my_compute_pipeline) @@ -548,14 +549,15 @@ impl<'a> Compute<'a> { /// # use screen_13::driver::{Device, DriverConfig, DriverError}; /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; /// # use screen_13::driver::compute::{ComputePipeline, ComputePipelineInfo}; + /// # use screen_13::driver::shader::{Shader}; /// # use screen_13::graph::RenderGraph; /// # fn main() -> Result<(), DriverError> { /// # let device = Arc::new(Device::new(DriverConfig::new().build())?); /// # let buf_info = BufferInfo::new(8, vk::BufferUsageFlags::STORAGE_BUFFER); /// # let my_buf = Buffer::create(&device, buf_info)?; - /// # let shader = [0u8; 1]; - /// # let pipeline_info = ComputePipelineInfo::new(shader.as_slice()); - /// # let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, pipeline_info)?); + /// # let info = ComputePipelineInfo::default(); + /// # let shader = Shader::new_compute([0u8; 1].as_slice()); + /// # let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, info, shader)?); /// # let mut my_graph = RenderGraph::new(); /// # let my_buf_node = my_graph.bind_node(my_buf); /// my_graph.begin_pass("fill my_buf_node with data") @@ -630,14 +632,15 @@ impl<'a> Compute<'a> { /// # use screen_13::driver::{Device, DriverConfig, DriverError}; /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; /// # use screen_13::driver::compute::{ComputePipeline, ComputePipelineInfo}; + /// # use screen_13::driver::shader::{Shader}; /// # use screen_13::graph::RenderGraph; /// # fn main() -> Result<(), DriverError> { /// # let device = Arc::new(Device::new(DriverConfig::new().build())?); /// # let buf_info = BufferInfo::new(8, vk::BufferUsageFlags::STORAGE_BUFFER); /// # let my_buf = Buffer::create(&device, buf_info)?; - /// # let shader = [0u8; 1]; - /// # let pipeline_info = ComputePipelineInfo::new(shader.as_slice()); - /// # let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, pipeline_info)?); + /// # let info = ComputePipelineInfo::default(); + /// # let shader = Shader::new_compute([0u8; 1].as_slice()); + /// # let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, info, shader)?); /// # let mut my_graph = RenderGraph::new(); /// # let my_buf_node = my_graph.bind_node(my_buf); /// const CMD_SIZE: usize = size_of::(); @@ -723,12 +726,13 @@ impl<'a> Compute<'a> { /// # use screen_13::driver::{Device, DriverConfig, DriverError}; /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; /// # use screen_13::driver::compute::{ComputePipeline, ComputePipelineInfo}; + /// # use screen_13::driver::shader::{Shader}; /// # use screen_13::graph::RenderGraph; /// # fn main() -> Result<(), DriverError> { /// # let device = Arc::new(Device::new(DriverConfig::new().build())?); - /// # let shader = [0u8; 1]; - /// # let pipeline_info = ComputePipelineInfo::new(shader.as_slice()); - /// # let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, pipeline_info)?); + /// # let info = ComputePipelineInfo::default(); + /// # let shader = Shader::new_compute([0u8; 1].as_slice()); + /// # let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, info, shader)?); /// # let mut my_graph = RenderGraph::new(); /// my_graph.begin_pass("compute the ultimate question") /// .bind_pipeline(&my_compute_pipeline) @@ -783,12 +787,13 @@ impl<'a> Compute<'a> { /// # use screen_13::driver::{Device, DriverConfig, DriverError}; /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; /// # use screen_13::driver::compute::{ComputePipeline, ComputePipelineInfo}; + /// # use screen_13::driver::shader::{Shader}; /// # use screen_13::graph::RenderGraph; /// # fn main() -> Result<(), DriverError> { /// # let device = Arc::new(Device::new(DriverConfig::new().build())?); - /// # let shader = [0u8; 1]; - /// # let pipeline_info = ComputePipelineInfo::new(shader.as_slice()); - /// # let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, pipeline_info)?); + /// # let info = ComputePipelineInfo::default(); + /// # let shader = Shader::new_compute([0u8; 1].as_slice()); + /// # let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, info, shader)?); /// # let mut my_graph = RenderGraph::new(); /// my_graph.begin_pass("calculate the wow factor") /// .bind_pipeline(&my_compute_pipeline) diff --git a/src/lib.rs b/src/lib.rs index dfc7e645..14fa8ce2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -228,12 +228,14 @@ Pipeline instances may be bound to a [`PassRef`] in order to execute the associa # use ash::vk; # use screen_13::driver::{Device, DriverConfig, DriverError}; # use screen_13::driver::compute::{ComputePipeline, ComputePipelineInfo}; +# use screen_13::driver::shader::{Shader}; # use screen_13::graph::RenderGraph; # fn main() -> Result<(), DriverError> { # let device = Arc::new(Device::new(DriverConfig::new().build())?); # let my_shader_code = [0u8; 1]; -# let info = ComputePipelineInfo::new(my_shader_code.as_slice()); -# let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, info)?); +# let info = ComputePipelineInfo::default(); +# let shader = Shader::new_compute(my_shader_code.as_slice()); +# let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, info, shader)?); # let mut graph = RenderGraph::new(); graph .begin_pass("My compute pass")