authorgravatar for alichraghi@proton.meAli Chraghi <alichraghi@proton.me> 2026-07-08 16:44:29+03:30
committergravatar for alichraghi@proton.meAli Chraghi <alichraghi@proton.me> 2026-07-10 11:39:54+03:30
logc498b66434412616a97328a7f7a05014aa5183dc
tree5bd059bc5bb8bf9c3629e7706d56343571418011
parent332c73ccd2246af417afaa42597055c9ba6255c1

spirv: spec constants

add `std.spirv.specConst()` and support lowering literal values as string in inline assembly

4 files changed, 262 insertions(+), 48 deletions(-)

lib/std/spirv.zig+30
......@@ -82,3 +82,33 @@ pub fn workgroupBarrier() void {
8282 .{ .acquire_release = true, .workgroup_memory = true },
8383 );
8484}
85
86pub fn specConst(T: type, comptime default_value: T, comptime spec_id: u32) T {
87 switch (@typeInfo(T)) {
88 .bool => {
89 const op = if (default_value) "OpSpecConstantTrue" else "OpSpecConstantFalse";
90 return asm ("%ret = " ++ op ++ " %ty\n" ++
91 "OpDecorate %ret SpecId $spec_id"
92 : [ret] "" (-> T),
93 : [ty] "t" (T),
94 [spec_id] "c" (spec_id),
95 );
96 },
97 .int, .float => return asm (
98 \\%ret = OpSpecConstant %ty $default_value
99 \\OpDecorate %ret SpecId $spec_id"
100 : [ret] "" (-> T),
101 : [ty] "t" (T),
102 [default_value] "c" (default_value),
103 [spec_id] "c" (spec_id),
104 ),
105 .vector => return asm (
106 \\%ret = OpSpecConstantComposite %ty %default_value %spec_id
107 : [ret] "" (-> T),
108 : [ty] "t" (T),
109 [default_value] "c" (default_value),
110 [spec_id] "c" (spec_id),
111 ),
112 else => @compileError("unsupported spec constant type"),
113 }
114}
src/codegen/spirv/Assembler.zig+153-7
......@@ -58,6 +58,10 @@ const Operand = union(enum) {
5858pub fn deinit(ass: *Assembler) void {
5959 const gpa = ass.cg.gpa;
6060 for (ass.errors.items) |err| gpa.free(err.msg);
61 for (ass.value_map.values()) |v| switch (v) {
62 .constant_composite => |cc| gpa.free(cc.values),
63 else => {},
64 };
6165 ass.tokens.deinit(gpa);
6266 ass.errors.deinit(gpa);
6367 ass.inst.operands.deinit(gpa);
......@@ -132,8 +136,18 @@ const AsmValue = union(enum) {
132136 value: Id,
133137 /// A type registered into the module's type system.
134138 ty: Id,
135 /// A pre-supplied constant integer value.
136 constant: u32,
139 /// A pre-supplied constant value, holding the raw bit pattern of the input.
140 /// For integers the value is sign-extended (for signed) or zero-extended
141 /// (for unsigned) to 64 bits. For floats, the value is the bit pattern
142 /// zero-extended from the float's width to 64 bits.
143 constant: u64,
144 /// A vector "c" input expanded by `processSpecConstVector`.
145 constant_composite: struct {
146 child: Id,
147 child_kind: std.lang.TypeId,
148 child_bit_width: u16,
149 values: []u64,
150 },
137151 string: []const u8,
138152
139153 /// Retrieve the result-id of this AsmValue. Asserts that this AsmValue
......@@ -145,6 +159,7 @@ const AsmValue = union(enum) {
145159 .unresolved_forward_reference,
146160 // TODO: Lower this value as constant?
147161 .constant,
162 .constant_composite,
148163 .string,
149164 => unreachable,
150165 .value => |result| result,
......@@ -178,6 +193,12 @@ fn processInstruction(ass: *Assembler) !void {
178193 };
179194 break :blk .{ .value = try cg.importInstructionSet(set_tag) };
180195 },
196 .OpSpecConstantComposite => blk: {
197 if (try ass.processSpecConstVector()) |result| {
198 break :blk result;
199 }
200 break :blk (try ass.processGenericInstruction()) orelse return;
201 },
181202 else => switch (ass.inst.opcode.class()) {
182203 .type_declaration => try ass.processTypeInstruction(),
183204 else => (try ass.processGenericInstruction()) orelse return,
......@@ -398,6 +419,87 @@ fn processGenericInstruction(ass: *Assembler) !?AsmValue {
398419 return null;
399420}
400421
422/// Handles `%ret = OpSpecConstantComposite %ty %vec %spec_id` where `%vec` is a
423/// vector `"c"` input and `%spec_id` is a base SpecId `"c"` input.
424/// returns null to fall back to normal processing.
425fn processSpecConstVector(ass: *Assembler) !?AsmValue {
426 if (ass.inst.operands.items.len != 4) return null;
427 const vec_ref = switch (ass.inst.operands.items[2]) {
428 .ref_id => |i| i,
429 else => return null,
430 };
431 const sid_ref = switch (ass.inst.operands.items[3]) {
432 .ref_id => |i| i,
433 else => return null,
434 };
435 const cc = switch (try ass.resolveRef(vec_ref)) {
436 .constant_composite => |cc| cc,
437 else => return null,
438 };
439 const spec_id_base = switch (try ass.resolveRef(sid_ref)) {
440 .constant => |v| v,
441 else => return null,
442 };
443
444 const cg = ass.cg;
445 const gpa = cg.gpa;
446 const ty_ref = switch (ass.inst.operands.items[0]) {
447 .ref_id => |i| i,
448 else => return ass.fail(0, "missing result type", .{}),
449 };
450 const composite_ty_id = switch (try ass.resolveRef(ty_ref)) {
451 .ty => |id| id,
452 else => return ass.fail(0, "%ty must be a type", .{}),
453 };
454
455 const globals = &cg.sections.globals;
456 const annotations = &cg.sections.annotations;
457 const literal_words: usize = if (cc.child_bit_width <= @bitSizeOf(Word)) 1 else 2;
458
459 const elem_ids = try gpa.alloc(Id, cc.values.len);
460 defer gpa.free(elem_ids);
461 for (cc.values, elem_ids, 0..) |value, *elem_id_out, i| {
462 const elem_id = cg.allocId();
463 elem_id_out.* = elem_id;
464
465 switch (cc.child_kind) {
466 .bool => {
467 const opcode: Opcode = if (value & 1 != 0) .OpSpecConstantTrue else .OpSpecConstantFalse;
468 try globals.emitRaw(gpa, opcode, 2);
469 globals.writeOperand(Id, cc.child);
470 globals.writeOperand(Id, elem_id);
471 },
472 .int, .float => {
473 try globals.emitRaw(gpa, .OpSpecConstant, 2 + literal_words);
474 globals.writeOperand(Id, cc.child);
475 globals.writeOperand(Id, elem_id);
476 if (literal_words == 1) {
477 globals.writeWord(@truncate(value));
478 } else {
479 globals.writeDoubleWord(value);
480 }
481 },
482 else => unreachable,
483 }
484
485 const spec_id_word = std.math.cast(u32, spec_id_base + i) orelse {
486 return ass.fail(0, "SpecId {} does not fit in 32 bits", .{spec_id_base + i});
487 };
488 try annotations.emitRaw(gpa, .OpDecorate, 3);
489 annotations.writeOperand(Id, elem_id);
490 annotations.writeWord(@intFromEnum(spec.Decoration.spec_id));
491 annotations.writeWord(spec_id_word);
492 }
493
494 const result_id = cg.allocId();
495 try globals.emitRaw(gpa, .OpSpecConstantComposite, 2 + cc.values.len);
496 globals.writeOperand(Id, composite_ty_id);
497 globals.writeOperand(Id, result_id);
498 for (elem_ids) |id| globals.writeOperand(Id, id);
499
500 return .{ .value = result_id };
501}
502
401503fn resolveMaybeForwardRef(ass: *Assembler, ref: AsmValue.Ref) !AsmValue {
402504 const value = ass.value_map.values()[ref];
403505 switch (value) {
......@@ -579,7 +681,14 @@ fn parseValueEnum(ass: *Assembler, kind: spec.OperandKind) !void {
579681 return ass.fail(tok.start, "invalid placeholder '${s}'", .{name});
580682 };
581683 switch (value) {
582 .constant => |literal32| {
684 .constant => |literal| {
685 const literal32 = std.math.cast(u32, literal) orelse {
686 return ass.fail(
687 tok.start,
688 "placeholder value {} does not fit in 32 bits",
689 .{literal},
690 );
691 };
583692 try ass.inst.operands.append(gpa, .{ .value = literal32 });
584693 },
585694 .string => |str| {
......@@ -646,7 +755,14 @@ fn parseLiteralInteger(ass: *Assembler) !void {
646755 return ass.fail(tok.start, "invalid placeholder '${s}'", .{name});
647756 };
648757 switch (value) {
649 .constant => |literal32| {
758 .constant => |literal| {
759 const literal32 = std.math.cast(u32, literal) orelse {
760 return ass.fail(
761 tok.start,
762 "placeholder value {} does not fit in 32 bits",
763 .{literal},
764 );
765 };
650766 try ass.inst.operands.append(gpa, .{ .literal32 = literal32 });
651767 },
652768 else => {
......@@ -679,7 +795,14 @@ fn parseLiteralExtInstInteger(ass: *Assembler) !void {
679795 return ass.fail(tok.start, "invalid placeholder '${s}'", .{name});
680796 };
681797 switch (value) {
682 .constant => |literal32| {
798 .constant => |literal| {
799 const literal32 = std.math.cast(u32, literal) orelse {
800 return ass.fail(
801 tok.start,
802 "placeholder value {} does not fit in 32 bits",
803 .{literal},
804 );
805 };
683806 try ass.inst.operands.append(gpa, .{ .literal32 = literal32 });
684807 },
685808 else => {
......@@ -767,8 +890,12 @@ fn parseContextDependentInt(ass: *Assembler, signedness: std.lang.Signedness, wi
767890 return ass.fail(tok.start, "invalid placeholder '${s}'", .{name});
768891 };
769892 switch (value) {
770 .constant => |literal32| {
771 try ass.inst.operands.append(gpa, .{ .literal32 = literal32 });
893 .constant => |literal| {
894 if (width <= @bitSizeOf(spec.Word)) {
895 try ass.inst.operands.append(gpa, .{ .literal32 = @truncate(literal) });
896 } else {
897 try ass.inst.operands.append(gpa, .{ .literal64 = literal });
898 }
772899 },
773900 else => {
774901 return ass.fail(tok.start, "value '{s}' cannot be used as placeholder", .{name});
......@@ -815,6 +942,25 @@ fn parseContextDependentFloat(ass: *Assembler, comptime width: u16) !void {
815942 const Int = @Int(.unsigned, width);
816943
817944 const tok = ass.currentToken();
945 if (ass.eatToken(.placeholder)) {
946 const name = ass.tokenText(tok)[1..];
947 const value = ass.value_map.get(name) orelse {
948 return ass.fail(tok.start, "invalid placeholder '${s}'", .{name});
949 };
950 switch (value) {
951 .constant => |literal| {
952 if (width <= @bitSizeOf(spec.Word)) {
953 try ass.inst.operands.append(gpa, .{ .literal32 = @truncate(literal) });
954 } else {
955 try ass.inst.operands.append(gpa, .{ .literal64 = literal });
956 }
957 },
958 else => {
959 return ass.fail(tok.start, "value '{s}' cannot be used as placeholder", .{name});
960 },
961 }
962 return;
963 }
818964 try ass.expectToken(.value);
819965
820966 const text = ass.tokenText(tok);
src/codegen/spirv/CodeGen.zig+78-40
......@@ -423,7 +423,7 @@ pub fn addEntryPointDeps(
423423 cg: *CodeGen,
424424 decl_index: Decl.Index,
425425 seen: *std.bit_set.Dynamic,
426 interface: *std.array_list.Managed(Id),
426 interface: *std.ArrayList(Id),
427427) !void {
428428 const decl = cg.declPtr(decl_index);
429429 const deps = cg.decl_deps.items[decl.begin_dep..decl.end_dep];
......@@ -435,7 +435,7 @@ pub fn addEntryPointDeps(
435435 seen.set(@intFromEnum(decl_index));
436436
437437 if (decl.kind == .global) {
438 try interface.append(decl.result_id);
438 try interface.append(cg.gpa, decl.result_id);
439439 }
440440
441441 for (deps) |dep| {
......@@ -1806,11 +1806,11 @@ fn constant(cg: *CodeGen, ty: Type, val: Value, repr: Repr) Error!Id {
18061806 const struct_type = zcu.typeToStruct(ty).?;
18071807 assert(struct_type.layout != .@"packed"); // packed structs use `bitpack`
18081808
1809 var types = std.array_list.Managed(Type).init(gpa);
1810 defer types.deinit();
1809 var types: std.ArrayList(Type) = .empty;
1810 defer types.deinit(gpa);
18111811
1812 var constituents = std.array_list.Managed(Id).init(gpa);
1813 defer constituents.deinit();
1812 var constituents: std.ArrayList(Id) = .empty;
1813 defer constituents.deinit(gpa);
18141814
18151815 var it = struct_type.iterateRuntimeOrder(ip);
18161816 while (it.next()) |field_index| {
......@@ -1824,8 +1824,8 @@ fn constant(cg: *CodeGen, ty: Type, val: Value, repr: Repr) Error!Id {
18241824 const field_val = try val.fieldValue(pt, field_index);
18251825 const field_id = try cg.constant(field_ty, field_val, .indirect);
18261826
1827 try types.append(field_ty);
1828 try constituents.append(field_id);
1827 try types.append(gpa, field_ty);
1828 try constituents.append(gpa, field_id);
18291829 }
18301830
18311831 const comp_ty_id = try cg.resolveType(ty, .direct);
......@@ -2366,11 +2366,11 @@ fn resolveType(cg: *CodeGen, ty: Type, repr: Repr) Error!Id {
23662366 return try cg.resolveType(.fromInterned(struct_type.packed_backing_int_type), .direct);
23672367 }
23682368
2369 var member_types = std.array_list.Managed(Id).init(gpa);
2370 defer member_types.deinit();
2369 var member_types: std.ArrayList(Id) = .empty;
2370 defer member_types.deinit(gpa);
23712371
2372 var member_names = std.array_list.Managed([]const u8).init(gpa);
2373 defer member_names.deinit();
2372 var member_names: std.ArrayList([]const u8) = .empty;
2373 defer member_names.deinit(gpa);
23742374
23752375 var it = struct_type.iterateRuntimeOrder(ip);
23762376 while (it.next()) |field_index| {
......@@ -2378,8 +2378,8 @@ fn resolveType(cg: *CodeGen, ty: Type, repr: Repr) Error!Id {
23782378 if (!field_ty.hasRuntimeBits(zcu)) continue;
23792379
23802380 const field_name = struct_type.field_names.get(ip)[field_index];
2381 try member_types.append(try cg.resolveType(field_ty, .indirect));
2382 try member_names.append(field_name.toSlice(ip));
2381 try member_types.append(gpa, try cg.resolveType(field_ty, .indirect));
2382 try member_names.append(gpa, field_name.toSlice(ip));
23832383 }
23842384
23852385 const result_id = try cg.structType(
......@@ -8688,31 +8688,69 @@ fn airAssembly(cg: *CodeGen, inst: Air.Inst.Index) !?Id {
86888688 });
86898689
86908690 const ip = &zcu.intern_pool;
8691 switch (ip.indexToKey(val.toIntern())) {
8692 .int_type,
8693 .ptr_type,
8694 .array_type,
8695 .vector_type,
8696 .opt_type,
8697 .anyframe_type,
8698 .error_union_type,
8699 .simple_type,
8700 .struct_type,
8701 .union_type,
8702 .opaque_type,
8703 .spirv_type,
8704 .enum_type,
8705 .func_type,
8706 .error_set_type,
8707 .inferred_error_set_type,
8708 => unreachable, // types, not values
8709
8710 .undef => return cg.fail("assembly input with 'c' constraint cannot be undefined", .{}),
8711
8712 .int => try ass.value_map.put(gpa, in.name, .{ .constant = @intCast(val.toUnsignedInt(zcu)) }),
8713 .enum_literal => |str| try ass.value_map.put(gpa, in.name, .{ .string = str.toSlice(ip) }),
8714
8715 else => unreachable, // TODO
8691 const target = cg.pt.zcu.getTarget();
8692 if (ip.indexToKey(val.toIntern()) == .undef) {
8693 return cg.fail("assembly input with 'c' constraint cannot be undefined", .{});
8694 }
8695 switch (input_ty.zigTypeTag(zcu)) {
8696 .int => {
8697 const bits: u64 = switch (input_ty.intInfo(zcu).signedness) {
8698 .unsigned => val.toUnsignedInt(zcu),
8699 .signed => @bitCast(val.toSignedInt(zcu)),
8700 };
8701 try ass.value_map.put(gpa, in.name, .{ .constant = bits });
8702 },
8703 .float => {
8704 const bits: u64 = switch (input_ty.floatBits(target)) {
8705 16 => @as(u16, @bitCast(val.toFloat(f16, zcu))),
8706 32 => @as(u32, @bitCast(val.toFloat(f32, zcu))),
8707 64 => @bitCast(val.toFloat(f64, zcu)),
8708 else => return cg.fail("unsupported float width for 'c' constraint", .{}),
8709 };
8710 try ass.value_map.put(gpa, in.name, .{ .constant = bits });
8711 },
8712 .vector => {
8713 const child_ty = input_ty.childType(zcu);
8714 const child_kind = child_ty.zigTypeTag(zcu);
8715 const child_bit_width: u16 = switch (child_kind) {
8716 .bool => 0,
8717 .int => @intCast(child_ty.intInfo(zcu).bits),
8718 .float => child_ty.floatBits(target),
8719 else => return cg.fail("'c' constraint vector element must be bool, int, or float", .{}),
8720 };
8721 const vec_len: usize = @intCast(input_ty.vectorLen(zcu));
8722 const values = try gpa.alloc(u64, vec_len);
8723 errdefer gpa.free(values);
8724 for (values, 0..) |*out, i| {
8725 const elem: Value = try val.elemValue(cg.pt, i);
8726 out.* = switch (child_kind) {
8727 .bool => @intFromBool(elem.toBool()),
8728 .int => switch (child_ty.intInfo(zcu).signedness) {
8729 .unsigned => elem.toUnsignedInt(zcu),
8730 .signed => @bitCast(elem.toSignedInt(zcu)),
8731 },
8732 .float => switch (child_bit_width) {
8733 16 => @as(u16, @bitCast(elem.toFloat(f16, zcu))),
8734 32 => @as(u32, @bitCast(elem.toFloat(f32, zcu))),
8735 64 => @bitCast(elem.toFloat(f64, zcu)),
8736 else => unreachable,
8737 },
8738 else => unreachable,
8739 };
8740 }
8741 const child_ty_id = try cg.resolveType(child_ty, .direct);
8742 try ass.value_map.put(gpa, in.name, .{ .constant_composite = .{
8743 .child = child_ty_id,
8744 .child_kind = child_kind,
8745 .child_bit_width = child_bit_width,
8746 .values = values,
8747 } });
8748 },
8749 .@"enum" => switch (ip.indexToKey(val.toIntern())) {
8750 .enum_literal => |str| try ass.value_map.put(gpa, in.name, .{ .string = str.toSlice(ip) }),
8751 else => unreachable,
8752 },
8753 else => return cg.fail("unsupported type for 'c' constraint", .{}),
87168754 }
87178755 } else if (std.mem.eql(u8, in.constraint, "t")) {
87188756 // type
......@@ -8779,7 +8817,7 @@ fn airAssembly(cg: *CodeGen, inst: Air.Inst.Index) !?Id {
87798817 .just_declared, .unresolved_forward_reference => unreachable,
87808818 .ty => return cg.fail("cannot return spir-v type as value from assembly", .{}),
87818819 .value => |ref| return ref,
8782 .constant, .string => return cg.fail("cannot return constant from assembly", .{}),
8820 .constant, .constant_composite, .string => return cg.fail("cannot return constant from assembly", .{}),
87838821 }
87848822 // TODO: Multiple results
87858823 // TODO: Check that the output type from assembly is the same as the type actually expected by Zig.
src/link/SpirV/BinaryModule.zig+1-1
......@@ -303,7 +303,7 @@ pub const Parser = struct {
303303 }
304304 },
305305 .literal_context_dependent_number => {
306 assert(inst.opcode == .OpConstant or inst.opcode == .OpSpecConstantOp);
306 assert(inst.opcode == .OpConstant or inst.opcode == .OpSpecConstant);
307307 const bit_width = binary.arith_type_width.get(@enumFromInt(inst.operands[0])) orelse {
308308 log.err("invalid LiteralContextDependentNumber type {}", .{inst.operands[0]});
309309 return error.InvalidId;