From 2b12dd08f926e655e149045b462bd8f87cabf9b2 Mon Sep 17 00:00:00 2001 From: Ivan Stepanov Date: Mon, 7 Jul 2025 23:16:22 +0200 Subject: [PATCH 1/4] spirv: enhance assembler to handle constant strings and result IDs --- src/codegen/spirv.zig | 24 +++++++++++++++++- src/codegen/spirv/Assembler.zig | 20 +++++++++++++-- src/codegen/spirv/Module.zig | 43 +++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/codegen/spirv.zig b/src/codegen/spirv.zig index 292f5a62fc74..df81e6a77ef0 100644 --- a/src/codegen/spirv.zig +++ b/src/codegen/spirv.zig @@ -6439,7 +6439,8 @@ const NavGen = struct { // TODO: This entire function should be handled a bit better... const ip = &zcu.intern_pool; - switch (ip.indexToKey(val.toIntern())) { + const key = ip.indexToKey(val.toIntern()); + switch (key) { .int_type, .ptr_type, .array_type, @@ -6461,6 +6462,27 @@ const NavGen = struct { .int => try as.value_map.put(as.gpa, name, .{ .constant = @intCast(val.toUnsignedInt(zcu)) }), .enum_literal => |str| try as.value_map.put(as.gpa, name, .{ .string = str.toSlice(ip) }), + .ptr => |ptr| { + switch (ptr.base_addr) { + .uav => |uav| { + const pointee_val = uav.val; + const pointee_key = ip.indexToKey(pointee_val); + switch (pointee_key) { + .aggregate => |aggregate| { + if (Type.fromInterned(pointee_key.aggregate.ty).zigTypeTag(zcu) == .array and aggregate.storage == .bytes) { + const pointee_type = Type.fromInterned(pointee_key.aggregate.ty); + const bytes = aggregate.storage.bytes.toSlice(pointee_type.arrayLenIncludingSentinel(zcu), ip); + try as.value_map.put(as.gpa, name, .{ .string = bytes }); + } else { + return self.fail("unsupported pointee constant type for 'c' constraint (ptr.uav.aggregate case - pointee type tag: {}, storage: {})", .{ Type.fromInterned(pointee_key.aggregate.ty).zigTypeTag(zcu), aggregate.storage }); + } + }, + else => return self.fail("unsupported pointee constant type for 'c' constraint (ptr.uav case): {}", .{pointee_key}), + } + }, + else => return self.fail("unsupported pointer base address type for 'c' constraint: {}", .{ptr.base_addr}), + } + }, else => unreachable, // TODO } diff --git a/src/codegen/spirv/Assembler.zig b/src/codegen/spirv/Assembler.zig index 190c8c2e927c..2641665b01f1 100644 --- a/src/codegen/spirv/Assembler.zig +++ b/src/codegen/spirv/Assembler.zig @@ -152,6 +152,22 @@ const AsmValue = union(enum) { .ty => |result| result, }; } + + /// Retrieve the result-id of this AsmValue, creating constants as needed. + /// This is used by the assembler to convert constants and strings to result IDs. + pub fn resultIdOrConstant(self: AsmValue, spv: *SpvModule) !Id { + return switch (self) { + .just_declared, + .unresolved_forward_reference, + .constant, + => unreachable, // TODO: Create constant integer instruction + .string => |str| { + return try spv.constStringGlobal(str); + }, + .value => |result| result, + .ty => |result| result, + }; + } }; /// This map type maps results to values. Results can be addressed either by name (without the %), or by @@ -508,7 +524,7 @@ fn processGenericInstruction(self: *Assembler) !?AsmValue { .ref_id => |index| { const result = try self.resolveRef(index); try section.ensureUnusedCapacity(self.spv.gpa, 1); - section.writeOperand(spec.Id, result.resultId()); + section.writeOperand(spec.Id, try result.resultIdOrConstant(self.spv)); }, .string => |offset| { const text = std.mem.sliceTo(self.inst.string_bytes.items[offset..], 0); @@ -559,7 +575,7 @@ fn resolveRef(self: *Assembler, ref: AsmValue.Ref) !AsmValue { fn resolveRefId(self: *Assembler, ref: AsmValue.Ref) !Id { const value = try self.resolveRef(ref); - return value.resultId(); + return value.resultIdOrConstant(self.spv); } /// Attempt to parse an instruction into `self.inst`. diff --git a/src/codegen/spirv/Module.zig b/src/codegen/spirv/Module.zig index ffcff0eab444..13f08a654b89 100644 --- a/src/codegen/spirv/Module.zig +++ b/src/codegen/spirv/Module.zig @@ -171,6 +171,7 @@ cache: struct { extended_instruction_set: std.AutoHashMapUnmanaged(spec.InstructionSet, Id) = .empty, decorations: std.AutoHashMapUnmanaged(struct { Id, spec.Decoration }, void) = .empty, builtins: std.AutoHashMapUnmanaged(struct { Id, spec.BuiltIn }, Decl.Index) = .empty, + const_strings: std.StringHashMapUnmanaged(Id) = .empty, bool_const: [2]?Id = .{ null, null }, } = .{}, @@ -229,6 +230,7 @@ pub fn deinit(self: *Module) void { self.cache.extended_instruction_set.deinit(self.gpa); self.cache.decorations.deinit(self.gpa); self.cache.builtins.deinit(self.gpa); + self.cache.const_strings.deinit(self.gpa); self.decls.deinit(self.gpa); self.decl_deps.deinit(self.gpa); @@ -504,6 +506,47 @@ pub fn resolveString(self: *Module, string: []const u8) !Id { return id; } +pub fn constStringGlobal(self: *Module, str: []const u8) !Id { + if (self.cache.const_strings.get(str)) |id| return id; + + const u8_ty = try self.intType(.unsigned, 8); + const len = str.len + 1; + const len_id = try self.constant(try self.intType(.unsigned, 32), .{ .uint32 = @intCast(len) }); + const arr_ty = try self.arrayType(len_id, u8_ty); + + const char_ids = try self.gpa.alloc(Id, len); + defer self.gpa.free(char_ids); + for (str, 0..) |c, i| { + char_ids[i] = try self.constant(u8_ty, .{ .uint32 = c }); + } + char_ids[str.len] = try self.constant(u8_ty, .{ .uint32 = 0 }); + + const const_arr_id = self.allocId(); + try self.sections.types_globals_constants.emit(self.gpa, .OpConstantComposite, .{ + .id_result_type = arr_ty, + .id_result = const_arr_id, + .constituents = char_ids, + }); + + const ptr_ty = self.allocId(); + try self.sections.types_globals_constants.emit(self.gpa, .OpTypePointer, .{ + .id_result = ptr_ty, + .storage_class = .uniform_constant, + .type = arr_ty, + }); + + const var_id = self.allocId(); + try self.sections.types_globals_constants.emit(self.gpa, .OpVariable, .{ + .id_result_type = ptr_ty, + .id_result = var_id, + .storage_class = .uniform_constant, + .initializer = const_arr_id, + }); + + try self.cache.const_strings.put(self.gpa, try self.arena.allocator().dupe(u8, str), var_id); + return var_id; +} + pub fn structType(self: *Module, result_id: Id, types: []const Id, maybe_names: ?[]const []const u8) !void { try self.sections.types_globals_constants.emit(self.gpa, .OpTypeStruct, .{ .id_result = result_id, From a230da2c59bb8db651931c9e45f95e8f4935db87 Mon Sep 17 00:00:00 2001 From: Ivan Stepanov Date: Mon, 7 Jul 2025 23:38:11 +0200 Subject: [PATCH 2/4] spirv: implement OpenCL printf function --- lib/std/gpu.zig | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/lib/std/gpu.zig b/lib/std/gpu.zig index d72d298b32d2..1c45d29f9fcb 100644 --- a/lib/std/gpu.zig +++ b/lib/std/gpu.zig @@ -1,4 +1,5 @@ const std = @import("std.zig"); +const builtin = @import("builtin"); pub const position_in = @extern(*addrspace(.input) @Vector(4, f32), .{ .name = "position" }); pub const position_out = @extern(*addrspace(.output) @Vector(4, f32), .{ .name = "position" }); @@ -126,3 +127,35 @@ pub fn executionMode(comptime entry_point: anytype, comptime mode: ExecutionMode }, } } + +/// Writes formatted output to an implementation-defined stream. +/// Returns 0 on success, –1 on failure. +pub fn printf(comptime fmt: [*:0]const u8, args: anytype) u32 { + if (builtin.zig_backend == .stage2_spirv and builtin.target.os.tag == .opencl) { + comptime var expr: []const u8 = + \\%std = OpExtInstImport "OpenCL.std" + \\%u32 = OpTypeInt 32 0 + \\%ret = OpExtInst %u32 %std 184 %fmt + ; + inline for (0..args.len) |i| { + expr = expr ++ std.fmt.comptimePrint(" %arg{d}", .{i}); + } + const result = switch (args.len) { + // zig fmt: off + 0 => asm volatile (expr : [ret] "" (-> u32), : [fmt] "c" (fmt)), + 1 => asm volatile (expr : [ret] "" (-> u32), : [fmt] "c" (fmt), [arg0] "" (args[0])), + 2 => asm volatile (expr : [ret] "" (-> u32), : [fmt] "c" (fmt), [arg0] "" (args[0]), [arg1] "" (args[1])), + 3 => asm volatile (expr : [ret] "" (-> u32), : [fmt] "c" (fmt), [arg0] "" (args[0]), [arg1] "" (args[1]), [arg2] "" (args[2])), + 4 => asm volatile (expr : [ret] "" (-> u32), : [fmt] "c" (fmt), [arg0] "" (args[0]), [arg1] "" (args[1]), [arg2] "" (args[2]), [arg3] "" (args[3])), + 5 => asm volatile (expr : [ret] "" (-> u32), : [fmt] "c" (fmt), [arg0] "" (args[0]), [arg1] "" (args[1]), [arg2] "" (args[2]), [arg3] "" (args[3]), [arg4] "" (args[4])), + 6 => asm volatile (expr : [ret] "" (-> u32), : [fmt] "c" (fmt), [arg0] "" (args[0]), [arg1] "" (args[1]), [arg2] "" (args[2]), [arg3] "" (args[3]), [arg4] "" (args[4]), [arg5] "" (args[5])), + 7 => asm volatile (expr : [ret] "" (-> u32), : [fmt] "c" (fmt), [arg0] "" (args[0]), [arg1] "" (args[1]), [arg2] "" (args[2]), [arg3] "" (args[3]), [arg4] "" (args[4]), [arg5] "" (args[5]), [arg6] "" (args[6])), + 8 => asm volatile (expr : [ret] "" (-> u32), : [fmt] "c" (fmt), [arg0] "" (args[0]), [arg1] "" (args[1]), [arg2] "" (args[2]), [arg3] "" (args[3]), [arg4] "" (args[4]), [arg5] "" (args[5]), [arg6] "" (args[6]), [arg7] "" (args[7])), + 9 => asm volatile (expr : [ret] "" (-> u32), : [fmt] "c" (fmt), [arg0] "" (args[0]), [arg1] "" (args[1]), [arg2] "" (args[2]), [arg3] "" (args[3]), [arg4] "" (args[4]), [arg5] "" (args[5]), [arg6] "" (args[6]), [arg7] "" (args[7]), [arg8] "" (args[8])), + // zig fmt: on + else => @compileError("too many arguments"), + }; + return result; + } + @compileError("unsupported Zig backend or target OS"); +} From 96b6dbae4b03599867fb19dc41699b2975ef0615 Mon Sep 17 00:00:00 2001 From: Ivan Stepanov Date: Mon, 7 Jul 2025 23:00:06 +0200 Subject: [PATCH 3/4] spirv: enhance assembler to support OpTypeArray --- src/codegen/spirv/Assembler.zig | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/codegen/spirv/Assembler.zig b/src/codegen/spirv/Assembler.zig index 2641665b01f1..e5e2ab925698 100644 --- a/src/codegen/spirv/Assembler.zig +++ b/src/codegen/spirv/Assembler.zig @@ -381,10 +381,18 @@ fn processTypeInstruction(self: *Assembler) !AsmValue { const child_type = try self.resolveRefId(operands[1].ref_id); break :blk try self.spv.vectorType(operands[2].literal32, child_type); }, - .OpTypeArray => { + .OpTypeArray => blk: { // TODO: The length of an OpTypeArray is determined by a constant (which may be a spec constant), // and so some consideration must be taken when entering this in the type system. - return self.todo("process OpTypeArray", .{}); + const element_type = try self.resolveRefId(operands[1].ref_id); + const length = try self.resolveRefId(operands[2].ref_id); + const result_id = self.spv.allocId(); + try section.emit(self.spv.gpa, .OpTypeArray, .{ + .id_result = result_id, + .element_type = element_type, + .length = length, + }); + break :blk result_id; }, .OpTypeRuntimeArray => blk: { const element_type = try self.resolveRefId(operands[1].ref_id); From 199cda8c90d7fcebcc0c977f98c92817fe1d3236 Mon Sep 17 00:00:00 2001 From: Ivan Stepanov Date: Mon, 7 Jul 2025 23:38:31 +0200 Subject: [PATCH 4/4] spirv: add test for SPIR-V string constants with c constraint --- test/behavior/asm.zig | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/behavior/asm.zig b/test/behavior/asm.zig index d79fca930a8b..96985cd4a338 100644 --- a/test/behavior/asm.zig +++ b/test/behavior/asm.zig @@ -179,3 +179,13 @@ test "asm modifiers (AArch64)" { ); try expectEqual(2 * x, double); } + +test "SPIR-V string constants with c constraint" { + if (builtin.zig_backend != .stage2_spirv) return error.SkipZigTest; + if (builtin.target.os.tag != .opencl) return error.SkipZigTest; + + _ = std.gpu.printf("testing printf\n", .{}); + var a: i32 = -10; + _ = &a; + _ = std.gpu.printf("a: %d, a + 10: %d\n", .{ a, a + 10 }); +}