Skip to content

Commit

Permalink
Lower aborts (incl. panics) to "return from entry-point", instead of …
Browse files Browse the repository at this point in the history
…infinite loops.
  • Loading branch information
eddyb committed Jul 7, 2023
1 parent b2e5eb7 commit ce8c3f8
Show file tree
Hide file tree
Showing 14 changed files with 272 additions and 77 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Changed 🛠
- [PR#1070](https://github.com/EmbarkStudios/rust-gpu/pull/1070) made panics (via the `abort` intrinsic)
early-exit (i.e. `return` from) the shader entry-point, instead of looping infinitely
- [PR#1071](https://github.com/EmbarkStudios/rust-gpu/pull/1071) updated toolchain to `nightly-2023-05-27`

## [0.8.0]
Expand Down
3 changes: 1 addition & 2 deletions crates/rustc_codegen_spirv/src/builder/builder_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2457,8 +2457,7 @@ impl<'a, 'tcx> BuilderMethods<'a, 'tcx> for Builder<'a, 'tcx> {
let is_standard_debug = [Op::Line, Op::NoLine].contains(&inst.class.opcode);
let is_custom_debug = inst.class.opcode == Op::ExtInst
&& inst.operands[0].unwrap_id_ref() == custom_ext_inst_set_import
&& [CustomOp::SetDebugSrcLoc, CustomOp::ClearDebugSrcLoc]
.contains(&CustomOp::decode_from_ext_inst(inst));
&& CustomOp::decode_from_ext_inst(inst).is_debuginfo();
!(is_standard_debug || is_custom_debug)
});

Expand Down
20 changes: 11 additions & 9 deletions crates/rustc_codegen_spirv/src/builder/intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use super::Builder;
use crate::abi::ConvSpirvType;
use crate::builder_spirv::{SpirvValue, SpirvValueExt};
use crate::codegen_cx::CodegenCx;
use crate::custom_insts::CustomInst;
use crate::spirv_type::SpirvType;
use rspirv::spirv::GLOp;
use rustc_codegen_ssa::mir::operand::OperandRef;
Expand Down Expand Up @@ -337,17 +338,18 @@ impl<'a, 'tcx> IntrinsicCallMethods<'tcx> for Builder<'a, 'tcx> {
}

fn abort(&mut self) {
// HACK(eddyb) there is no `abort` or `trap` instruction in SPIR-V,
// so the best thing we can do is inject an infinite loop.
// (While there is `OpKill`, it doesn't really have the right semantics)
let abort_loop_bb = self.append_sibling_block("abort_loop");
let abort_continue_bb = self.append_sibling_block("abort_continue");
self.br(abort_loop_bb);
// FIXME(eddyb) this should be cached more efficiently.
let void_ty = SpirvType::Void.def(rustc_span::DUMMY_SP, self);

self.switch_to_block(abort_loop_bb);
self.br(abort_loop_bb);
// HACK(eddyb) there is no `abort` or `trap` instruction in SPIR-V,
// so the best thing we can do is use our own custom instruction.
self.custom_inst(void_ty, CustomInst::Abort);
self.unreachable();

self.switch_to_block(abort_continue_bb);
// HACK(eddyb) we still need an active block in case the user of this
// `Builder` will continue to emit instructions after the `.abort()`.
let post_abort_dead_bb = self.append_sibling_block("post_abort_dead");
self.switch_to_block(post_abort_dead_bb);
}

