👀 hot shader reloading (#655)

* Update builder to use a shared validation method

* Add the error for using print_metadata and watching

We cannot use print_metadata with watching
because print_metadata only makes sense
in build scripts, but watching does not
and would instead stall the script

* Add the initial implementation of watching

* Make hot reloading work in the wgpu example

* Attempt to address CI failures

* Add exception for notify CC0-1.0 license

* Address review comments

Co-authored-by: khyperia <github@khyperia.com>
This commit is contained in:
Daniel McNab 2021-06-09 09:30:44 +01:00 committed by GitHub
parent 0410fc53e1
commit 3bbe963998
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 370 additions and 89 deletions

73
Cargo.lock generated
View File

@ -841,6 +841,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "fsevent-sys"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c0e564d24da983c053beff1bb7178e237501206840a3e6bf4e267b9e8ae734a"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "fuchsia-zircon" name = "fuchsia-zircon"
version = "0.3.3" version = "0.3.3"
@ -1227,6 +1236,26 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "inotify"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b031475cb1b103ee221afb806a23d35e0570bf7271d7588762ceba8127ed43b3"
dependencies = [
"bitflags",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "inplace_it" name = "inplace_it"
version = "0.3.3" version = "0.3.3"
@ -1485,6 +1514,19 @@ dependencies = [
"winapi 0.2.8", "winapi 0.2.8",
] ]
[[package]]
name = "mio"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956"
dependencies = [
"libc",
"log",
"miow 0.3.7",
"ntapi",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "mio-extras" name = "mio-extras"
version = "2.0.6" version = "2.0.6"
@ -1493,7 +1535,7 @@ checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
dependencies = [ dependencies = [
"lazycell", "lazycell",
"log", "log",
"mio", "mio 0.6.23",
"slab", "slab",
] ]
@ -1649,6 +1691,32 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "notify"
version = "5.0.0-pre.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51f18203a26893ca1d3526cf58084025d5639f91c44f8b70ab3b724f60e819a0"
dependencies = [
"bitflags",
"crossbeam-channel",
"filetime",
"fsevent-sys",
"inotify",
"libc",
"mio 0.7.11",
"walkdir",
"winapi 0.3.9",
]
[[package]]
name = "ntapi"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
dependencies = [
"winapi 0.3.9",
]
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.44" version = "0.1.44"
@ -2338,6 +2406,7 @@ name = "spirv-builder"
version = "0.4.0-alpha.9" version = "0.4.0-alpha.9"
dependencies = [ dependencies = [
"memchr", "memchr",
"notify",
"raw-string", "raw-string",
"rustc_codegen_spirv", "rustc_codegen_spirv",
"serde", "serde",
@ -3074,7 +3143,7 @@ dependencies = [
"lazy_static", "lazy_static",
"libc", "libc",
"log", "log",
"mio", "mio 0.6.23",
"mio-extras", "mio-extras",
"ndk", "ndk",
"ndk-glue", "ndk-glue",

View File

@ -10,6 +10,7 @@ license = "MIT OR Apache-2.0"
default = ["use-compiled-tools"] default = ["use-compiled-tools"]
use-installed-tools = ["rustc_codegen_spirv/use-installed-tools"] use-installed-tools = ["rustc_codegen_spirv/use-installed-tools"]
use-compiled-tools = ["rustc_codegen_spirv/use-compiled-tools"] use-compiled-tools = ["rustc_codegen_spirv/use-compiled-tools"]
watch = ["notify"]
[dependencies] [dependencies]
memchr = "2.3" memchr = "2.3"
@ -18,3 +19,5 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
# See comment in lib.rs invoke_rustc for why this is here # See comment in lib.rs invoke_rustc for why this is here
rustc_codegen_spirv = { path = "../rustc_codegen_spirv", default-features = false } rustc_codegen_spirv = { path = "../rustc_codegen_spirv", default-features = false }
notify = { version = "5.0.0-pre.10", optional = true }

View File

@ -54,6 +54,8 @@
#![allow()] #![allow()]
mod depfile; mod depfile;
#[cfg(feature = "watch")]
mod watch;
use raw_string::{RawStr, RawString}; use raw_string::{RawStr, RawString};
use serde::Deserialize; use serde::Deserialize;
@ -71,10 +73,12 @@ pub use rustc_codegen_spirv::rspirv::spirv::Capability;
pub use rustc_codegen_spirv::{CompileResult, ModuleResult}; pub use rustc_codegen_spirv::{CompileResult, ModuleResult};
#[derive(Debug)] #[derive(Debug)]
#[non_exhaustive]
pub enum SpirvBuilderError { pub enum SpirvBuilderError {
CratePathDoesntExist(PathBuf), CratePathDoesntExist(PathBuf),
BuildFailed, BuildFailed,
MultiModuleWithPrintMetadata, MultiModuleWithPrintMetadata,
WatchWithPrintMetadata,
MetadataFileMissing(std::io::Error), MetadataFileMissing(std::io::Error),
MetadataFileMalformed(serde_json::Error), MetadataFileMalformed(serde_json::Error),
} }
@ -89,6 +93,9 @@ impl fmt::Display for SpirvBuilderError {
SpirvBuilderError::MultiModuleWithPrintMetadata => f.write_str( SpirvBuilderError::MultiModuleWithPrintMetadata => f.write_str(
"Multi-module build cannot be used with print_metadata = MetadataPrintout::Full", "Multi-module build cannot be used with print_metadata = MetadataPrintout::Full",
), ),
SpirvBuilderError::WatchWithPrintMetadata => {
f.write_str("Watching within build scripts will prevent build completion")
}
SpirvBuilderError::MetadataFileMissing(_) => { SpirvBuilderError::MetadataFileMissing(_) => {
f.write_str("Multi-module metadata file missing") f.write_str("Multi-module metadata file missing")
} }
@ -244,22 +251,47 @@ impl SpirvBuilder {
/// Builds the module. If `print_metadata` is [`MetadataPrintout::Full`], you usually don't have to inspect the path /// Builds the module. If `print_metadata` is [`MetadataPrintout::Full`], you usually don't have to inspect the path
/// in the result, as the environment variable for the path to the module will already be set. /// in the result, as the environment variable for the path to the module will already be set.
pub fn build(self) -> Result<CompileResult, SpirvBuilderError> { pub fn build(mut self) -> Result<CompileResult, SpirvBuilderError> {
self.validate_running_conditions()?;
let metadata_file = invoke_rustc(&self)?;
match self.print_metadata {
MetadataPrintout::Full | MetadataPrintout::DependencyOnly => {
leaf_deps(&metadata_file, |artifact| {
println!("cargo:rerun-if-changed={}", artifact)
})
// Close enough
.map_err(SpirvBuilderError::MetadataFileMissing)?;
}
MetadataPrintout::None => (),
}
let metadata = self.parse_metadata_file(&metadata_file)?;
Ok(metadata)
}
pub(crate) fn validate_running_conditions(&mut self) -> Result<(), SpirvBuilderError> {
if (self.print_metadata == MetadataPrintout::Full) && self.multimodule { if (self.print_metadata == MetadataPrintout::Full) && self.multimodule {
return Err(SpirvBuilderError::MultiModuleWithPrintMetadata); return Err(SpirvBuilderError::MultiModuleWithPrintMetadata);
} }
if !self.path_to_crate.is_dir() { if !self.path_to_crate.is_dir() {
return Err(SpirvBuilderError::CratePathDoesntExist(self.path_to_crate)); return Err(SpirvBuilderError::CratePathDoesntExist(std::mem::take(
&mut self.path_to_crate,
)));
} }
let metadata_file = invoke_rustc(&self)?; Ok(())
let metadata_contents = }
File::open(&metadata_file).map_err(SpirvBuilderError::MetadataFileMissing)?;
pub(crate) fn parse_metadata_file(
&self,
at: &Path,
) -> Result<CompileResult, SpirvBuilderError> {
let metadata_contents = File::open(&at).map_err(SpirvBuilderError::MetadataFileMissing)?;
let metadata: CompileResult = serde_json::from_reader(BufReader::new(metadata_contents)) let metadata: CompileResult = serde_json::from_reader(BufReader::new(metadata_contents))
.map_err(SpirvBuilderError::MetadataFileMalformed)?; .map_err(SpirvBuilderError::MetadataFileMalformed)?;
match &metadata.module { match &metadata.module {
ModuleResult::SingleModule(spirv_module) => { ModuleResult::SingleModule(spirv_module) => {
assert!(!self.multimodule); assert!(!self.multimodule);
let env_var = metadata_file.file_name().unwrap().to_str().unwrap(); let env_var = at.file_name().unwrap().to_str().unwrap();
if self.print_metadata == MetadataPrintout::Full { if self.print_metadata == MetadataPrintout::Full {
println!("cargo:rustc-env={}={}", env_var, spirv_module.display()); println!("cargo:rustc-env={}={}", env_var, spirv_module.display());
} }
@ -424,13 +456,8 @@ fn invoke_rustc(builder: &SpirvBuilder) -> Result<PathBuf, SpirvBuilderError> {
// that ended up on stdout instead of stderr. // that ended up on stdout instead of stderr.
let stdout = String::from_utf8(build.stdout).unwrap(); let stdout = String::from_utf8(build.stdout).unwrap();
let artifact = get_last_artifact(&stdout); let artifact = get_last_artifact(&stdout);
if build.status.success() { if build.status.success() {
match builder.print_metadata { Ok(artifact.expect("Artifact created when compilation succeeded"))
MetadataPrintout::Full | MetadataPrintout::DependencyOnly => print_deps_of(&artifact),
MetadataPrintout::None => (),
}
Ok(artifact)
} else { } else {
Err(SpirvBuilderError::BuildFailed) Err(SpirvBuilderError::BuildFailed)
} }
@ -442,7 +469,7 @@ struct RustcOutput {
filenames: Option<Vec<String>>, filenames: Option<Vec<String>>,
} }
fn get_last_artifact(out: &str) -> PathBuf { fn get_last_artifact(out: &str) -> Option<PathBuf> {
let last = out let last = out
.lines() .lines()
.filter_map(|line| match serde_json::from_str::<RustcOutput>(line) { .filter_map(|line| match serde_json::from_str::<RustcOutput>(line) {
@ -462,28 +489,33 @@ fn get_last_artifact(out: &str) -> PathBuf {
.unwrap() .unwrap()
.into_iter() .into_iter()
.filter(|v| v.ends_with(".spv")); .filter(|v| v.ends_with(".spv"));
let filename = filenames.next().expect("Crate had no .spv artifacts"); let filename = filenames.next()?;
assert_eq!(filenames.next(), None, "Crate had multiple .spv artifacts"); assert_eq!(filenames.next(), None, "Crate had multiple .spv artifacts");
filename.into() Some(filename.into())
} }
fn print_deps_of(artifact: &Path) { /// Internally iterate through the leaf dependencies of the artifact at `artifact`
fn leaf_deps(artifact: &Path, mut handle: impl FnMut(&RawStr)) -> std::io::Result<()> {
let deps_file = artifact.with_extension("d"); let deps_file = artifact.with_extension("d");
let mut deps_map = HashMap::new(); let mut deps_map = HashMap::new();
depfile::read_deps_file(&deps_file, |item, deps| { depfile::read_deps_file(&deps_file, |item, deps| {
deps_map.insert(item, deps); deps_map.insert(item, deps);
Ok(()) Ok(())
}) })?;
.expect("Could not read dep file"); fn recurse(
fn recurse(map: &HashMap<RawString, Vec<RawString>>, artifact: &RawStr) { map: &HashMap<RawString, Vec<RawString>>,
artifact: &RawStr,
handle: &mut impl FnMut(&RawStr),
) {
match map.get(artifact) { match map.get(artifact) {
Some(entries) => { Some(entries) => {
for entry in entries { for entry in entries {
recurse(map, entry) recurse(map, entry, handle)
} }
} }
None => println!("cargo:rerun-if-changed={}", artifact), None => handle(artifact),
} }
} }
recurse(&deps_map, artifact.to_str().unwrap().into()); recurse(&deps_map, artifact.to_str().unwrap().into(), &mut handle);
Ok(())
} }

View File

@ -0,0 +1,104 @@
use std::{collections::HashSet, sync::mpsc::sync_channel};
use notify::{Event, RecursiveMode, Watcher};
use rustc_codegen_spirv::CompileResult;
use crate::{leaf_deps, SpirvBuilder, SpirvBuilderError};
impl SpirvBuilder {
/// Watches the module for changes using [`notify`](https://crates.io/crates/notify).
///
/// This is a blocking operation, wand should never return in the happy path
pub fn watch(
mut self,
on_compilation_finishes: impl Fn(CompileResult),
) -> Result<(), SpirvBuilderError> {
self.validate_running_conditions()?;
if !matches!(self.print_metadata, crate::MetadataPrintout::None) {
return Err(SpirvBuilderError::WatchWithPrintMetadata);
}
let metadata_result = crate::invoke_rustc(&self);
// Load the dependencies of the thing
let metadata_file = match metadata_result {
Ok(path) => path,
Err(_) => {
let (tx, rx) = sync_channel(0);
// Fall back to watching from the crate root if the inital compilation fails
let mut watcher =
notify::immediate_watcher(move |event: notify::Result<Event>| match event {
Ok(e) => match e.kind {
notify::EventKind::Access(_) => (),
notify::EventKind::Any
| notify::EventKind::Create(_)
| notify::EventKind::Modify(_)
| notify::EventKind::Remove(_)
| notify::EventKind::Other => {
let _ = tx.try_send(());
}
},
Err(e) => println!("notify error: {:?}", e),
})
.expect("Could create watcher");
// This is likely to notice changes in the `target` dir, however, given that `cargo watch` doesn't seem to handle that,
watcher
.watch(&self.path_to_crate, RecursiveMode::Recursive)
.expect("Could watch crate root");
loop {
rx.recv().expect("Watcher still alive");
let metadata_file = crate::invoke_rustc(&self);
if let Ok(f) = metadata_file {
break f;
}
}
}
};
let metadata = self.parse_metadata_file(&metadata_file)?;
on_compilation_finishes(metadata);
let mut watched_paths = HashSet::new();
let (tx, rx) = sync_channel(0);
let mut watcher =
notify::immediate_watcher(move |event: notify::Result<Event>| match event {
Ok(e) => match e.kind {
notify::EventKind::Access(_) => (),
notify::EventKind::Any
| notify::EventKind::Create(_)
| notify::EventKind::Modify(_)
| notify::EventKind::Remove(_)
| notify::EventKind::Other => {
let _ = tx.try_send(());
}
},
Err(e) => println!("notify error: {:?}", e),
})
.expect("Could create watcher");
leaf_deps(&metadata_file, |it| {
let path = it.to_path().unwrap();
if watched_paths.insert(path.to_owned()) {
watcher
.watch(it.to_path().unwrap(), RecursiveMode::NonRecursive)
.expect("Cargo dependencies are valid files");
}
})
.expect("Could read dependencies file");
loop {
rx.recv().expect("Watcher still alive");
let metadata_result = crate::invoke_rustc(&self);
if let Ok(file) = metadata_result {
// We can bubble this error up because it's an internal error (e.g. rustc_codegen_spirv's version of CompileResult is somehow out of sync)
let metadata = self.parse_metadata_file(&file)?;
leaf_deps(&file, |it| {
let path = it.to_path().unwrap();
if watched_paths.insert(path.to_owned()) {
watcher
.watch(it.to_path().unwrap(), RecursiveMode::NonRecursive)
.expect("Cargo dependencies are valid files");
}
})
.expect("Could read dependencies file");
on_compilation_finishes(metadata);
}
}
}
}

View File

@ -69,6 +69,11 @@ exceptions = [
# https://tldrlegal.com/license/do-what-the-f*ck-you-want-to-public-license-(wtfpl) # https://tldrlegal.com/license/do-what-the-f*ck-you-want-to-public-license-(wtfpl)
{ allow = ["WTFPL"], name = "xkb" }, { allow = ["WTFPL"], name = "xkb" },
{ allow = ["WTFPL"], name = "xkbcommon-sys" }, { allow = ["WTFPL"], name = "xkbcommon-sys" },
# CC0 is a permissive license but somewhat unclear status for source code
# so we prefer to not have dependencies using it
# https://tldrlegal.com/license/creative-commons-cc0-1.0-universal
{ allow = ["CC0-1.0"], name = "notify" },
] ]
copyleft = "deny" copyleft = "deny"

View File

@ -25,7 +25,7 @@ clap = "3.0.0-beta.2"
strum = { version = "0.20", default_features = false, features = ["derive"] } strum = { version = "0.20", default_features = false, features = ["derive"] }
[target.'cfg(not(any(target_os = "android", target_arch = "wasm32")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_arch = "wasm32")))'.dependencies]
spirv-builder = { path = "../../../crates/spirv-builder", default-features = false } spirv-builder = { path = "../../../crates/spirv-builder", default-features = false, features = ["watch"] }
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
ndk-glue = "0.2" ndk-glue = "0.2"

View File

@ -1,6 +1,6 @@
use wgpu::util::DeviceExt; use wgpu::util::DeviceExt;
use super::{shader_module, Options}; use super::Options;
use futures::future::join; use futures::future::join;
use std::{convert::TryInto, future::Future, num::NonZeroU64, time::Duration}; use std::{convert::TryInto, future::Future, num::NonZeroU64, time::Duration};
@ -15,7 +15,8 @@ fn block_on<T>(future: impl Future<Output = T>) -> T {
} }
pub fn start(options: &Options) { pub fn start(options: &Options) {
let shader_binary = shader_module(options.shader); let rx = crate::maybe_watch(options.shader, true);
let shader_binary = rx.recv().expect("Should send one binary");
block_on(start_internal(options, shader_binary)) block_on(start_internal(options, shader_binary))
} }

View File

@ -1,4 +1,8 @@
use super::{shader_module, Options}; use std::thread::spawn;
use crate::maybe_watch;
use super::Options;
use shared::ShaderConstants; use shared::ShaderConstants;
use winit::{ use winit::{
event::{ElementState, Event, KeyboardInput, MouseButton, VirtualKeyCode, WindowEvent}, event::{ElementState, Event, KeyboardInput, MouseButton, VirtualKeyCode, WindowEvent},
@ -35,10 +39,10 @@ fn mouse_button_index(button: MouseButton) -> usize {
} }
async fn run( async fn run(
event_loop: EventLoop<()>, event_loop: EventLoop<wgpu::ShaderModuleDescriptor<'static>>,
window: Window, window: Window,
swapchain_format: wgpu::TextureFormat, swapchain_format: wgpu::TextureFormat,
shader_binary: wgpu::ShaderModuleDescriptor<'_>, shader_binary: wgpu::ShaderModuleDescriptor<'static>,
) { ) {
let size = window.inner_size(); let size = window.inner_size();
let instance = wgpu::Instance::new(wgpu::BackendBit::VULKAN | wgpu::BackendBit::METAL); let instance = wgpu::Instance::new(wgpu::BackendBit::VULKAN | wgpu::BackendBit::METAL);
@ -80,7 +84,6 @@ async fn run(
.expect("Failed to create device"); .expect("Failed to create device");
// Load the shaders from disk // Load the shaders from disk
let module = device.create_shader_module(&shader_binary);
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None, label: None,
@ -91,38 +94,8 @@ async fn run(
}], }],
}); });
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { let mut render_pipeline =
label: None, create_pipeline(&device, &pipeline_layout, swapchain_format, shader_binary);
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &module,
entry_point: shaders::main_vs,
buffers: &[],
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: wgpu::CullMode::None,
polygon_mode: wgpu::PolygonMode::Fill,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: shaders::main_fs,
targets: &[wgpu::ColorTargetState {
format: swapchain_format,
alpha_blend: wgpu::BlendState::REPLACE,
color_blend: wgpu::BlendState::REPLACE,
write_mask: wgpu::ColorWrite::ALL,
}],
}),
});
let mut sc_desc = wgpu::SwapChainDescriptor { let mut sc_desc = wgpu::SwapChainDescriptor {
usage: wgpu::TextureUsage::RENDER_ATTACHMENT, usage: wgpu::TextureUsage::RENDER_ATTACHMENT,
@ -149,7 +122,8 @@ async fn run(
// Have the closure take ownership of the resources. // Have the closure take ownership of the resources.
// `event_loop.run` never returns, therefore we must do this to ensure // `event_loop.run` never returns, therefore we must do this to ensure
// the resources are properly cleaned up. // the resources are properly cleaned up.
let _ = (&instance, &adapter, &module, &pipeline_layout); let _ = (&instance, &adapter, &pipeline_layout);
let render_pipeline = &mut render_pipeline;
*control_flow = ControlFlow::Wait; *control_flow = ControlFlow::Wait;
match event { match event {
@ -225,7 +199,7 @@ async fn run(
mouse_button_press_time, mouse_button_press_time,
}; };
rpass.set_pipeline(&render_pipeline); rpass.set_pipeline(render_pipeline);
rpass.set_push_constants(wgpu::ShaderStage::all(), 0, unsafe { rpass.set_push_constants(wgpu::ShaderStage::all(), 0, unsafe {
any_as_u8_slice(&push_constants) any_as_u8_slice(&push_constants)
}); });
@ -282,16 +256,75 @@ async fn run(
drag_end_y = cursor_y; drag_end_y = cursor_y;
} }
} }
Event::UserEvent(new_module) => {
*render_pipeline =
create_pipeline(&device, &pipeline_layout, swapchain_format, new_module);
window.request_redraw();
*control_flow = ControlFlow::Poll;
}
_ => {} _ => {}
} }
}); });
} }
fn create_pipeline(
device: &wgpu::Device,
pipeline_layout: &wgpu::PipelineLayout,
swapchain_format: wgpu::TextureFormat,
shader_binary: wgpu::ShaderModuleDescriptor<'_>,
) -> wgpu::RenderPipeline {
let module = device.create_shader_module(&shader_binary);
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(pipeline_layout),
vertex: wgpu::VertexState {
module: &module,
entry_point: shaders::main_vs,
buffers: &[],
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: wgpu::CullMode::None,
polygon_mode: wgpu::PolygonMode::Fill,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: shaders::main_fs,
targets: &[wgpu::ColorTargetState {
format: swapchain_format,
alpha_blend: wgpu::BlendState::REPLACE,
color_blend: wgpu::BlendState::REPLACE,
write_mask: wgpu::ColorWrite::ALL,
}],
}),
})
}
pub fn start(options: &Options) { pub fn start(options: &Options) {
// Build the shader before we pop open a window, since it might take a while. // Build the shader before we pop open a window, since it might take a while.
let shader_binary = shader_module(options.shader); let rx = maybe_watch(options.shader, false);
let initial_shader = rx.recv().expect("Initial shader is required");
let event_loop = EventLoop::new(); let event_loop = EventLoop::with_user_event();
let proxy = event_loop.create_proxy();
let thread = spawn(move || loop {
while let Ok(result) = rx.recv() {
match proxy.send_event(result) {
Ok(()) => {}
// If something goes wrong, close this thread
Err(_) => break,
}
}
});
std::mem::forget(thread);
let window = winit::window::WindowBuilder::new() let window = winit::window::WindowBuilder::new()
.with_title("Rust GPU - wgpu") .with_title("Rust GPU - wgpu")
.with_inner_size(winit::dpi::LogicalSize::new(1280.0, 720.0)) .with_inner_size(winit::dpi::LogicalSize::new(1280.0, 720.0))
@ -317,6 +350,7 @@ pub fn start(options: &Options) {
event_loop, event_loop,
window, window,
wgpu::TextureFormat::Bgra8Unorm, wgpu::TextureFormat::Bgra8Unorm,
initial_shader,
)); ));
} else { } else {
wgpu_subscriber::initialize_default_subscriber(None); wgpu_subscriber::initialize_default_subscriber(None);
@ -328,7 +362,8 @@ pub fn start(options: &Options) {
} else { } else {
wgpu::TextureFormat::Bgra8UnormSrgb wgpu::TextureFormat::Bgra8UnormSrgb
}, },
shader_binary, initial_shader,
)); ));
} }
} }

