Skip to content

Commit 8535bb3

Browse files
committed
Minimally integrate SPIR-T (opt-in via RUSTGPU_CODEGEN_ARGS=--spirt).
1 parent 78130e1 commit 8535bb3

File tree

7 files changed

+214
-5
lines changed

7 files changed

+214
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3434
- `RUSTGPU_RUSTFLAGS="..."` for shader `RUSTFLAGS="..."`
3535
- `RUSTGPU_CODEGEN_ARGS="..."` for shader "codegen args" (i.e. `RUSTFLAGS=-Cllvm-args="..."`)
3636
(check out ["codegen args" docs](docs/src/codegen-args.md), or run with `RUSTGPU_CODEGEN_ARGS=--help` to see the full list of options)
37+
- [PR#940](https://github.com/EmbarkStudios/rust-gpu/pull/940) integrated the experimental [`SPIR-🇹` shader IR framework](https://github.com/EmbarkStudios/spirt) into the linker
38+
(opt-in via `RUSTGPU_CODEGEN_ARGS=--spirt`, see also [the `--spirt` docs](docs/src/codegen-args.md#--spirt), for more details)
3739

3840
### Changed 🛠️
3941
- [PR#958](https://github.com/EmbarkStudios/rust-gpu/pull/958) updated toolchain to `nightly-2022-10-29`

Cargo.lock

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,7 @@ codegen-units = 256
5252
opt-level = 3
5353
incremental = true
5454
codegen-units = 256
55+
56+
[patch.crates-io]
57+
# HACK(eddyb) only needed until `spirt 0.1.0` is released.
58+
spirt = { git = "https://github.com/EmbarkStudios/spirt.git", rev = "cda161b7e7b336685448ab0a7f8cfe96bd90e752" }

crates/rustc_codegen_spirv/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ serde_json = "1.0"
5656
smallvec = { version = "1.6.1", features = ["union"] }
5757
spirv-tools = { version = "0.9", default-features = false }
5858
rustc_codegen_spirv-types.workspace = true
59+
spirt = "0.1.0"
5960

6061
[dev-dependencies]
6162
pipe = "0.4"

crates/rustc_codegen_spirv/src/codegen_cx/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,12 @@ impl CodegenArgs {
351351
);
352352
opts.optflag("", "no-structurize", "disables CFG structurization");
353353

354+
opts.optflag(
355+
"",
356+
"spirt",
357+
"use SPIR-T for legalization (see also `docs/src/codegen-args.md`)",
358+
);
359+
354360
// NOTE(eddyb) these are debugging options that used to be env vars
355361
// (for more information see `docs/src/codegen-args.md`).
356362
opts.optopt(
@@ -365,6 +371,12 @@ impl CodegenArgs {
365371
"dump modules immediately after multimodule splitting, to files in DIR",
366372
"DIR",
367373
);
374+
opts.optopt(
375+
"",
376+
"dump-spirt-passes",
377+
"dump the SPIR-T module across passes, to FILE and FILE.html",
378+
"FILE",
379+
);
368380
opts.optflag(
369381
"",
370382
"specializer-debug",
@@ -511,6 +523,7 @@ impl CodegenArgs {
511523
dce: !matches.opt_present("no-dce"),
512524
compact_ids: !matches.opt_present("no-compact-ids"),
513525
structurize: !matches.opt_present("no-structurize"),
526+
spirt: matches.opt_present("spirt"),
514527

515528
// FIXME(eddyb) deduplicate between `CodegenArgs` and `linker::Options`.
516529
emit_multiple_modules: module_output_type == ModuleOutputType::Multiple,
@@ -521,6 +534,7 @@ impl CodegenArgs {
521534
// (for more information see `docs/src/codegen-args.md`).
522535
dump_post_merge: matches_opt_path("dump-post-merge"),
523536
dump_post_split: matches_opt_dump_dir_path("dump-post-split"),
537+
dump_spirt_passes: matches_opt_path("dump-spirt-passes"),
524538
specializer_debug: matches.opt_present("specializer-debug"),
525539
specializer_dump_instances: matches_opt_path("specializer-dump-instances"),
526540
print_all_zombie: matches.opt_present("print-all-zombie"),

crates/rustc_codegen_spirv/src/linker/mod.rs

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ pub struct Options {
3434
pub compact_ids: bool,
3535
pub dce: bool,
3636
pub structurize: bool,
37+
pub spirt: bool,
3738

3839
pub emit_multiple_modules: bool,
3940
pub spirv_metadata: SpirvMetadata,
@@ -48,6 +49,7 @@ pub struct Options {
4849
// (for more information see `docs/src/codegen-args.md`).
4950
pub dump_post_merge: Option<PathBuf>,
5051
pub dump_post_split: Option<PathBuf>,
52+
pub dump_spirt_passes: Option<PathBuf>,
5153
pub specializer_debug: bool,
5254
pub specializer_dump_instances: Option<PathBuf>,
5355
pub print_all_zombie: bool,
@@ -141,10 +143,10 @@ pub fn link(sess: &Session, mut inputs: Vec<Module>, opts: &Options) -> Result<L
141143
bound += module.header.as_ref().unwrap().bound - 1;
142144
let this_version = module.header.as_ref().unwrap().version();
143145
if version != this_version {
144-
sess.fatal(format!(
146+
return Err(sess.err(format!(
145147
"cannot link two modules with different SPIR-V versions: v{}.{} and v{}.{}",
146148
version.0, version.1, this_version.0, this_version.1
147-
))
149+
)));
148150
}
149151
}
150152

@@ -234,25 +236,70 @@ pub fn link(sess: &Session, mut inputs: Vec<Module>, opts: &Options) -> Result<L
234236
);
235237
}
236238

239+
// NOTE(eddyb) with SPIR-T, we can do `mem2reg` before inlining, too!
240+
if opts.spirt {
241+
if opts.dce {
242+
let _timer = sess.timer("link_dce-before-inlining");
243+
dce::dce(&mut output);
244+
}
245+
246+
let _timer = sess.timer("link_block_ordering_pass_and_mem2reg-before-inlining");
247+
let mut pointer_to_pointee = FxHashMap::default();
248+
let mut constants = FxHashMap::default();
249+
let mut u32 = None;
250+
for inst in &output.types_global_values {
251+
match inst.class.opcode {
252+
Op::TypePointer => {
253+
pointer_to_pointee
254+
.insert(inst.result_id.unwrap(), inst.operands[1].unwrap_id_ref());
255+
}
256+
Op::TypeInt
257+
if inst.operands[0].unwrap_literal_int32() == 32
258+
&& inst.operands[1].unwrap_literal_int32() == 0 =>
259+
{
260+
assert!(u32.is_none());
261+
u32 = Some(inst.result_id.unwrap());
262+
}
263+
Op::Constant if u32.is_some() && inst.result_type == u32 => {
264+
let value = inst.operands[0].unwrap_literal_int32();
265+
constants.insert(inst.result_id.unwrap(), value);
266+
}
267+
_ => {}
268+
}
269+
}
270+
for func in &mut output.functions {
271+
simple_passes::block_ordering_pass(func);
272+
// Note: mem2reg requires functions to be in RPO order (i.e. block_ordering_pass)
273+
mem2reg::mem2reg(
274+
output.header.as_mut().unwrap(),
275+
&mut output.types_global_values,
276+
&pointer_to_pointee,
277+
&constants,
278+
func,
279+
);
280+
destructure_composites::destructure_composites(func);
281+
}
282+
}
283+
237284
{
238285
let _timer = sess.timer("link_inline");
239286
inline::inline(sess, &mut output)?;
240287
}
241288

242289
if opts.dce {
243-
let _timer = sess.timer("link_dce");
290+
let _timer = sess.timer("link_dce-after-inlining");
244291
dce::dce(&mut output);
245292
}
246293

247-
let mut output = if opts.structurize {
294+
let mut output = if opts.structurize && !opts.spirt {
248295
let _timer = sess.timer("link_structurize");
249296
structurizer::structurize(output)
250297
} else {
251298
output
252299
};
253300

254301
{
255-
let _timer = sess.timer("link_block_ordering_pass_and_mem2reg");
302+
let _timer = sess.timer("link_block_ordering_pass_and_mem2reg-after-inlining");
256303
let mut pointer_to_pointee = FxHashMap::default();
257304
let mut constants = FxHashMap::default();
258305
let mut u32 = None;
@@ -290,6 +337,86 @@ pub fn link(sess: &Session, mut inputs: Vec<Module>, opts: &Options) -> Result<L
290337
}
291338
}
292339

340+
if opts.spirt {
341+
let mut per_pass_module_for_dumping = vec![];
342+
let mut after_pass = |pass, module: &spirt::Module| {
343+
if opts.dump_spirt_passes.is_some() {
344+
per_pass_module_for_dumping.push((pass, module.clone()));
345+
}
346+
};
347+
348+
let spv_bytes = {
349+
let _timer = sess.timer("assemble-to-spv_bytes-for-spirt");
350+
spirv_tools::binary::from_binary(&output.assemble()).to_vec()
351+
};
352+
let cx = std::rc::Rc::new(spirt::Context::new());
353+
let mut module = {
354+
let _timer = sess.timer("spirt::Module::lower_from_spv_file");
355+
match spirt::Module::lower_from_spv_bytes(cx.clone(), spv_bytes) {
356+
Ok(module) => module,
357+
Err(e) => {
358+
use rspirv::binary::Disassemble;
359+
360+
return Err(sess
361+
.struct_err(format!("{e}"))
362+
.note(format!(
363+
"while lowering this SPIR-V module to SPIR-T:\n{}",
364+
output.disassemble()
365+
))
366+
.emit());
367+
}
368+
}
369+
};
370+
after_pass("lower_from_spv", &module);
371+
372+
if opts.structurize {
373+
{
374+
let _timer = sess.timer("spirt::legalize::structurize_func_cfgs");
375+
spirt::passes::legalize::structurize_func_cfgs(&mut module);
376+
}
377+
after_pass("structurize_func_cfgs", &module);
378+
}
379+
380+
// NOTE(eddyb) this should be *before* `lift_to_spv` below,
381+
// so if that fails, the dump could be used to debug it.
382+
if let Some(dump_file) = &opts.dump_spirt_passes {
383+
// HACK(eddyb) using `.with_extension(...)` may replace part of a
384+
// `.`-containing file name, but we want to always append `.html`.
385+
let mut dump_file_html = dump_file.as_os_str().to_owned();
386+
dump_file_html.push(".html");
387+
388+
let plan = spirt::print::Plan::for_versions(
389+
&cx,
390+
per_pass_module_for_dumping
391+
.iter()
392+
.map(|(pass, module)| (format!("after {pass}"), module)),
393+
);
394+
let pretty = plan.pretty_print();
395+
396+
// FIXME(eddyb) don't allocate whole `String`s here.
397+
std::fs::write(dump_file, pretty.to_string()).unwrap();
398+
std::fs::write(
399+
dump_file_html,
400+
pretty
401+
.render_to_html()
402+
.with_dark_mode_support()
403+
.to_html_doc(),
404+
)
405+
.unwrap();
406+
}
407+
408+
let spv_words = {
409+
let _timer = sess.timer("spirt::Module::lift_to_spv_module_emitter");
410+
module.lift_to_spv_module_emitter().unwrap().words
411+
};
412+
output = {
413+
let _timer = sess.timer("parse-spv_words-from-spirt");
414+
let mut loader = Loader::new();
415+
rspirv::binary::parse_words(&spv_words, &mut loader).unwrap();
416+
loader.module()
417+
};
418+
}
419+
293420
{
294421
let _timer = sess.timer("peephole_opts");
295422
let types = peephole_opts::collect_types(&output);

docs/src/codegen-args.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,18 @@ anyway, be careful).
138138
### `--no-structurize`
139139

140140
Disables CFG structurization. Probably results in invalid modules.
141+
142+
### `--spirt`
143+
144+
Enables using the experimental [`SPIR-🇹` shader IR framework](https://github.com/EmbarkStudios/spirt) in the linker - more specifically, this:
145+
- adds a `SPIR-V -> SPIR-🇹 -> SPIR-V` roundtrip
146+
(future `SPIR-🇹` passes would go in the middle of this, and eventually codegen might not produce `SPIR-V` at all)
147+
- replaces the existing structurizer with `SPIR-🇹` structurization (which is more robust and can e.g. handle `OpPhi`s)
148+
- runs some existing `SPIR-V` legalization/optimization passes (`mem2reg`) *before* inlining, instead of *only after* (as the `OpPhi`s they would produce are no longer an issue for structurization)
149+
150+
For more information, also see [the `SPIR-🇹` repository](https://github.com/EmbarkStudios/spirt).
151+
152+
### `--dump-spirt-passes FILE`
153+
154+
Dump the `SPIR-🇹` module across passes (i.e. all of the versions before/after each pass), as a combined report, to `FILE` and `FILE.html`.
155+
<sub>(the `.html` version of the report is the recommended form for viewing, as it uses tabling for versions, syntax-highlighting-like styling, and use->def linking)</sub>

0 commit comments

Comments
 (0)