naga/back/msl/hoist.rs
1/*!
2Finding temporaries that must be declared outside a loop.
3
4Naga IR's [`Loop`] statement runs its `continuing` block after its `body`, but
5MSL has no equivalent construct, so [`Writer::put_block`] emits the `continuing`
6block at the *top* of a `while` loop, skipped on the first iteration:
7
8```ignore
9bool loop_init = true;
10while(true) {
11 if (!loop_init) {
12 <continuing>
13 if (<break_if>) { break; }
14 }
15 loop_init = false;
16 <body>
17}
18```
19
20Expressions emitted in `body` are in scope in `continuing`, but the temporaries
21holding their values are declared *below* the generated `continuing` code, so
22they can't be named there. Re-evaluating such an expression in place would use
23the state at the end of the loop iteration, which may have changed from the
24state at the original position.
25
26So when a loop has a `continuing` block, we declare the temporaries the
27`continuing` block needs ahead of the `while` loop, and have the body merely
28assign to them.
29
30[`Loop`]: crate::Statement::Loop
31[`Writer::put_block`]: super::writer::Writer::put_block
32*/
33
34use core::fmt::Write;
35
36use super::{writer::StatementContext, BackendResult, Writer};
37use crate::{
38 arena::{Arena, Handle, HandleSet},
39 back,
40 compact::ModuleTracer,
41 proc::TypeResolution,
42};
43
44impl<W: Write> Writer<W> {
45 /// Declare the temporaries that a loop's `continuing` block needs ahead of
46 /// the loop.
47 ///
48 /// Record them in [`Self::hoisted_loop_expressions`], so that `continuing` can
49 /// name them and the body assigns to them rather than declaring them. Each
50 /// entry is moved to [`Self::named_expressions`] when the body writes the
51 /// expression's definition, which it always does, since every candidate
52 /// comes from a statement in `body`.
53 ///
54 /// See [the module documentation](self) for why this is needed.
55 pub(super) fn hoist_continuing_dependencies(
56 &mut self,
57 level: back::Level,
58 body: &crate::Block,
59 continuing: &crate::Block,
60 break_if: Option<Handle<crate::Expression>>,
61 context: &StatementContext,
62 ) -> BackendResult {
63 let context = &context.expression;
64 let expressions = &context.function.expressions;
65
66 // The candidates are the definitions that get a temporary at all.
67 //
68 // Skipping the rest is deliberate: `continuing` re-evaluates those in
69 // place, and that is sound, because every expression whose value
70 // depends on *when* it is evaluated already gets a temporary. Loads,
71 // samples and derivatives have a `bake_ref_count` of one, so any use at
72 // all bakes them, and statement results have no inline form. What's
73 // left to re-evaluate is arithmetic and address computation.
74
75 let mut candidates = HandleSet::for_arena(expressions);
76 for handle in definitions(expressions, body).iter() {
77 if self.bakes_expression(handle, context) {
78 assert!(fits_in_temporary(context.module, context.info, handle));
79 candidates.insert(handle);
80 }
81 }
82 if candidates.is_empty() {
83 return Ok(());
84 }
85
86 let used = expressions_used(
87 context.module,
88 context.function,
89 continuing,
90 break_if,
91 &candidates,
92 );
93
94 for handle in candidates.iter() {
95 if !used.contains(handle) {
96 continue;
97 }
98 let Some(name) = self.baked_name(handle, context) else {
99 continue;
100 };
101
102 write!(self.out, "{level}")?;
103 self.put_baked_type(handle, context)?;
104 writeln!(self.out, " {name} = {{}};")?;
105
106 self.hoisted_loop_expressions.insert(handle, name);
107 }
108
109 Ok(())
110 }
111}
112
113/// Return the expressions that `block` itself computes values for, in
114/// increasing handle order.
115fn definitions(
116 expressions: &Arena<crate::Expression>,
117 block: &crate::Block,
118) -> HandleSet<crate::Expression> {
119 use crate::Statement as St;
120
121 let mut defined = HandleSet::for_arena(expressions);
122 for statement in block.iter() {
123 match *statement {
124 St::Emit(ref range) => defined.insert_iter(range.clone()),
125 St::Atomic {
126 result: Some(result),
127 ..
128 }
129 | St::Call {
130 result: Some(result),
131 ..
132 }
133 | St::RayQuery {
134 fun: crate::RayQueryFunction::Proceed { result },
135 ..
136 }
137 | St::WorkGroupUniformLoad { result, .. }
138 | St::SubgroupBallot { result, .. }
139 | St::SubgroupGather { result, .. }
140 | St::SubgroupCollectiveOperation { result, .. } => {
141 defined.insert(result);
142 }
143
144 St::Atomic { result: None, .. }
145 | St::Call { result: None, .. }
146 | St::RayQuery {
147 fun:
148 crate::RayQueryFunction::Initialize { .. }
149 | crate::RayQueryFunction::GenerateIntersection { .. }
150 | crate::RayQueryFunction::ConfirmIntersection
151 | crate::RayQueryFunction::Terminate
152 | crate::RayQueryFunction::Begin,
153 ..
154 }
155 | St::Block(_)
156 | St::If { .. }
157 | St::Switch { .. }
158 | St::Loop { .. }
159 | St::Break
160 | St::Continue
161 | St::Return { .. }
162 | St::Kill
163 | St::ControlBarrier(_)
164 | St::MemoryBarrier(_)
165 | St::Store { .. }
166 | St::ImageStore { .. }
167 | St::ImageAtomic { .. }
168 | St::RayPipelineFunction(_)
169 | St::CooperativeStore { .. }
170 | St::DebugPrintf { .. } => {}
171 }
172 }
173 defined
174}
175
176/// Return the expressions of `function` that the code generated for `block`
177/// and `extra_roots` will refer to, directly or indirectly.
178///
179/// This is wider than what `block` needs the values of, because we do no dead
180/// code elimination: [`Writer::put_block`] writes out a definition for every
181/// expression it bakes, whether or not anything reads it, and that definition
182/// refers to the expression's operands. Hence
183/// [`trace_block_including_emits`] rather than [`trace_block`]. Miss those and
184/// a dead definition would leave its operands un-hoisted, and `continuing`
185/// would re-evaluate them in place - or, for a statement result, reach the
186/// `unreachable!` in [`Writer::put_expression`], which has no inline form to
187/// fall back on.
188///
189/// The search does not continue past an expression in `stop_at`: such an
190/// expression is marked if something refers to it, but the search does not
191/// follow it to its operands. Anything reachable only through it is therefore
192/// left out.
193///
194/// [`trace_block_including_emits`]: crate::compact::functions::FunctionTracer::trace_block_including_emits
195/// [`trace_block`]: crate::compact::functions::FunctionTracer::trace_block
196/// [`Writer::put_block`]: super::writer::Writer::put_block
197/// [`Writer::put_expression`]: super::writer::Writer::put_expression
198fn expressions_used(
199 module: &crate::Module,
200 function: &crate::Function,
201 block: &crate::Block,
202 extra_roots: impl IntoIterator<Item = Handle<crate::Expression>>,
203 stop_at: &HandleSet<crate::Expression>,
204) -> HandleSet<crate::Expression> {
205 let mut module_tracer = ModuleTracer::new(module);
206
207 let mut function_tracer = module_tracer.as_function(function);
208 function_tracer.trace_block_including_emits(block);
209 function_tracer.expressions_used.insert_iter(extra_roots);
210
211 let mut expression_tracer = function_tracer.as_expression();
212 for (handle, expression) in function.expressions.iter().rev() {
213 if expression_tracer.expressions_used.contains(handle) && !stop_at.contains(handle) {
214 expression_tracer.trace_expression(expression);
215 }
216 }
217
218 function_tracer.expressions_used
219}
220
221/// Whether `expression` is the result of a statement.
222///
223/// There is no way to write these inline, so the statement that produces one
224/// always stores it in a temporary, whether or not anything asked for one.
225pub(super) const fn is_statement_result(expression: &crate::Expression) -> bool {
226 use crate::Expression as Ex;
227 match *expression {
228 Ex::CallResult(_)
229 | Ex::AtomicResult { .. }
230 | Ex::WorkGroupUniformLoadResult { .. }
231 | Ex::SubgroupBallotResult
232 | Ex::SubgroupOperationResult { .. }
233 | Ex::RayQueryProceedResult => true,
234
235 Ex::Literal(_)
236 | Ex::Constant(_)
237 | Ex::Override(_)
238 | Ex::ZeroValue(_)
239 | Ex::Compose { .. }
240 | Ex::Access { .. }
241 | Ex::AccessIndex { .. }
242 | Ex::Splat { .. }
243 | Ex::Swizzle { .. }
244 | Ex::FunctionArgument(_)
245 | Ex::GlobalVariable(_)
246 | Ex::LocalVariable(_)
247 | Ex::Load { .. }
248 | Ex::ImageSample { .. }
249 | Ex::ImageLoad { .. }
250 | Ex::ImageQuery { .. }
251 | Ex::Unary { .. }
252 | Ex::Binary { .. }
253 | Ex::Select { .. }
254 | Ex::Derivative { .. }
255 | Ex::Relational { .. }
256 | Ex::Math { .. }
257 | Ex::As { .. }
258 | Ex::Bitcast { .. }
259 | Ex::ArrayLength(_)
260 | Ex::RayQueryVertexPositions { .. }
261 | Ex::RayQueryGetIntersection { .. }
262 | Ex::CooperativeLoad { .. }
263 | Ex::CooperativeMultiplyAdd { .. } => false,
264 }
265}
266
267/// Whether a value of `handle`'s type can be held in a default-initialized
268/// local temporary.
269fn fits_in_temporary(
270 module: &crate::Module,
271 info: &crate::valid::FunctionInfo,
272 handle: Handle<crate::Expression>,
273) -> bool {
274 use crate::TypeInner as Ti;
275 match info[handle].ty {
276 TypeResolution::Handle(ty) => matches!(
277 module.types[ty].inner,
278 Ti::Scalar(_)
279 | Ti::Vector { .. }
280 | Ti::Matrix { .. }
281 | Ti::Array {
282 size: crate::ArraySize::Constant(_),
283 ..
284 }
285 | Ti::Struct { .. }
286 | Ti::CooperativeMatrix { .. }
287 ),
288 TypeResolution::Value(
289 Ti::Scalar(_) | Ti::Vector { .. } | Ti::Matrix { .. } | Ti::CooperativeMatrix { .. },
290 ) => true,
291 TypeResolution::Value(_) => false,
292 }
293}