Hot-reloading
The code described in this section can be run from the
hot_reloading.rs example with
cargo run --features derive --example hot_reloading
. Complete source code is also provided at the
end of this page.
Hot-reloading lets you edit and apply changes to your WGSL shaders while the application using them is running. This
enables quick iterations and debugging on existing shaders without needing to rebuild and restart the application.
When using wgcore
(and its derive(Shader)
) to associate a .wgsl
file to a rust struct,
it is possible to benefit from hot-reloading with very little boilerplate:
// Initialize the gpu device and queue.
let gpu = GpuInstance::new().await?;
// Detects file changes.
let mut hot_reload = HotReloadState::new()?;
// Register the shader for hot-reloading.
MyShader::watch_sources(&mut hot_reload)?;
// Instantiate the compute pipeline.
let mut compute_pipeline = MyShader::from_device(gpu.device())?;
// This can be, for example, your game loop or window event loop.
loop {
// Detects file changes since the last call to `update_changes`.
hot_reload.update_changes();
// Reload the compute pipeline if it changed.
compute_pipeline.reload_if_changed(gpu.device(), &hot_reload);
}
How does this work?
Say you are working on an application’s code (like, for example, one of the examples of
wgsparkl), and depends on local version of its dependencies (like
wgebra) using a cargo patch.
Using the code snippet from the previous section, you can leverage hot-reloading on any shader, even the ones
from the local dependencies. The derive(Shader)
macros will automatically figure out the absolute path of all the
shaders at compile-time so the can be watched with Shader::watch_sources
.
This automatic detection of shader paths might not work properly if you run your application from a directory that is different from the root of the rust workspace it is built from. This is due to some limitations in the Rust libraries that will hopefully be stabilized in future versions of the compiler.
This won’t work for shaders of a dependency that is not available locally on your machine, since there is no way to
actual shader file that could be modified (since they are embedded in the library directly). In order to make it work
for these shaders, you can overwrite them with a local version of the shader by specifying
their path with Shader::set_wgsl_path
. After their path is overwritten, Shader::watch_sources
needs to be called
and hot-reloading will work.
If your shader depends on other shaders with shader(derive(...))
, then calling Shader::watch_sources
on the
top-level shader will automatically register all its dependencies as well for hot-reloading 🔥
Complete example
- main.rs
- kernel.wgsl
#[cfg(not(feature = "derive"))]
std::compile_error!(
r#"
###############################################################
## The `derive` feature must be enabled to run this example. ##
###############################################################
"#
);
use nalgebra::{DVector, Vector4};
use wgcore::composer::ComposerExt;
use wgcore::gpu::GpuInstance;
use wgcore::hot_reloading::HotReloadState;
use wgcore::kernel::{KernelInvocationBuilder, KernelInvocationQueue};
use wgcore::tensor::{GpuScalar, GpuVector};
use wgcore::Shader;
use wgpu::{BufferUsages, ComputePipeline};
#[derive(Shader)]
#[shader(src = "hot_reloading.wgsl", composable = false)]
struct ShaderHotReloading {
main: ComputePipeline,
}
#[async_std::main]
async fn main() -> anyhow::Result<()> {
// Initialize the gpu device and its queue.
//
// Note that `GpuInstance` is just a simple helper struct for initializing the gpu resources.
// You are free to initialize them independently if more control is needed, or reuse the ones
// that were already created/owned by e.g., a game engine.
let gpu = GpuInstance::new().await?;
// Load and compile our kernel. The `from_device` function was generated by the `Shader` derive.
// Note that its dependency to `Composable` is automatically resolved by the `Shader` derive
// too.
let mut kernel = ShaderHotReloading::from_device(gpu.device())?;
// Create the buffers.
let buffer = GpuScalar::init(
gpu.device(),
0u32,
BufferUsages::STORAGE | BufferUsages::COPY_SRC,
);
let staging = GpuScalar::init(
gpu.device(),
0u32,
BufferUsages::COPY_DST | BufferUsages::MAP_READ,
);
// Init hot-reloading.
let mut hot_reload = HotReloadState::new()?;
ShaderHotReloading::watch_sources(&mut hot_reload)?;
// Queue the operation.
println!("#############################");
println!("Edit the file `hot_reloading.wgsl`.\nThe updated result will be printed below whenever a change is detected.");
println!("#############################");
for loop_id in 0.. {
// Detect & apply changes.
hot_reload.update_changes();
match kernel.reload_if_changed(gpu.device(), &hot_reload) {
Ok(changed) => {
if changed || loop_id == 0 {
// We detected a change (or this is the first loop).
// Read the result.
let mut queue = KernelInvocationQueue::new(gpu.device());
KernelInvocationBuilder::new(&mut queue, &kernel.main)
.bind0([buffer.buffer()])
.queue(1);
// Encode & submit the operation to the gpu.
let mut encoder = gpu.device().create_command_encoder(&Default::default());
// Run our kernel.
queue.encode(&mut encoder, None);
// Copy the result to the staging buffer.
staging.copy_from(&mut encoder, &buffer);
gpu.queue().submit(Some(encoder.finish()));
let result_read = staging.read(gpu.device()).await.unwrap();
println!("Current result value: {}", result_read[0]);
}
}
Err(e) => {
// Hot-reloading failed, likely due to a syntax error in the shader.
println!("Hot reloading error: {:?}", e);
}
}
}
Ok(())
}
@group(0) @binding(0)
var<storage, read_write> a: u32;
@compute @workgroup_size(1, 1, 1)
fn main(@builtin(global_invocation_id) invocation_id: vec3<u32>) {
a = 1u; // Change this value and save the file while running the `hot_reloading` example.
}