lib/choir/src/backends/gpu/spirv/emitter/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const emitter = @import("root.zig");
3
4 test "spirv emitter declaration coverage" {
5 std.testing.refAllDecls(emitter.codegen);
6 std.testing.refAllDecls(emitter.catalog);
7 std.testing.refAllDecls(emitter.dialect);
8 std.testing.refAllDecls(emitter.emit);
9 std.testing.refAllDecls(emitter.gpu);
10 std.testing.refAllDecls(emitter.module);
11 std.testing.refAllDecls(emitter.memory);
12 std.testing.refAllDecls(emitter.plan);
13 std.testing.refAllDecls(emitter.scalar);
14 std.testing.refAllDecls(emitter.spec);
15 std.testing.refAllDecls(emitter.stage);
16 std.testing.refAllDecls(emitter.validation);
17 std.testing.refAllDecls(emitter.ops);
18 std.testing.refAllDecls(emitter);
19 }
20
21 const abi = @import("choir_abi");
22 const choir = @import("../../../../root.zig");
23 const fixture = @import("../../fixture/root.zig");
24 const registration = @import("../../registration.zig");
25
26 fn emitFixture(source: []const u8, entry_name: []const u8, limits: abi.Limits) !emitter.plan.Emission {
27 const allocator = std.testing.allocator;
28 var ctx = try choir.ir.Context.init(allocator, choir.ir.Context.Limits.testing);
29 defer ctx.deinit(allocator);
30 try registration.prepareCompilationDialects(&ctx);
31 const module = try fixture.lower(&ctx, source, .spirv);
32 defer module.erase();
33 return emitter.plan.emitPlanWords(allocator, module, entry_name, limits, .{});
34 }
35
36 fn emitScalars(limits: abi.Limits) !emitter.plan.Emission {
37 return emitFixture(@embedFile("fixture/scalars.txt"), "test_scalars", limits);
38 }
39
40 test "spirv emitter returns the push-constant layout it emitted" {
41 const emission = try emitScalars(.{});
42 defer std.testing.allocator.free(emission.words);
43 const layout = emission.push_constants;
44 try std.testing.expectEqual(@as(u8, 3), layout.count);
45 try std.testing.expectEqual(@as(u32, 0), layout.member(0).offset);
46 try std.testing.expectEqual(@as(u32, 8), layout.member(1).offset);
47 try std.testing.expectEqual(@as(u32, 8), layout.member(1).byte_size);
48 try std.testing.expectEqual(@as(u32, 16), layout.member(2).offset);
49 try std.testing.expectEqual(@as(u8, 20), layout.byte_size);
50 }
51
52 test "spirv emitter refuses a push-constant block over the target limit" {
53 try std.testing.expectError(
54 error.CapabilityMismatch,
55 emitScalars(.{ .push_constant_bytes = 16 }),
56 );
57 }
58
59 test "spirv emitter writes subgroup group operations as literal operands" {
60 const emission = try emitFixture(@embedFile("fixture/subgroup.txt"), "test_subgroup", .{});
61 defer std.testing.allocator.free(emission.words);
62 const words = emission.words;
63 var reduce_count: u32 = 0;
64 var scan_count: u32 = 0;
65 var index: usize = 5;
66 while (index < words.len) {
67 const word_count = words[index] >> 16;
68 try std.testing.expect(word_count > 0);
69 if (@as(u16, @truncate(words[index])) == emitter.ops.SpirvOp.GroupNonUniformFAdd) {
70 try std.testing.expectEqual(@as(u32, 6), word_count);
71 switch (words[index + 4]) {
72 0 => reduce_count += 1,
73 2 => scan_count += 1,
74 else => return error.TestUnexpectedResult,
75 }
76 }
77 index += word_count;
78 }
79 try std.testing.expectEqual(@as(usize, words.len), index);
80 try std.testing.expectEqual(@as(u32, 1), reduce_count);
81 try std.testing.expectEqual(@as(u32, 1), scan_count);
82 }
83
84 const stages = fixture.stages;
85 const spec = emitter.spec;
86 const SpirvOp = emitter.ops.SpirvOp;
87
88 /// Opcode counts and entry-point facts read back from emitted words.
89 const Census = struct {
90 opcodes: [512]u32 = @splat(0),
91 models: [2]u32 = undefined,
92 entries: u32 = 0,
93 decorations: [64]u32 = @splat(0),
94 builtins: [64]u32 = @splat(0),
95 modes: [32]u32 = @splat(0),
96 /// Declared capabilities, in declaration order.
97 capabilities: [16]u32 = undefined,
98 capability_count: u32 = 0,
99
100 fn of(words: []const u32) !Census {
101 var census: Census = .{};
102 var index: usize = 5;
103 while (index < words.len) {
104 const count = words[index] >> 16;
105 const opcode: u16 = @truncate(words[index]);
106 try std.testing.expect(count > 0 and index + count <= words.len);
107 census.opcodes[opcode] += 1;
108 switch (opcode) {
109 SpirvOp.EntryPoint => {
110 census.models[census.entries] = words[index + 1];
111 census.entries += 1;
112 },
113 SpirvOp.ExecutionMode => census.modes[words[index + 2]] += 1,
114 SpirvOp.Capability => {
115 try std.testing.expect(census.capability_count < census.capabilities.len);
116 census.capabilities[census.capability_count] = words[index + 1];
117 census.capability_count += 1;
118 },
119 SpirvOp.Decorate => {
120 census.decorations[words[index + 2]] += 1;
121 if (words[index + 2] == spec.Decoration.BuiltIn) {
122 census.builtins[words[index + 3]] += 1;
123 }
124 },
125 else => {},
126 }
127 index += count;
128 }
129 try std.testing.expectEqual(words.len, index);
130 return census;
131 }
132
133 fn declares(census: *const Census, capability: u32) bool {
134 return std.mem.indexOfScalar(u32, census.capabilities[0..census.capability_count], capability) != null;
135 }
136 };
137
138 fn emitStageCase(case: stages.Case) ![]u32 {
139 const allocator = std.testing.allocator;
140 var ctx = try choir.ir.Context.init(allocator, choir.ir.Context.Limits.testing);
141 defer ctx.deinit(allocator);
142 try registration.prepareCompilationDialects(&ctx);
143 const module = try fixture.lower(&ctx, case.source, .spirv);
144 defer module.erase();
145 return stages.emitSpirv(allocator, module);
146 }
147
148 test "spirv stage local array has a Function variable before ordinary instructions" {
149 const words = try emitStageCase(stages.alloca_case);
150 defer std.testing.allocator.free(words);
151 const classes = try variableClasses(words);
152 const census = try Census.of(words);
153 try std.testing.expectEqual(@as(u32, 2), classes[spec.StorageClass.Function]);
154 try std.testing.expect(census.opcodes[SpirvOp.Store] >= 5);
155 try std.testing.expectEqual(@as(u32, 10), census.opcodes[SpirvOp.AccessChain]);
156
157 var after_label = false;
158 var found = false;
159 var index: usize = 5;
160 while (index < words.len) {
161 const count = words[index] >> 16;
162 const opcode: u16 = @truncate(words[index]);
163 if (opcode == SpirvOp.Label) {
164 after_label = true;
165 } else if (after_label and opcode == SpirvOp.Variable and words[index + 3] == spec.StorageClass.Function) {
166 found = true;
167 break;
168 } else if (after_label and opcode != SpirvOp.FunctionEnd and opcode != SpirvOp.Function) {
169 after_label = false;
170 }
171 index += count;
172 }
173 try std.testing.expect(found);
174 }
175
176 test "spirv stage refuses dynamic function-local array" {
177 const source = try std.mem.replaceOwned(
178 u8,
179 std.testing.allocator,
180 stages.alloca_case.source,
181 "%16 = memref.alloca() : !memref<4,arith.f32,local>",
182 "%16 = memref.alloca(%12) : !memref<?,arith.f32,local>",
183 );
184 defer std.testing.allocator.free(source);
185 try std.testing.expectError(error.UnsupportedStageMemory, emitStageSource(source));
186 }
187
188 test "spirv emitter writes one vertex and one fragment entry per stage module" {
189 for (stages.cases) |case| {
190 const words = try emitStageCase(case);
191 defer std.testing.allocator.free(words);
192 const census = try Census.of(words);
193 try std.testing.expectEqual(@as(u32, 2), census.entries);
194 try std.testing.expectEqual(spec.ExecutionModel.Vertex, census.models[0]);
195 try std.testing.expectEqual(spec.ExecutionModel.Fragment, census.models[1]);
196 try std.testing.expectEqual(@as(u32, 1), census.modes[spec.ExecutionMode.OriginUpperLeft]);
197 try std.testing.expectEqual(@as(u32, 0), census.modes[spec.ExecutionMode.LocalSize]);
198 try std.testing.expectEqual(@as(u32, 0), census.opcodes[SpirvOp.SpecConstant]);
199 const textured = std.mem.count(u8, case.source, "gpu.sampled_texture") > 0;
200 const sampled_images: u32 = if (textured) 1 else 0;
201 try std.testing.expectEqual(sampled_images, census.opcodes[SpirvOp.TypeSampledImage]);
202 try std.testing.expectEqual(@as(u32, 1), census.builtins[emitter.gpu.SpvBuiltIn.Position]);
203 }
204 }
205
206 test "spirv stage interfaces carry locations, builtins, flat integers and bindings" {
207 const words = try emitStageCase(stages.cases[1]);
208 defer std.testing.allocator.free(words);
209 const census = try Census.of(words);
210 const builtin = emitter.gpu.SpvBuiltIn;
211 const stage_builtins = .{
212 builtin.VertexIndex,
213 builtin.InstanceIndex,
214 builtin.FragCoord,
215 builtin.FrontFacing,
216 };
217 inline for (stage_builtins) |kind| {
218 try std.testing.expectEqual(@as(u32, 1), census.builtins[kind]);
219 }
220 try std.testing.expectEqual(@as(u32, 6), census.decorations[spec.Decoration.Location]);
221 try std.testing.expectEqual(@as(u32, 2), census.decorations[spec.Decoration.Flat]);
222 try std.testing.expectEqual(@as(u32, 1), census.decorations[spec.Decoration.Binding]);
223 try std.testing.expectEqual(@as(u32, 1), census.opcodes[SpirvOp.ImageSampleExplicitLod]);
224 inline for (.{ SpirvOp.DPdxFine, SpirvOp.DPdyFine, SpirvOp.FwidthFine }) |opcode| {
225 try std.testing.expectEqual(@as(u32, 1), census.opcodes[opcode]);
226 }
227 try std.testing.expect(census.declares(spec.Capability.DerivativeControl));
228 }
229
230 test "spirv stage modules share one texture variable per group and binding" {
231 const words = try emitStageCase(stages.cases[0]);
232 defer std.testing.allocator.free(words);
233 const census = try Census.of(words);
234 try std.testing.expectEqual(@as(u32, 1), census.decorations[spec.Decoration.DescriptorSet]);
235 try std.testing.expectEqual(@as(u32, 1), census.opcodes[SpirvOp.ImageSampleImplicitLod]);
236 try std.testing.expectEqual(@as(u32, 8), census.decorations[spec.Decoration.Location]);
237 try std.testing.expectEqual(@as(u32, 0), census.decorations[spec.Decoration.Flat]);
238 try std.testing.expect(!census.declares(spec.Capability.DerivativeControl));
239 }
240
241 /// The storage class of every OpVariable in `words`, counted.
242 fn variableClasses(words: []const u32) ![16]u32 {
243 var classes: [16]u32 = @splat(0);
244 var index: usize = 5;
245 while (index < words.len) {
246 const count = words[index] >> 16;
247 try std.testing.expect(count > 0);
248 if (@as(u16, @truncate(words[index])) == SpirvOp.Variable) classes[words[index + 3]] += 1;
249 index += count;
250 }
251 return classes;
252 }
253
254 test "spirv block reads share one push-constant and one uniform block per module" {
255 const words = try emitStageCase(stages.cases[2]);
256 defer std.testing.allocator.free(words);
257 const census = try Census.of(words);
258 const classes = try variableClasses(words);
259 try std.testing.expectEqual(@as(u32, 1), classes[spec.StorageClass.PushConstant]);
260 try std.testing.expectEqual(@as(u32, 1), classes[spec.StorageClass.Uniform]);
261 try std.testing.expectEqual(@as(u32, 2), census.decorations[spec.Decoration.Block]);
262 try std.testing.expectEqual(@as(u32, 2), census.decorations[spec.Decoration.ArrayStride]);
263 try std.testing.expectEqual(@as(u32, 1), census.decorations[spec.Decoration.DescriptorSet]);
264 try std.testing.expectEqual(@as(u32, 1), census.decorations[spec.Decoration.Binding]);
265 try std.testing.expectEqual(@as(u32, 12), census.opcodes[SpirvOp.AccessChain]);
266 try std.testing.expectEqual(@as(u32, 12), census.opcodes[SpirvOp.Bitcast]);
267 }
268
269 fn emitStageSource(source: []const u8) ![]u32 {
270 const allocator = std.testing.allocator;
271 var ctx = try choir.ir.Context.init(allocator, choir.ir.Context.Limits.testing);
272 defer ctx.deinit(allocator);
273 try registration.prepareCompilationDialects(&ctx);
274 const module = try fixture.lower(&ctx, source, .spirv);
275 defer module.erase();
276 return stages.emitSpirv(allocator, module);
277 }
278
279 /// `case`'s source with its one `needle` replaced. The caller owns it.
280 fn variant(case: stages.Case, needle: []const u8, replacement: []const u8) ![]u8 {
281 try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, case.source, needle));
282 return std.mem.replaceOwned(u8, std.testing.allocator, case.source, needle, replacement);
283 }
284
285 test "spirv stage calls emit each reachable helper once and refuse recursion" {
286 const calls = stages.calls_case;
287 const words = try emitStageSource(calls.source);
288 defer std.testing.allocator.free(words);
289 const census = try Census.of(words);
290 try std.testing.expectEqual(@as(u32, 4), census.opcodes[SpirvOp.Function]);
291 try std.testing.expectEqual(@as(u32, 4), census.opcodes[SpirvOp.FunctionCall]);
292 const recursive = try variant(calls, "%9 = func.call(%8) {callee = @invert}", "%9 = func.call(%8) {callee = @nested_invert}");
293 defer std.testing.allocator.free(recursive);
294 try std.testing.expectError(error.UnsupportedOperation, emitStageSource(recursive));
295 }
296
297 test "gpu dialect refuses a derivative inside a non-stage helper" {
298 const source = try variant(stages.calls_case, "%9 = func.call(%8) {callee = @invert}", "%9 = gpu.dpdx(%8)");
299 defer std.testing.allocator.free(source);
300 var ctx = try choir.ir.Context.init(std.testing.allocator, choir.ir.Context.Limits.testing);
301 defer ctx.deinit(std.testing.allocator);
302 try registration.prepareCompilationDialects(&ctx);
303 const module = try choir.ir.parse.operation(&ctx, source);
304 defer module.erase();
305 try std.testing.expectError(error.DerivativeOutsideFragmentStage, choir.ir.verifyOperation(module, .{}));
306 }
307
308 test "spirv stage emission rejects ops outside their stage and split interface slots" {
309 const indexed = stages.cases[1];
310 const textured = stages.cases[0];
311 const implicit_in_vertex = try variant(
312 indexed,
313 "gpu.sample_lod(%4, %2, %3, %5)",
314 "gpu.sample(%4, %2, %3)",
315 );
316 defer std.testing.allocator.free(implicit_in_vertex);
317 try std.testing.expectError(error.UnsupportedOperation, emitStageSource(implicit_in_vertex));
318 const kernel = try variant(
319 textured,
320 "stage = #attr<gpu.stage>(\"vertex\")",
321 "kernel = #attr<func.kernel>",
322 );
323 defer std.testing.allocator.free(kernel);
324 try std.testing.expectError(error.UnsupportedOperation, emitStageSource(kernel));
325 const split_slot = try variant(
326 textured,
327 "%12, %13, %14, %15 = gpu.stage_input() {location = 1:i64}",
328 "%12, %13, %14, %15 = gpu.stage_input() {location = 0:i64}",
329 );
330 defer std.testing.allocator.free(split_slot);
331 try std.testing.expectError(error.UnsupportedType, emitStageSource(split_slot));
332 }
333
334 test "spirv block reads refuse members std140 cannot place" {
335 const pushed = stages.cases[2];
336 const cases = .{
337 .{ "{offset = 8:i64} : !arith.f32", "{offset = 10:i64} : !arith.f32" },
338 .{
339 "%7, %8 = gpu.push_constant() {offset = 0:i64}",
340 "%7, %8 = gpu.push_constant() {offset = 4:i64}",
341 },
342 .{
343 "{binding = 1:i64, group = 0:i64, offset = 16:i64}",
344 "{binding = 1:i64, group = 0:i64, offset = 20:i64}",
345 },
346 .{
347 "%7, %8 = gpu.push_constant() {offset = 0:i64}",
348 "%7, %8 = gpu.push_constant() {offset = 128:i64}",
349 },
350 };
351 inline for (cases) |case| {
352 const source = try variant(pushed, case[0], case[1]);
353 defer std.testing.allocator.free(source);
354 try std.testing.expectError(error.UnsupportedOperation, emitStageSource(source));
355 }
356 }
357
358 test "spirv dialect modules write vertex and fragment entry points" {
359 const allocator = std.testing.allocator;
360 var ctx = try choir.ir.Context.init(allocator, choir.ir.Context.Limits.testing);
361 defer ctx.deinit(allocator);
362 const module = try stages.buildDialectModule(&ctx);
363 defer module.erase();
364 const words = try stages.emitSpirv(allocator, module);
365 defer allocator.free(words);
366 const census = try Census.of(words);
367 try std.testing.expectEqual(@as(u32, 2), census.entries);
368 try std.testing.expectEqual(spec.ExecutionModel.Vertex, census.models[0]);
369 try std.testing.expectEqual(spec.ExecutionModel.Fragment, census.models[1]);
370 try std.testing.expectEqual(@as(u32, 1), census.modes[spec.ExecutionMode.OriginUpperLeft]);
371 try std.testing.expectEqual(@as(u32, 0), census.modes[spec.ExecutionMode.LocalSize]);
372 try std.testing.expectEqual(@as(u32, 1), census.opcodes[SpirvOp.TypeFunction]);
373 }