View File

@ -42,6 +42,8 @@
rust_2018_idioms rust_2018_idioms
)] )]
use std::sync::mpsc::{self, Receiver};
use clap::Clap; use clap::Clap;
use strum::{Display, EnumString}; use strum::{Display, EnumString};
@ -56,10 +58,16 @@ pub enum RustGPUShader {
Mouse, Mouse,
} }
fn shader_module(shader: RustGPUShader) -> wgpu::ShaderModuleDescriptor<'static> { fn maybe_watch(
shader: RustGPUShader,
force_no_watch: bool,
) -> Receiver<wgpu::ShaderModuleDescriptor<'static>> {
// This bound needs to be 1, because in cases where this function is used for direct building (e.g. for the compute example or on android)
// we send the value directly in the same thread. This avoids deadlocking in those cases.
let (tx, rx) = mpsc::sync_channel(1);
#[cfg(not(any(target_os = "android", target_arch = "wasm32")))] #[cfg(not(any(target_os = "android", target_arch = "wasm32")))]
{ {
use spirv_builder::{Capability, MetadataPrintout, SpirvBuilder}; use spirv_builder::{Capability, CompileResult, MetadataPrintout, SpirvBuilder};
use std::borrow::Cow; use std::borrow::Cow;
use std::path::PathBuf; use std::path::PathBuf;
// Hack: spirv_builder builds into a custom directory if running under cargo, to not // Hack: spirv_builder builds into a custom directory if running under cargo, to not
@ -86,29 +94,53 @@ fn shader_module(shader: RustGPUShader) -> wgpu::ShaderModuleDescriptor<'static>
for &cap in capabilities { for &cap in capabilities {
builder = builder.capability(cap); builder = builder.capability(cap);
} }
if force_no_watch {
let compile_result = builder.build().unwrap(); let compile_result = builder.build().unwrap();
handle_builder_result(compile_result, &tx);
} else {
let thread = std::thread::spawn(move || {
builder
.watch(|compile_result| {
handle_builder_result(compile_result, &tx);
})
.expect("Configuration is correct for watching")
});
std::mem::forget(thread);
}
fn handle_builder_result(
compile_result: CompileResult,
tx: &mpsc::SyncSender<wgpu::ShaderModuleDescriptor<'static>>,
) {
let module_path = compile_result.module.unwrap_single(); let module_path = compile_result.module.unwrap_single();
let data = std::fs::read(module_path).unwrap(); let data = std::fs::read(module_path).unwrap();
let spirv = wgpu::util::make_spirv(&data); let spirv = wgpu::util::make_spirv(&data);
let spirv = match spirv { let spirv = match spirv {
wgpu::ShaderSource::Wgsl(cow) => wgpu::ShaderSource::Wgsl(Cow::Owned(cow.into_owned())), wgpu::ShaderSource::Wgsl(cow) => {
wgpu::ShaderSource::Wgsl(Cow::Owned(cow.into_owned()))
}
wgpu::ShaderSource::SpirV(cow) => { wgpu::ShaderSource::SpirV(cow) => {
wgpu::ShaderSource::SpirV(Cow::Owned(cow.into_owned())) wgpu::ShaderSource::SpirV(Cow::Owned(cow.into_owned()))
} }
}; };
wgpu::ShaderModuleDescriptor { tx.send(wgpu::ShaderModuleDescriptor {
label: None, label: None,
source: spirv, source: spirv,
flags: wgpu::ShaderFlags::default(), flags: wgpu::ShaderFlags::default(),
})
.expect("Rx is still alive");
} }
} }
#[cfg(any(target_os = "android", target_arch = "wasm32"))] #[cfg(any(target_os = "android", target_arch = "wasm32"))]
match shader { {
tx.send(match shader {
RustGPUShader::Simplest => wgpu::include_spirv!(env!("simplest_shader.spv")), RustGPUShader::Simplest => wgpu::include_spirv!(env!("simplest_shader.spv")),
RustGPUShader::Sky => wgpu::include_spirv!(env!("sky_shader.spv")), RustGPUShader::Sky => wgpu::include_spirv!(env!("sky_shader.spv")),
RustGPUShader::Compute => wgpu::include_spirv!(env!("compute_shader.spv")), RustGPUShader::Compute => wgpu::include_spirv!(env!("compute_shader.spv")),
RustGPUShader::Mouse => wgpu::include_spirv!(env!("mouse_shader.spv")), RustGPUShader::Mouse => wgpu::include_spirv!(env!("mouse_shader.spv")),
})
.expect("rx to be alive")
} }
rx
} }
fn is_compute_shader(shader: RustGPUShader) -> bool { fn is_compute_shader(shader: RustGPUShader) -> bool {