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 }