fn assume(&mut self, _val: Self::Value) {
Expand Down
2 changes: 1 addition & 1 deletion crates/rustc_codegen_spirv/src/codegen_cx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ pub struct CodegenCx<'tcx> {

/// All `panic!(...)`s and builtin panics (from MIR `Assert`s) call into one
/// of these lang items, which we always replace with an "abort", erasing
/// anything passed in (and that "abort" is just an infinite loop for now).
/// anything passed in.
//
// FIXME(eddyb) we should not erase anywhere near as much, but `format_args!`
// is not representable due to containg Rust slices, and Rust 2021 has made
Expand Down
51 changes: 51 additions & 0 deletions crates/rustc_codegen_spirv/src/custom_insts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,55 @@ def_custom_insts! {
// Leave the most recent inlined call frame entered by a `PushInlinedCallFrame`
// (i.e. the inlined call frames form a virtual call stack in debuginfo).
3 => PopInlinedCallFrame,

// [Semantic] Similar to some proposed `OpAbort`, but without any ability to
// indicate abnormal termination (so it's closer to `OpTerminateInvocation`,
// which we could theoretically use, but that's limited to fragment shaders).
//
// Lowering takes advantage of inlining happening before CFG structurization
// (by forcing inlining of `Abort`s all the way up to entry-points, as to be
// able to turn the `Abort`s into regular `OpReturn`s, from an entry-point),
// but if/when inlining works on structured SPIR-T instead, it's not much
// harder to make any call to a "may (transitively) abort" function branch on
// an additional returned `bool`, instead (i.e. a form of emulated unwinding).
//
// As this is a custom terminator, it must only appear before `OpUnreachable`,
// *without* any instructions in between (not even debuginfo ones).
//
// FIXME(eddyb) long-term this kind of custom control-flow could be generalized
// to fully emulate unwinding (resulting in codegen similar to `?` in functions
// returning `Option` or `Result`), to e.g. run destructors, or even allow
// users to do `catch_unwind` at the top-level of their shader to handle
// panics specially (e.g. by appending to a custom buffer, or using some
// specific color in a fragment shader, to indicate a panic happened).
4 => Abort,
}

impl CustomOp {
/// Returns `true` iff this `CustomOp` is a custom debuginfo instruction,
/// i.e. non-semantic (can/must be ignored wherever `OpLine`/`OpNoLine` are).
pub fn is_debuginfo(self) -> bool {
match self {
CustomOp::SetDebugSrcLoc
| CustomOp::ClearDebugSrcLoc
| CustomOp::PushInlinedCallFrame
| CustomOp::PopInlinedCallFrame => true,

CustomOp::Abort => false,
}
}

/// Returns `true` iff this `CustomOp` is a custom terminator instruction,
/// i.e. semantic and must always appear just before an `OpUnreachable`
/// standard terminator (without even debuginfo in between the two).
pub fn is_terminator(self) -> bool {
match self {
CustomOp::SetDebugSrcLoc
| CustomOp::ClearDebugSrcLoc
| CustomOp::PushInlinedCallFrame
| CustomOp::PopInlinedCallFrame => false,

CustomOp::Abort => true,
}
}
}
151 changes: 115 additions & 36 deletions crates/rustc_codegen_spirv/src/linker/inline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! run mem2reg (see mem2reg.rs) on the result to "unwrap" the Function pointer.

use super::apply_rewrite_rules;
use super::ipo::CallGraph;
use super::simple_passes::outgoing_edges;
use super::{get_name, get_names};
use crate::custom_insts::{self, CustomInst, CustomOp};
Expand All @@ -30,6 +31,64 @@ pub fn inline(sess: &Session, module: &mut Module) -> super::Result<()> {
// This algorithm gets real sad if there's recursion - but, good news, SPIR-V bans recursion
deny_recursion_in_module(sess, module)?;

let custom_ext_inst_set_import = module
.ext_inst_imports
.iter()
.find(|inst| {
assert_eq!(inst.class.opcode, Op::ExtInstImport);
inst.operands[0].unwrap_literal_string() == &custom_insts::CUSTOM_EXT_INST_SET[..]
})
.map(|inst| inst.result_id.unwrap());

// HACK(eddyb) compute the set of functions that may `Abort` *transitively*,
// which is only needed because of how we inline (sometimes it's outside-in,
// aka top-down, instead of always being inside-out, aka bottom-up).
//
// (inlining is needed in the first place because our custom `Abort`
// instructions get lowered to a simple `OpReturn` in entry-points, but
// that requires that they get inlined all the way up to the entry-points)
let functions_that_may_abort = custom_ext_inst_set_import
.map(|custom_ext_inst_set_import| {
let mut may_abort_by_id = FxHashSet::default();

// FIXME(eddyb) use this `CallGraph` abstraction more during inlining.
let call_graph = CallGraph::collect(module);
for func_idx in call_graph.post_order() {
let func_id = module.functions[func_idx].def_id().unwrap();

let any_callee_may_abort = call_graph.callees[func_idx].iter().any(|&callee_idx| {
may_abort_by_id.contains(&module.functions[callee_idx].def_id().unwrap())
});
if any_callee_may_abort {
may_abort_by_id.insert(func_id);
continue;
}

let may_abort_directly = module.functions[func_idx].blocks.iter().any(|block| {
match &block.instructions[..] {
[.., last_normal_inst, terminator_inst]
if last_normal_inst.class.opcode == Op::ExtInst
&& last_normal_inst.operands[0].unwrap_id_ref()
== custom_ext_inst_set_import
&& CustomOp::decode_from_ext_inst(last_normal_inst)
== CustomOp::Abort =>
{
assert_eq!(terminator_inst.class.opcode, Op::Unreachable);
true
}

_ => false,
}
});
if may_abort_directly {
may_abort_by_id.insert(func_id);
}
}

may_abort_by_id
})
.unwrap_or_default();

let functions = module
.functions
.iter()
Expand All @@ -42,7 +101,7 @@ pub fn inline(sess: &Session, module: &mut Module) -> super::Result<()> {
let mut dropped_ids = FxHashSet::default();
let mut inlined_to_legalize_dont_inlines = Vec::new();
module.functions.retain(|f| {
let should_inline_f = should_inline(&legal_globals, f, None);
let should_inline_f = should_inline(&legal_globals, &functions_that_may_abort, f, None);
if should_inline_f != Ok(false) {
if should_inline_f == Err(MustInlineToLegalize) && has_dont_inline(f) {
inlined_to_legalize_dont_inlines.push(f.def_id().unwrap());
Expand Down Expand Up @@ -82,27 +141,19 @@ pub fn inline(sess: &Session, module: &mut Module) -> super::Result<()> {
id
}),

custom_ext_inst_set_import: module
.ext_inst_imports
.iter()
.find(|inst| {
assert_eq!(inst.class.opcode, Op::ExtInstImport);
inst.operands[0].unwrap_literal_string() == &custom_insts::CUSTOM_EXT_INST_SET[..]
})
.map(|inst| inst.result_id.unwrap())
.unwrap_or_else(|| {
let id = next_id(header);
let inst = Instruction::new(
Op::ExtInstImport,
None,
Some(id),
vec![Operand::LiteralString(
custom_insts::CUSTOM_EXT_INST_SET.to_string(),
)],
);
module.ext_inst_imports.push(inst);
id
}),
custom_ext_inst_set_import: custom_ext_inst_set_import.unwrap_or_else(|| {
let id = next_id(header);
let inst = Instruction::new(
Op::ExtInstImport,
None,
Some(id),
vec![Operand::LiteralString(
custom_insts::CUSTOM_EXT_INST_SET.to_string(),
)],
);
module.ext_inst_imports.push(inst);
id
}),

id_to_name: module
.debug_names
Expand All @@ -125,6 +176,7 @@ pub fn inline(sess: &Session, module: &mut Module) -> super::Result<()> {

functions: &functions,
legal_globals: &legal_globals,
functions_that_may_abort: &functions_that_may_abort,
};
for function in &mut module.functions {
inliner.inline_fn(function);
Expand Down Expand Up @@ -329,12 +381,20 @@ struct MustInlineToLegalize;
/// and inlining is *mandatory* due to an illegal signature/arguments.
fn should_inline(
legal_globals: &FxHashMap<Word, LegalGlobal>,
functions_that_may_abort: &FxHashSet<Word>,
callee: &Function,
call_site: Option<CallSite<'_>>,
) -> Result<bool, MustInlineToLegalize> {
let callee_def = callee.def.as_ref().unwrap();
let callee_control = callee_def.operands[0].unwrap_function_control();

// HACK(eddyb) this "has a call-site" check ensures entry-points don't get
// accidentally removed as "must inline to legalize" function, but can still
// be inlined into other entry-points (if such an unusual situation arises).
if call_site.is_some() && functions_that_may_abort.contains(&callee.def_id().unwrap()) {
return Err(MustInlineToLegalize);
}

let ret_ty = legal_globals
.get(&callee_def.result_type.unwrap())
.ok_or(MustInlineToLegalize)?;
Expand Down Expand Up @@ -428,6 +488,7 @@ struct Inliner<'m, 'map> {

functions: &'map FunctionMap,
legal_globals: &'map FxHashMap<Word, LegalGlobal>,
functions_that_may_abort: &'map FxHashSet<Word>,
// rewrite_rules: FxHashMap<Word, Word>,
}

Expand Down Expand Up @@ -509,7 +570,12 @@ impl Inliner<'_, '_> {
caller,
call_inst: inst,
};
match should_inline(self.legal_globals, f, Some(call_site)) {
match should_inline(
self.legal_globals,
self.functions_that_may_abort,
f,
Some(call_site),
) {
Ok(inline) => inline,
Err(MustInlineToLegalize) => true,
}
Expand Down Expand Up @@ -655,16 +721,9 @@ impl Inliner<'_, '_> {
insts.retain_mut(|inst| {
let is_debuginfo = match inst.class.opcode {
Op::Line | Op::NoLine => true,
Op::ExtInst
if inst.operands[0].unwrap_id_ref()
== self.custom_ext_inst_set_import =>
{
match CustomOp::decode_from_ext_inst(inst) {
CustomOp::SetDebugSrcLoc
| CustomOp::ClearDebugSrcLoc
| CustomOp::PushInlinedCallFrame
| CustomOp::PopInlinedCallFrame => true,
}
Op::ExtInst => {
inst.operands[0].unwrap_id_ref() == self.custom_ext_inst_set_import
&& CustomOp::decode_from_ext_inst(inst).is_debuginfo()
}
_ => false,
};
Expand Down Expand Up @@ -737,6 +796,12 @@ impl Inliner<'_, '_> {
return_variable: Option<Word>,
return_jump: Word,
) -> Vec<Block> {
let Self {
custom_ext_inst_set_import,
op_type_void_id,
..
} = *self;

// Prepare the debuginfo insts to prepend/append to every block.
// FIXME(eddyb) this could be more efficient if we only used one pair of
// `{Push,Pop}InlinedCallFrame` for the whole inlined callee, but there
Expand Down Expand Up @@ -774,10 +839,10 @@ impl Inliner<'_, '_> {
let mut custom_inst_to_inst = |inst: CustomInst<_>| {
Instruction::new(
Op::ExtInst,
Some(self.op_type_void_id),
Some(op_type_void_id),
Some(self.id()),
[
Operand::IdRef(self.custom_ext_inst_set_import),
Operand::IdRef(custom_ext_inst_set_import),
Operand::LiteralExtInstInteger(inst.op() as u32),
]
.into_iter()
Expand Down Expand Up @@ -837,7 +902,21 @@ impl Inliner<'_, '_> {
// Insert the suffix debuginfo instructions before the terminator,
// which sadly can't be covered by them.
{
let i = block.instructions.len() - 1;
let last_non_terminator = block.instructions.iter().rposition(|inst| {
let is_standard_terminator =
rspirv::grammar::reflect::is_block_terminator(inst.class.opcode);
let is_custom_terminator = match inst.class.opcode {
Op::ExtInst
if inst.operands[0].unwrap_id_ref()
== custom_ext_inst_set_import =>
{
CustomOp::decode_from_ext_inst(inst).is_terminator()
}
_ => false,
};
!(is_standard_terminator || is_custom_terminator)
});
let i = last_non_terminator.map_or(0, |x| x + 1);
i..i
},
debuginfo_suffix,
Expand Down
11 changes: 10 additions & 1 deletion crates/rustc_codegen_spirv/src/linker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,12 @@ pub fn link(
};
after_pass("lower_from_spv", &module);

// NOTE(eddyb) this *must* run on unstructured CFGs, to do its job.
{
let _timer = sess.timer("spirt_passes::controlflow::convert_custom_aborts_to_unstructured_returns_in_entry_points");
spirt_passes::controlflow::convert_custom_aborts_to_unstructured_returns_in_entry_points(&mut module);
}

if opts.structurize {
{
let _timer = sess.timer("spirt::legalize::structurize_func_cfgs");
Expand Down Expand Up @@ -473,7 +479,10 @@ pub fn link(
report_diagnostics_result?;

// Replace our custom debuginfo instructions just before lifting to SPIR-V.
spirt_passes::debuginfo::convert_custom_debuginfo_to_spv(&mut module);
{
let _timer = sess.timer("spirt_passes::debuginfo::convert_custom_debuginfo_to_spv");
spirt_passes::debuginfo::convert_custom_debuginfo_to_spv(&mut module);
}

let spv_words = {
let _timer = sess.timer("spirt::Module::lift_to_spv_module_emitter");
Expand Down
Loading

0 comments on commit ce8c3f8

Please sign in to comment.