Skip to content

Commit 79a825c

Browse files
authored
Support hot-reloading + shader overwrite (#1)
* feat: hot-reloading * feat: support dynamic paths for hot-reloading + shader overwriting * chore: add more wgcore examples * fix hot-reloading boilerplate generation * chore: clippy fixes
1 parent e56bf9e commit 79a825c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+773
-133
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dist
1818
assets
1919

2020
*_bg.wasm
21+
website
2122

2223
# JetBrain IDEs
2324
#

.idea/vcs.xml

-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/wg-math.iml

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ resolver = "2"
88

99
[workspace.dependencies]
1010
nalgebra = { version = "0.33.1", features = ["convert-bytemuck"] }
11-
wgpu = { version = "22.1", features = ["naga-ir"] }
11+
wgpu = { version = "23", features = ["naga-ir"] }
1212
bytemuck = { version = "1", features = ["derive"] }
1313
anyhow = "1"
1414
async-channel = "2"
15-
naga_oil = "0.15"
15+
naga_oil = "0.16"
1616
thiserror = "1"
1717
encase = { version = "0.10.0", features = ["nalgebra"] }
1818

crates/wgcore-derive/src/lib.rs

+80-20
Original file line numberDiff line numberDiff line change
@@ -73,28 +73,43 @@ pub fn derive_shader(item: TokenStream) -> TokenStream {
7373

7474
let shader_defs = derive_shaders.shader_defs.map(|defs| quote! { #defs() })
7575
.unwrap_or_else(|| quote! { Default::default() });
76-
let src = derive_shaders.src_fn.map(|f| quote! { #f(include_str!(#src_path)) })
77-
.unwrap_or_else(|| quote! { include_str!(#src_path).to_string() });
76+
let raw_src = quote! {
77+
// First try to find a path from the shader registry.
78+
// If doesn’t exist in the registry, try the absolute path.
79+
// If it doesn’t exist in the absolute path, load the embedded string.
80+
if let Some(path) = Self::absolute_path() {
81+
// TODO: handle error
82+
std::fs::read_to_string(path).unwrap()
83+
} else {
84+
include_str!(#src_path).to_string()
85+
}
86+
};
87+
88+
let src = derive_shaders.src_fn.map(|f| quote! { #f(&#raw_src) })
89+
.unwrap_or_else(|| quote! { #raw_src });
90+
let naga_module = quote! {
91+
Self::composer().and_then(|mut c|
92+
c.make_naga_module(wgcore::re_exports::naga_oil::compose::NagaModuleDescriptor {
93+
source: &Self::src(),
94+
file_path: Self::FILE_PATH,
95+
shader_defs: #shader_defs,
96+
..Default::default()
97+
})
98+
)
99+
};
78100

79101
let from_device = if !kernels_to_build.is_empty() {
80102
quote! {
81-
let module = Self::composer()
82-
.make_naga_module(wgcore::re_exports::naga_oil::compose::NagaModuleDescriptor {
83-
source: &Self::src(),
84-
file_path: Self::FILE_PATH,
85-
shader_defs: #shader_defs,
86-
..Default::default()
87-
})
88-
.unwrap();
89-
Self {
103+
let module = #naga_module?;
104+
Ok(Self {
90105
#(
91106
#kernels_to_build
92107
)*
93-
}
108+
})
94109
}
95110
} else {
96111
quote ! {
97-
Self
112+
Ok(Self)
98113
}
99114
};
100115

@@ -112,19 +127,37 @@ pub fn derive_shader(item: TokenStream) -> TokenStream {
112127
impl wgcore::shader::Shader for #struct_identifier {
113128
const FILE_PATH: &'static str = #src_path;
114129

115-
fn from_device(device: &wgcore::re_exports::Device) -> Self {
130+
fn from_device(device: &wgcore::re_exports::Device) -> Result<Self, wgcore::re_exports::ComposerError> {
116131
#from_device
117132
}
118133

119-
// TODO: could we avoid the String allocation here?
120134
fn src() -> String {
121135
#src
122136
}
123137

124-
fn compose(composer: &mut wgcore::re_exports::Composer) -> &mut wgcore::re_exports::Composer {
138+
fn naga_module() -> Result<wgcore::re_exports::wgpu::naga::Module, wgcore::re_exports::ComposerError> {
139+
#naga_module
140+
}
141+
142+
fn absolute_path() -> Option<std::path::PathBuf> {
143+
if let Some(path) = wgcore::ShaderRegistry::get().get_path::<#struct_identifier>() {
144+
Some(path.clone())
145+
} else {
146+
// NOTE: this is a bit fragile, and won’t work if the current working directory
147+
// isn’t the root of the workspace the binary crate is being run from.
148+
// Ideally we need `proc_macro2::Span::source_file` but it is currently unstable.
149+
// See: https://users.rust-lang.org/t/how-to-get-the-macro-called-file-path-in-a-rust-procedural-macro/109613/5
150+
std::path::Path::new(file!())
151+
.parent()?
152+
.join(Self::FILE_PATH)
153+
.canonicalize().ok()
154+
}
155+
}
156+
157+
fn compose(composer: &mut wgcore::re_exports::Composer) -> Result<(), wgcore::re_exports::ComposerError> {
125158
use wgcore::composer::ComposerExt;
126159
#(
127-
#to_derive::compose(composer);
160+
#to_derive::compose(composer)?;
128161
)*
129162

130163
if #composable {
@@ -134,10 +167,37 @@ pub fn derive_shader(item: TokenStream) -> TokenStream {
134167
file_path: Self::FILE_PATH,
135168
shader_defs: #shader_defs,
136169
..Default::default()
137-
})
138-
.unwrap();
170+
})?;
171+
}
172+
173+
Ok(())
174+
}
175+
176+
/*
177+
* Hot reloading.
178+
*/
179+
fn watch_sources(state: &mut wgcore::hot_reloading::HotReloadState) -> wgcore::re_exports::notify::Result<()> {
180+
#(
181+
#to_derive::watch_sources(state)?;
182+
)*
183+
184+
if let Some(path) = Self::absolute_path() {
185+
state.watch_file(&path)?;
139186
}
140-
composer
187+
188+
Ok(())
189+
}
190+
191+
fn needs_reload(state: &wgcore::hot_reloading::HotReloadState) -> bool {
192+
#(
193+
if #to_derive::needs_reload(state) {
194+
return true;
195+
}
196+
)*
197+
198+
Self::absolute_path()
199+
.map(|path| state.file_changed(&path))
200+
.unwrap_or_default()
141201
}
142202
}
143203
}

crates/wgcore/Cargo.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ license = "MIT OR Apache-2.0"
1111

1212
[features]
1313
derive = ["wgcore-derive"]
14+
hot_reloading = [] # "notify"]
1415

1516
[dependencies]
1617
nalgebra = { workspace = true }
17-
wgpu = { workspace = true }
18+
wgpu = { workspace = true, features = ["wgsl"] }
1819
bytemuck = { workspace = true }
1920
anyhow = { workspace = true }
2021
async-channel = { workspace = true }
@@ -24,6 +25,7 @@ encase = { workspace = true }
2425
wgcore-derive = { version = "0.1", path = "../wgcore-derive", optional = true }
2526

2627
dashmap = "5"
28+
notify = { version = "7" } # , optional = true }
2729

2830

2931
[dev-dependencies]
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use nalgebra::DVector;
2+
use wgcore::composer::ComposerExt;
3+
use wgcore::gpu::GpuInstance;
4+
use wgcore::kernel::{KernelInvocationBuilder, KernelInvocationQueue};
5+
use wgcore::tensor::GpuVector;
6+
use wgcore::Shader;
7+
use wgpu::{BufferUsages, ComputePipeline};
8+
9+
#[async_std::main]
10+
async fn main() -> anyhow::Result<()> {
11+
// Initialize the gpu device and its queue.
12+
//
13+
// Note that `GpuInstance` is just a simple helper struct for initializing the gpu resources.
14+
// You are free to initialize them independently if more control is needed, or reuse the ones
15+
// that were already created/owned by e.g., a game engine.
16+
let gpu = GpuInstance::new().await?;
17+
18+
// Create the buffers.
19+
const LEN: u32 = 1000;
20+
let buffer_data = DVector::from_fn(LEN as usize, |i, _| i as u32);
21+
let buffer = GpuVector::init(gpu.device(), &buffer_data, BufferUsages::STORAGE | BufferUsages::COPY_SRC);
22+
let staging = GpuVector::uninit(gpu.device(), LEN, BufferUsages::COPY_DST | BufferUsages::MAP_READ);
23+
24+
// Queue the operation.
25+
// Encode & submit the operation to the gpu.
26+
let mut encoder = gpu.device().create_command_encoder(&Default::default());
27+
// Copy the buffer to the staging buffer.
28+
staging.copy_from(&mut encoder, &buffer);
29+
gpu.queue().submit(Some(encoder.finish()));
30+
31+
let read = staging.read(gpu.device()).await?;
32+
assert_eq!(buffer_data, DVector::from(read));
33+
println!("Buffer copy & read succeeded!");
34+
35+
Ok(())
36+
}

crates/wgcore/examples/compose.rs

+9-3
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,19 @@ use wgpu::{BufferUsages, ComputePipeline};
1010
// Note that we don’t build any compute pipeline from this wgsl file.
1111
#[derive(Shader)]
1212
#[shader(
13-
src = "composable.wgsl" // Shader source code, will be embedded in the exe with `include_str!`
13+
src = "compose_dependency.wgsl" // Shader source code, will be embedded in the exe with `include_str!`
1414
)]
1515
struct Composable;
1616

1717
#[derive(Shader)]
1818
#[shader(
1919
derive(Composable), // This shader depends on the `Composable` shader.
20-
src = "kernel.wgsl", // Shader source code, will be embedded in the exe with `include_str!`.
20+
src = "compose_kernel.wgsl", // Shader source code, will be embedded in the exe with `include_str!`.
2121
composable = false // This shader doesn’t export any symbols reusable from other wgsl shaders.
2222
)]
2323
struct WgKernel {
24+
// This ComputePipeline field indicates that the Shader macro needs to generate the boilerplate
25+
// for loading the compute pipeline in `WgKernel::from_device`.
2426
main: ComputePipeline,
2527
}
2628

@@ -42,7 +44,11 @@ async fn main() -> anyhow::Result<()> {
4244
// Load and compile our kernel. The `from_device` function was generated by the `Shader` derive.
4345
// Note that its dependency to `Composable` is automatically resolved by the `Shader` derive
4446
// too.
45-
let kernel = WgKernel::from_device(gpu.device());
47+
let kernel = WgKernel::from_device(gpu.device())?;
48+
println!("######################################");
49+
println!("###### Composed shader sources: ######");
50+
println!("######################################");
51+
println!("{}", WgKernel::flat_wgsl()?);
4652

4753
// Create the buffers.
4854
const LEN: u32 = 1000;
File renamed without changes.

crates/wgcore/examples/encase.rs

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use nalgebra::{DVector, Vector4};
2+
use wgcore::composer::ComposerExt;
3+
use wgcore::gpu::GpuInstance;
4+
use wgcore::kernel::{KernelInvocationBuilder, KernelInvocationQueue};
5+
use wgcore::tensor::GpuVector;
6+
use wgcore::Shader;
7+
use wgpu::{BufferUsages, ComputePipeline};
8+
9+
#[derive(Copy, Clone, PartialEq, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
10+
#[repr(C)]
11+
pub struct BytemuckStruct {
12+
value: f32,
13+
}
14+
15+
#[derive(Copy, Clone, PartialEq, Debug, Default, encase::ShaderType)]
16+
#[repr(C)]
17+
pub struct EncaseStruct {
18+
value: f32,
19+
// This implies some internal padding, so we can’t rely on bytemuck.
20+
// Encase will handle that properly.
21+
value2: Vector4<f32>,
22+
}
23+
24+
#[derive(Shader)]
25+
#[shader(
26+
src = "encase.wgsl",
27+
composable = false
28+
)]
29+
struct ShaderEncase {
30+
main: ComputePipeline
31+
}
32+
33+
#[async_std::main]
34+
async fn main() -> anyhow::Result<()> {
35+
// Initialize the gpu device and its queue.
36+
//
37+
// Note that `GpuInstance` is just a simple helper struct for initializing the gpu resources.
38+
// You are free to initialize them independently if more control is needed, or reuse the ones
39+
// that were already created/owned by e.g., a game engine.
40+
let gpu = GpuInstance::new().await?;
41+
42+
// Load and compile our kernel. The `from_device` function was generated by the `Shader` derive.
43+
// Note that its dependency to `Composable` is automatically resolved by the `Shader` derive
44+
// too.
45+
let kernel = ShaderEncase::from_device(gpu.device())?;
46+
47+
// Create the buffers.
48+
const LEN: u32 = 1000;
49+
let a_data = (0..LEN).map(|x| EncaseStruct { value: x as f32, value2: Vector4::repeat(x as f32 * 10.0)}).collect::<Vec<_>>();
50+
let b_data = (0..LEN).map(|x| BytemuckStruct { value: x as f32}).collect::<Vec<_>>();
51+
// Call `encase` instead of `init` because `EncaseStruct` isn’t `Pod`.
52+
// The `encase` function has a bit of overhead so bytemuck should be preferred whenever possible.
53+
let a_buf = GpuVector::encase(gpu.device(), &a_data, BufferUsages::STORAGE);
54+
let b_buf = GpuVector::init(gpu.device(), &b_data, BufferUsages::STORAGE);
55+
56+
// Queue the operation.
57+
let mut queue = KernelInvocationQueue::new(gpu.device());
58+
KernelInvocationBuilder::new(&mut queue, &kernel.main)
59+
.bind0([a_buf.buffer(), b_buf.buffer()])
60+
.queue(LEN.div_ceil(64));
61+
62+
// Encode & submit the operation to the gpu.
63+
let mut encoder = gpu.device().create_command_encoder(&Default::default());
64+
queue.encode(&mut encoder, None);
65+
gpu.queue().submit(Some(encoder.finish()));
66+
67+
Ok(())
68+
}

crates/wgcore/examples/encase.wgsl

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
@group(0) @binding(0)
2+
var<storage, read_write> a: array<EncaseStruct>;
3+
@group(0) @binding(1)
4+
var<storage, read> b: array<BytemuckStruct>;
5+
6+
struct BytemuckStruct {
7+
value: f32,
8+
}
9+
10+
struct EncaseStruct {
11+
value: f32,
12+
value2: vec4<f32>
13+
}
14+
15+
@compute @workgroup_size(64, 1, 1)
16+
fn main(@builtin(global_invocation_id) invocation_id: vec3<u32>) {
17+
let i = invocation_id.x;
18+
if i < arrayLength(&a) {
19+
a[i].value += b[i].value;
20+
a[i].value2 += vec4(b[i].value);
21+
}
22+
}

0 commit comments

Comments
 (0)