1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
// The Vulkan half of the single-source shader compile: everything the
// declarations in `concinnity_core::render::slang_programs::vk` need a compiler, a
// content-addressed cache, or a filesystem for. The declarations themselves are
// re-exported here, so every call site still names them through this module.
use concinnity_slang as slang;
use super::builtins::Ctx;
pub(super) use concinnity_core::render::slang_programs::vk::*;
// What a declaration can do once a compiler and a cache are in reach. A trait
// rather than an inherent impl because `SlangProgram` is defined in
// `core::render`, which is `no_std` and knows nothing about either; bringing
// this into scope keeps `PROGRAM.compile(&ctx)` reading as it did before the
// declarations moved.
pub(crate) trait SlangCompile {
fn source(&self, ctx: &Ctx) -> String;
fn cache_key<'a>(&self, source: &'a str) -> crate::shader_cache::Key<'a>;
fn compile(&self, ctx: &Ctx) -> Result<Vec<u8>, String>;
}
impl SlangCompile for SlangProgram {
// Assemble the exact source text this program compiles under `ctx`.
fn source(&self, ctx: &Ctx) -> String {
let pool = ctx.pool_size.to_string();
let probes = ctx.probe_count.to_string();
let mut defines: Vec<(&str, &str)> = self.gates.iter().map(|g| (*g, "1")).collect();
if self.msaa {
defines.push(("USE_MSAA", if ctx.msaa { "1" } else { "0" }));
}
if self.sizes == Sizes::PoolAndProbes {
debug_assert!(
ctx.pool_size > 0,
"{}: sized program assembled with no pool count",
self.label
);
defines.push(("POOL_SIZE", pool.as_str()));
}
if self.sizes != Sizes::None {
debug_assert!(
ctx.probe_count > 0,
"{}: sized program assembled with no probe count",
self.label
);
defines.push(("MAX_PROBES", probes.as_str()));
}
crate::slang_source::assemble(ctx.hot_reload, self.file, &defines, &[])
}
// The shader-cache key for `source`. Shared by the runtime compile path
// and the export-time precompile so the two can never key differently.
fn cache_key<'a>(&self, source: &'a str) -> crate::shader_cache::Key<'a> {
crate::shader_cache::Key {
compiler: "slang",
source,
entry: self.entry,
target: "spirv",
options: 0,
}
}
// The SPIR-V for this program: the copy the build script embedded when it
// was built from this exact source, else a compile (reusing a cached
// artifact when this source has been compiled before).
//
// Matching on the source digest rather than on hot-reload being off is what
// makes this reach the binaries a developer runs: `cn debug` and `cn editor`
// both enable hot-reload, and a mode check would leave them compiling every
// shader at startup and needing slangc to do it. It is also what lets a
// device that sizes its pool or probe array differently from the build fall
// through to a compile without any special case for it.
fn compile(&self, ctx: &Ctx) -> Result<Vec<u8>, String> {
let source = self.source(ctx);
// Only a program that reads the sample count has two artifacts; keying
// the rest on it would miss the single one they do have.
let name = spirv_artifact_name(self.label, self.msaa && ctx.msaa);
if let Some((digest, bytes)) = embedded_spirv(&name)
&& digest == concinnity_core::render::slang_source::source_digest(&source)
{
return Ok(bytes.to_vec());
}
let key = self.cache_key(&source);
crate::shader_cache::cached(&key, self.label, || compile_uncached(self, &source))
}
}
// The build script's precompiled SPIR-V. Every name misses on a host that had
// no slangc at build time, and the renderer compiles instead -- the only path
// that then needs slangc at runtime.
include!(concat!(env!("OUT_DIR"), "/engine_spirv.rs"));
// The lookup key for one program's artifact. Mirrors `spirv_artifact_name` in
// build.rs. The MSAA flag is part of it because a program that reads the main
// pass's depth compiles into two artifacts from one label, and handing one to
// the other would sample the wrong depth image.
fn spirv_artifact_name(label: &str, msaa: bool) -> String {
if msaa {
format!("{label}.msaa")
} else {
label.to_string()
}
}
pub(super) fn compile_uncached(program: &SlangProgram, source: &str) -> Result<Vec<u8>, String> {
let job = slang::SlangJob {
source,
file_name: program.file,
entries: &[program.entry],
target: slang::SlangTarget::Spirv,
};
let work = crate::compiler_work::dir()?;
slang::compile(&job, work.path())
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx(pool_size: usize, probe_count: usize) -> Ctx {
Ctx {
hot_reload: false,
msaa: false,
pool_size,
probe_count,
}
}
#[test]
fn sized_programs_inject_their_counts_and_gates_lead() {
let src = MAIN_BINDLESS_FRAG.source(&ctx(17, 5));
assert!(src.starts_with("#define POOL_SIZE 17\n#define MAX_PROBES 5\n"));
// The SSR resolve reads the probe array but none of the texture pool,
// so it takes the probe count alone.
let src = SSR_RESOLVE.source(&ctx(17, 5));
assert!(src.starts_with("#define MAX_PROBES 5\n"));
let src = HIZ_INIT_MSAA.source(&ctx(0, 0));
assert!(src.starts_with("#define HIZ_INIT_MSAA 1\n"));
let src = LIGHT_CULL.source(&ctx(0, 0));
assert!(!src.starts_with("#define"));
}
// An unreplaced `{...}` fragment marker would reach slangc as a syntax
// error at renderer init; catch a missing splice here instead.
#[test]
fn every_program_assembles_with_its_fragments_spliced() {
for p in ALL {
let src = p.source(&ctx(4, 4));
for marker in [
"{POST_COMMON}",
"{OBJECT_COMMON}",
"{PROBE_TYPES}",
"{PROBE_COMMON}",
"{RT_TYPES}",
"{RT_TRACE}",
"{PARTICLE_TYPES}",
] {
assert!(
!src.contains(marker),
"{}: unspliced fragment marker {marker}",
p.label
);
}
}
}
// Reading the main pass's depth is what makes a program's assembly depend on
// the host's sample count, and the export-time precompile enumerates both
// variants for exactly those. A program that gained or lost the dependency
// silently would leave a bundle cold for one MSAA mode.
#[test]
fn only_the_depth_reading_programs_take_the_sample_count() {
let mut sampled: Vec<&str> = ALL.iter().filter(|p| p.msaa).map(|p| p.label).collect();
sampled.sort_unstable();
assert_eq!(
sampled,
[
"decal_frag.slang",
"fog_frag.slang",
"glass_frag.slang",
"glass_frag_rt.slang",
"glass_frag_rt_textured.slang",
"glass_mesh_frag_rt.slang",
"glass_mesh_frag_rt_textured.slang",
"glass_mesh_vert.slang",
"glass_vert.slang",
"line_frag.slang",
"water_frag.slang",
"water_frag_rt.slang",
"water_frag_rt_textured.slang",
"water_vert.slang",
]
);
}
// Two programs collide when they would compile identical source with the
// same entry; the table must not declare the same artifact twice.
#[test]
fn table_has_no_duplicate_programs() {
let mut seen = std::collections::HashSet::new();
for p in ALL {
let src = p.source(&ctx(4, 4));
assert!(
seen.insert((src, p.entry)),
"duplicate program: {}",
p.label
);
}
}
#[test]
fn every_key_field_tracks_the_program() {
let a = MAIN_BINDLESS_VERT.source(&ctx(4, 4));
let b = MAIN_BINDLESS_VERT.source(&ctx(5, 4));
assert_ne!(a, b, "pool size must change the assembled source");
let key = MAIN_BINDLESS_VERT.cache_key(&a);
assert_eq!(key.compiler, "slang");
assert_eq!(key.target, "spirv");
assert_eq!(key.entry, "vertex_main_bindless");
}
// The single-source files carry cluster constants that must track the
// Rust values the CPU sizes buffers with.
#[test]
fn cluster_constants_match_render_types() {
use crate::gfx::render_types::{CLUSTER_LIGHT_LIST_STRIDE, MAX_LIGHTS_PER_CLUSTER};
for src in [
concinnity_core::render::shaders::LIGHT_CULL,
concinnity_core::render::shaders::MAIN_SHADING,
] {
assert!(src.contains(&format!(
"CLUSTER_LIGHT_LIST_STRIDE = {CLUSTER_LIGHT_LIST_STRIDE}u"
)));
}
assert!(
concinnity_core::render::shaders::LIGHT_CULL.contains(&format!(
"MAX_LIGHTS_PER_CLUSTER = {MAX_LIGHTS_PER_CLUSTER}u"
))
);
}
}