commit ca57b33ddd308dd98c81a49a1734c549a5fb3dbe (tree)
parent ecb627eb396b550812e709e7f4277df8c88ad062
Author: Ali Cheraghi <alichraghi@proton.me>
Date: Mon, 15 Jun 2026 02:06:47 +0330
spirv: object file linking
Diffstat:
7 files changed, 724 insertions(+), 243 deletions(-)
diff --git a/src/Compilation.zig b/src/Compilation.zig
@@ -7062,7 +7062,8 @@ pub fn hasObjectExt(filename: []const u8) bool {
return mem.endsWith(u8, filename, ".o") or
mem.endsWith(u8, filename, ".lo") or
mem.endsWith(u8, filename, ".obj") or
- mem.endsWith(u8, filename, ".rmeta");
+ mem.endsWith(u8, filename, ".rmeta") or
+ mem.endsWith(u8, filename, ".spv");
}
pub fn hasStaticLibraryExt(filename: []const u8) bool {
diff --git a/src/codegen/spirv/CodeGen.zig b/src/codegen/spirv/CodeGen.zig
@@ -384,6 +384,13 @@ pub fn genNav(cg: *CodeGen, do_codegen: bool) Error!void {
switch (decl.kind) {
.func => {
+ if (nav.resolved.?.is_extern_decl) {
+ _ = try cg.resolveType(ty, .direct);
+ try emitExternFnStub(cg, nav, decl, ty);
+ decl.end_dep = cg.module.decl_deps.items.len;
+ return;
+ }
+
const fn_info = zcu.typeToFunc(ty).?;
const return_ty_id = try cg.resolveFnReturnType(.fromInterned(fn_info.return_type));
const is_test = zcu.test_functions.contains(cg.owner_nav);
@@ -677,14 +684,21 @@ fn resolve(cg: *CodeGen, inst: Air.Inst.Ref) !Id {
if (inst.toInterned()) |val_ip_index| {
const ty = cg.typeOf(inst);
if (ty.zigTypeTag(zcu) == .@"fn") {
- const fn_nav = switch (zcu.intern_pool.indexToKey(val_ip_index)) {
+ const val_key = zcu.intern_pool.indexToKey(val_ip_index);
+ const fn_nav = switch (val_key) {
.@"extern" => |@"extern"| @"extern".owner_nav,
.func => |func| func.owner_nav,
else => unreachable,
};
const spv_decl_index = try cg.module.resolveNav(ip, fn_nav);
try cg.module.decl_deps.append(cg.module.gpa, spv_decl_index);
- return cg.module.declPtr(spv_decl_index).result_id;
+ const decl = cg.module.declPtr(spv_decl_index);
+ if (val_key == .@"extern") {
+ const nav = ip.getNav(fn_nav);
+ const nav_ty: Type = .fromInterned(nav.resolved.?.type);
+ try emitExternFnStub(cg, nav, decl, nav_ty);
+ }
+ return decl.result_id;
}
return try cg.constant(ty, .fromInterned(val_ip_index), .direct);
@@ -1434,6 +1448,51 @@ fn constantUavRef(
}
}
+/// Emit a stub OpFunction/OpFunctionEnd + Import linkage decoration for an
+/// extern function so the module is structurally valid. The stub will be
+/// replaced by the real definition at link time.
+fn emitExternFnStub(cg: *CodeGen, nav: InternPool.Nav, decl: *Module.Decl, fn_ty: Type) !void {
+ if (decl.has_extern_stub) return;
+ decl.has_extern_stub = true;
+
+ const gpa = cg.module.gpa;
+ const zcu = cg.module.zcu;
+ const ip = &zcu.intern_pool;
+ const fn_info = zcu.typeToFunc(fn_ty).?;
+ const return_ty_id = try cg.resolveFnReturnType(.fromInterned(fn_info.return_type));
+ const prototype_ty_id = try cg.resolveType(fn_ty, .direct);
+
+ var stub: Section = .{};
+ defer stub.deinit(gpa);
+ try stub.emit(gpa, .OpFunction, .{
+ .id_result_type = return_ty_id,
+ .id_result = decl.result_id,
+ .function_type = prototype_ty_id,
+ .function_control = .{},
+ });
+ for (fn_info.param_types.get(ip)) |param_ty_index| {
+ const param_ty: Type = .fromInterned(param_ty_index);
+ if (!param_ty.hasRuntimeBits(zcu)) continue;
+ const param_type_id = try cg.resolveType(param_ty, .direct);
+ try stub.emit(gpa, .OpFunctionParameter, .{
+ .id_result_type = param_type_id,
+ .id_result = cg.module.allocId(),
+ });
+ }
+ try stub.emit(gpa, .OpFunctionEnd, {});
+ try cg.module.sections.functions.append(gpa, stub);
+
+ const extern_name = nav.getExtern(ip).?.name.toSlice(ip);
+ try cg.module.sections.annotations.emit(gpa, .OpDecorate, .{
+ .target = decl.result_id,
+ .decoration = .{ .linkage_attributes = .{
+ .name = extern_name,
+ .linkage_type = .import,
+ } },
+ });
+ try cg.module.debugName(decl.result_id, extern_name);
+}
+
fn constantNavRef(cg: *CodeGen, ty: Type, nav_index: InternPool.Nav.Index) !Id {
const zcu = cg.module.zcu;
const ip = &zcu.intern_pool;
@@ -1449,7 +1508,12 @@ fn constantNavRef(cg: *CodeGen, ty: Type, nav_index: InternPool.Nav.Index) !Id {
// just generate an empty pointer. Function pointers are represented by a pointer to usize.
return try cg.module.constUndef(ty_id);
},
- .@"extern" => if (ip.isFunctionType(nav_ty.toIntern())) @panic("TODO"),
+ .@"extern" => if (ip.isFunctionType(nav_ty.toIntern())) {
+ const spv_decl_index = try cg.module.resolveNav(ip, nav_index);
+ const decl = cg.module.declPtr(spv_decl_index);
+ try emitExternFnStub(cg, nav, decl, nav_ty);
+ return decl.result_id;
+ },
else => {},
},
}
diff --git a/src/codegen/spirv/Module.zig b/src/codegen/spirv/Module.zig
@@ -130,6 +130,9 @@ pub const Decl = struct {
begin_dep: usize = 0,
/// The past-end offset of the dependencies of this decl in the `decl_deps` array.
end_dep: usize = 0,
+ /// Whether a stub OpFunction/OpFunctionEnd + Import linkage decoration has
+ /// already been emitted for this extern function decl.
+ has_extern_stub: bool = false,
};
pub const EntryPoint = struct {
diff --git a/src/link.zig b/src/link.zig
@@ -1179,7 +1179,7 @@ pub const File = struct {
if (base.tag == .lld) return;
assert(!base.post_prelink);
switch (base.tag) {
- inline .elf, .elf2, .wasm => |tag| {
+ inline .elf, .elf2, .wasm, .spirv => |tag| {
dev.check(tag.devFeature());
return @as(*tag.Type(), @fieldParentPtr("base", base)).loadInput(input);
},
diff --git a/src/link/SpirV.zig b/src/link/SpirV.zig
@@ -3,7 +3,7 @@ const Allocator = std.mem.Allocator;
const Path = std.Build.Cache.Path;
const assert = std.debug.assert;
const log = std.log.scoped(.link);
-
+const zig_version = @import("builtin").zig_version;
const Zcu = @import("../Zcu.zig");
const InternPool = @import("../InternPool.zig");
const Compilation = @import("../Compilation.zig");
@@ -13,12 +13,10 @@ const Type = @import("../Type.zig");
const codegen = @import("../codegen.zig");
const CodeGen = @import("../codegen/spirv/CodeGen.zig");
const Module = @import("../codegen/spirv/Module.zig");
-const trace = @import("../tracy.zig").trace;
const BinaryModule = @import("SpirV/BinaryModule.zig");
const lower_invocation_globals = @import("SpirV/lower_invocation_globals.zig");
const dedup_types = @import("SpirV/dedup_types.zig");
const prune_unused = @import("SpirV/prune_unused.zig");
-
const spec = @import("../codegen/spirv/spec.zig");
const Section = @import("../codegen/spirv/Section.zig");
const Id = spec.Id;
@@ -31,6 +29,7 @@ base: link.File,
fragments: std.AutoArrayHashMapUnmanaged(InternPool.Nav.Index, Mir) = .empty,
pending_navs: std.ArrayListUnmanaged(InternPool.Nav.Index) = .empty,
entry_points: std.ArrayListUnmanaged(EntryPointDecl) = .empty,
+external_objects: std.ArrayListUnmanaged(ExternalObject) = .empty,
const EntryPointDecl = struct {
nav: InternPool.Nav.Index,
@@ -38,6 +37,11 @@ const EntryPointDecl = struct {
cc: std.builtin.CallingConvention,
};
+const ExternalObject = struct {
+ instructions: []const Word,
+ id_bound: u32,
+};
+
pub fn createEmpty(
arena: Allocator,
comp: *Compilation,
@@ -100,6 +104,55 @@ pub fn deinit(linker: *Linker) void {
linker.fragments.deinit(gpa);
linker.pending_navs.deinit(gpa);
linker.entry_points.deinit(gpa);
+ for (linker.external_objects.items) |obj| {
+ gpa.free(obj.instructions);
+ }
+ linker.external_objects.deinit(gpa);
+}
+
+pub fn loadInput(linker: *Linker, input: link.Input) !void {
+ switch (input) {
+ .object => |obj| {
+ const comp = linker.base.comp;
+ const gpa = comp.gpa;
+ const io = comp.io;
+ const diags = &comp.link_diags;
+
+ const stat = obj.file.stat(io) catch |err|
+ return diags.fail("failed to stat SPIR-V object '{f}': {t}", .{ obj.path, err });
+ const file_size = std.math.cast(usize, stat.size) orelse
+ return diags.fail("SPIR-V object '{f}' is too large", .{obj.path});
+ if (file_size < 5 * @sizeOf(Word))
+ return diags.fail("SPIR-V object '{f}' is too small to contain a valid header", .{obj.path});
+ if (file_size % @sizeOf(Word) != 0)
+ return diags.fail("SPIR-V object '{f}' size is not a multiple of the word size", .{obj.path});
+
+ const word_count = file_size / @sizeOf(Word);
+ const all_words = try gpa.alloc(Word, word_count);
+ defer gpa.free(all_words);
+
+ const bytes = std.mem.sliceAsBytes(all_words);
+ const n_read = obj.file.readPositionalAll(io, bytes, 0) catch |err|
+ return diags.fail("failed to read SPIR-V object '{f}': {t}", .{ obj.path, err });
+ if (n_read != bytes.len)
+ return diags.fail("SPIR-V object '{f}': incomplete read", .{obj.path});
+
+ if (all_words[0] != spec.magic_number)
+ return diags.fail("SPIR-V object '{f}': invalid magic number", .{obj.path});
+
+ const id_bound = all_words[3];
+ const instructions = try gpa.dupe(Word, all_words[5..]);
+
+ try linker.external_objects.append(gpa, .{
+ .instructions = instructions,
+ .id_bound = id_bound,
+ });
+ },
+ else => {
+ const diags = &linker.base.comp.link_diags;
+ return diags.fail("unsupported link input for SPIR-V target", .{});
+ },
+ }
}
pub fn updateFunc(
@@ -163,8 +216,6 @@ pub fn updateExports(
const nav_ty = ip.getNav(nav_index).resolved.?.type;
if (ip.isFunctionType(nav_ty)) {
const cc = Type.fromInterned(nav_ty).fnCallingConvention(zcu);
- if (cc == .spirv_device) return;
-
for (export_indices) |export_idx| {
const exp = export_idx.ptr(zcu);
try linker.entry_points.append(gpa, .{
@@ -182,9 +233,6 @@ pub fn flush(
tid: Zcu.PerThread.Id,
prog_node: std.Progress.Node,
) link.Error!void {
- const tracy = trace(@src());
- defer tracy.end();
-
const sub_prog_node = prog_node.start("Flush Module", 0);
defer sub_prog_node.end();
@@ -193,22 +241,23 @@ pub fn flush(
const gpa = comp.gpa;
const io = comp.io;
- const zcu = comp.zcu.?;
- const active = zcu.activate(tid);
- defer active.deactivate();
- const pt = active.pt;
- for (linker.pending_navs.items) |nav| {
- if (linker.fragments.contains(nav)) continue;
-
- const mir = CodeGen.generateNav(pt, nav) catch |err| switch (err) {
- error.OutOfMemory => return error.OutOfMemory,
- error.AlreadyReported => continue,
- error.Canceled => return error.Canceled,
- };
+ if (comp.zcu) |zcu| {
+ const active = zcu.activate(tid);
+ defer active.deactivate();
+ const pt = active.pt;
+ for (linker.pending_navs.items) |nav| {
+ if (linker.fragments.contains(nav)) continue;
+
+ const mir = CodeGen.generateNav(pt, nav) catch |err| switch (err) {
+ error.OutOfMemory => return error.OutOfMemory,
+ error.AlreadyReported => continue,
+ error.Canceled => return error.Canceled,
+ };
- linker.fragments.put(gpa, nav, mir) catch return error.OutOfMemory;
+ linker.fragments.put(gpa, nav, mir) catch return error.OutOfMemory;
+ }
+ linker.pending_navs.clearRetainingCapacity();
}
- linker.pending_navs.clearRetainingCapacity();
const merged = mergeFragments(linker, gpa, arena) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
@@ -216,7 +265,19 @@ pub fn flush(
var binary = linkModule(arena, merged.words, merged.id_bound, sub_prog_node) catch |err| switch (err) {
error.OutOfMemory => |e| return e,
- else => |other| return diags.fail("error while linking: {s}", .{@errorName(other)}),
+ else => |other| {
+ // Uncomment to write the pre-link merged module for debugging
+ // const dbg_header = [_]Word{
+ // spec.magic_number,
+ // merged.version.toWord(),
+ // merged.generator_id,
+ // merged.id_bound,
+ // 0,
+ // };
+ // linker.base.file.?.writeStreamingAll(io, @ptrCast(&dbg_header)) catch {};
+ // linker.base.file.?.writeStreamingAll(io, @ptrCast(merged.words)) catch {};
+ return diags.fail("error while linking: {s}", .{@errorName(other)});
+ },
};
defer binary.deinit(arena);
@@ -246,8 +307,9 @@ fn linkModule(arena: Allocator, words: []const Word, id_bound: u32, progress: st
fn mergeFragments(linker: *Linker, gpa: Allocator, arena: Allocator) error{OutOfMemory}!MergedModule {
const comp = linker.base.comp;
- const zcu = comp.zcu.?;
- const target = zcu.getTarget();
+ const target = &comp.root_mod.resolved_target.result;
+ const maybe_ip: ?*InternPool = if (comp.zcu) |zcu| &zcu.intern_pool else null;
+ const is_obj = comp.config.output_mode == .Obj;
var next_id: Word = 1;
@@ -263,14 +325,10 @@ fn mergeFragments(linker: *Linker, gpa: Allocator, arena: Allocator) error{OutOf
for (linker.fragments.keys(), linker.fragments.values()) |nav, *mir| {
const id_offset = next_id - 1;
- frag_infos.appendAssumeCapacity(.{
- .id_offset = id_offset,
- });
-
+ frag_infos.appendAssumeCapacity(.{ .id_offset = id_offset });
if (mir.decl_result_id != .none) {
try nav_final_ids.put(gpa, nav, @enumFromInt(@intFromEnum(mir.decl_result_id) + id_offset));
}
-
next_id += mir.id_bound - 1;
}
@@ -280,7 +338,6 @@ fn mergeFragments(linker: *Linker, gpa: Allocator, arena: Allocator) error{OutOf
try nav_final_ids.put(gpa, ref.nav, @enumFromInt(@intFromEnum(ref.local_id) + frag_info.id_offset));
}
}
-
for (mir.uav_refs) |ref| {
const key = .{ ref.val, ref.storage_class };
if (!uav_final_ids.contains(key)) {
@@ -289,123 +346,304 @@ fn mergeFragments(linker: *Linker, gpa: Allocator, arena: Allocator) error{OutOf
}
}
+ // Resolve Zig extern navs against external objects.
+ var ext_id_offsets: std.ArrayListUnmanaged(Word) = .empty;
+ defer ext_id_offsets.deinit(gpa);
+ try ext_id_offsets.ensureTotalCapacity(gpa, linker.external_objects.items.len);
+
+ var unresolved_extern_count: u32 = 0;
+ var resolved_ids: std.AutoArrayHashMapUnmanaged(Id, void) = .empty;
+ defer resolved_ids.deinit(gpa);
+
+ if (maybe_ip) |ip| {
+ var extern_name_map: std.StringArrayHashMapUnmanaged(InternPool.Nav.Index) = .empty;
+ defer extern_name_map.deinit(gpa);
+
+ var nav_it = nav_final_ids.iterator();
+ while (nav_it.next()) |entry| {
+ const nav = ip.getNav(entry.key_ptr.*);
+ if (!nav.resolved.?.is_extern_decl) continue;
+ const name = if (nav.getExtern(ip)) |e| e.name.toSlice(ip) else nav.fqn.toSlice(ip);
+ try extern_name_map.put(gpa, name, entry.key_ptr.*);
+ }
+
+ for (linker.external_objects.items) |ext_obj| {
+ const id_offset = next_id - 1;
+ ext_id_offsets.appendAssumeCapacity(id_offset);
+
+ var it: BinaryModule.Instruction.Iterator = .init(ext_obj.instructions, 0);
+ while (it.next()) |inst| {
+ const ld = LinkageDecoration.parse(inst) orelse continue;
+ if (ld.linkage_type != .@"export") continue;
+ const remapped_id: Id = @enumFromInt(@intFromEnum(ld.target_id) + id_offset);
+
+ if (extern_name_map.get(ld.name)) |nav_index| {
+ log.debug("extern resolve: '{s}' -> ext_fn_id={d}", .{ ld.name, @intFromEnum(remapped_id) });
+ nav_final_ids.getPtr(nav_index).?.* = remapped_id;
+ _ = extern_name_map.swapRemove(ld.name);
+ try resolved_ids.put(gpa, remapped_id, {});
+ }
+ }
+ next_id += ext_obj.id_bound - 1;
+ }
+
+ unresolved_extern_count = @intCast(extern_name_map.count());
+ } else {
+ for (linker.external_objects.items) |ext_obj| {
+ ext_id_offsets.appendAssumeCapacity(next_id - 1);
+ next_id += ext_obj.id_bound - 1;
+ }
+ }
+
var parser = BinaryModule.Parser.init(gpa) catch return error.OutOfMemory;
defer parser.deinit();
- var ext_inst_section = Section{};
- defer ext_inst_section.deinit(gpa);
- var globals_section = Section{};
- defer globals_section.deinit(gpa);
- var functions_section = Section{};
- defer functions_section.deinit(gpa);
- var annotations_section = Section{};
- defer annotations_section.deinit(gpa);
- var debug_names_section = Section{};
- defer debug_names_section.deinit(gpa);
- var debug_strings_section = Section{};
- defer debug_strings_section.deinit(gpa);
- var execution_modes_section = Section{};
- defer execution_modes_section.deinit(gpa);
+ var sections: Sections = .{};
+ defer sections.deinit(gpa);
- for (linker.fragments.values(), frag_infos.items) |*mir, frag_info| {
+ try mergeZigFragments(linker, gpa, &parser, §ions, frag_infos.items, &nav_final_ids, &uav_final_ids, &resolved_ids, maybe_ip);
+
+ var has_linkage = false;
+ try appendExternalObjects(linker, gpa, &parser, ext_id_offsets.items, §ions, &has_linkage, linker.fragments.count() == 0, is_obj, &resolved_ids);
+
+ if (is_obj) {
+ for (linker.entry_points.items) |ep| {
+ if (ep.cc != .spirv_device) continue;
+ const final_id = nav_final_ids.get(ep.nav) orelse continue;
+ try sections.annotations.emit(gpa, .OpDecorate, .{
+ .target = final_id,
+ .decoration = .{ .linkage_attributes = .{ .name = ep.name, .linkage_type = .@"export" } },
+ });
+ has_linkage = true;
+ }
+ if (unresolved_extern_count > 0) has_linkage = true;
+ }
+
+ var capabilities_section = Section{};
+ defer capabilities_section.deinit(gpa);
+ var extensions_section = Section{};
+ defer extensions_section.deinit(gpa);
+ var memory_model_section = Section{};
+ defer memory_model_section.deinit(gpa);
+
+ try emitPreamble(
+ gpa,
+ target,
+ has_linkage,
+ &capabilities_section,
+ &extensions_section,
+ &memory_model_section,
+ );
+ try emitEntryPoints(
+ linker,
+ gpa,
+ target,
+ §ions.entry_points,
+ §ions.execution_modes,
+ &nav_final_ids,
+ &uav_final_ids,
+ &frag_infos,
+ );
+
+ const zig_packed_version = (zig_version.major << 12) | (zig_version.minor << 7) | zig_version.patch;
+ if (maybe_ip) |ip| {
+ try emitSourceInfo(gpa, ip, zig_packed_version, §ions.debug_strings);
+ }
+
+ const version: spec.Version = .{
+ .major = 1,
+ .minor = blk: {
+ if (target.cpu.has(.spirv, .v1_6)) break :blk 6;
+ if (target.cpu.has(.spirv, .v1_5)) break :blk 5;
+ if (target.cpu.has(.spirv, .v1_4)) break :blk 4;
+ if (target.cpu.has(.spirv, .v1_3)) break :blk 3;
+ if (target.cpu.has(.spirv, .v1_2)) break :blk 2;
+ if (target.cpu.has(.spirv, .v1_1)) break :blk 1;
+ break :blk 0;
+ },
+ };
+
+ const buffers = &[_][]const Word{
+ capabilities_section.toWords(),
+ extensions_section.toWords(),
+ sections.ext_inst.toWords(),
+ memory_model_section.toWords(),
+ sections.entry_points.toWords(),
+ sections.execution_modes.toWords(),
+ sections.debug_strings.toWords(),
+ sections.debug_names.toWords(),
+ sections.annotations.toWords(),
+ sections.globals.toWords(),
+ sections.functions.toWords(),
+ };
+
+ var total_size: usize = 0;
+ for (buffers) |buffer| total_size += buffer.len;
+ const result = try arena.alloc(Word, total_size);
+
+ var offset: usize = 0;
+ for (buffers) |buffer| {
+ @memcpy(result[offset..][0..buffer.len], buffer);
+ offset += buffer.len;
+ }
+
+ return .{
+ .words = result,
+ .id_bound = next_id,
+ .version = version,
+ .generator_id = (spec.zig_generator_id << 16) | zig_packed_version,
+ };
+}
+
+fn mergeZigFragments(
+ linker: *Linker,
+ gpa: Allocator,
+ parser: *BinaryModule.Parser,
+ sections: *Sections,
+ frag_infos: []const FragmentInfo,
+ nav_final_ids: *const std.AutoHashMapUnmanaged(InternPool.Nav.Index, Id),
+ uav_final_ids: *const std.AutoHashMapUnmanaged(struct { InternPool.Index, spec.StorageClass }, Id),
+ resolved_ids: *const std.AutoArrayHashMapUnmanaged(Id, void),
+ maybe_ip: ?*InternPool,
+) error{OutOfMemory}!void {
+ for (linker.fragments.values(), frag_infos) |*mir, frag_info| {
var id_remap: std.AutoHashMapUnmanaged(Id, Id) = .empty;
defer id_remap.deinit(gpa);
+ var resolved_local_ids: std.AutoArrayHashMapUnmanaged(Id, void) = .empty;
+ defer resolved_local_ids.deinit(gpa);
+
for (mir.nav_refs) |ref| {
if (nav_final_ids.get(ref.nav)) |final_id| {
try id_remap.put(gpa, ref.local_id, final_id);
+ if (maybe_ip) |ip| {
+ const nav = ip.getNav(ref.nav);
+ if (nav.resolved.?.is_extern_decl and resolved_ids.contains(final_id)) {
+ try resolved_local_ids.put(gpa, ref.local_id, {});
+ }
+ }
}
}
-
for (mir.uav_refs) |ref| {
- const key = .{ ref.val, ref.storage_class };
- if (uav_final_ids.get(key)) |final_id| {
+ if (uav_final_ids.get(.{ ref.val, ref.storage_class })) |final_id| {
try id_remap.put(gpa, ref.local_id, final_id);
}
}
- try remapAndAppend(gpa, &ext_inst_section, mir.extended_instruction_set, frag_info.id_offset, &id_remap, &parser);
- try remapAndAppend(gpa, &globals_section, mir.globals, frag_info.id_offset, &id_remap, &parser);
- try remapAndAppend(gpa, &functions_section, mir.functions, frag_info.id_offset, &id_remap, &parser);
- try remapAndAppend(gpa, &annotations_section, mir.annotations, frag_info.id_offset, &id_remap, &parser);
- try remapAndAppend(gpa, &debug_names_section, mir.debug_names, frag_info.id_offset, &id_remap, &parser);
- try remapAndAppend(gpa, &debug_strings_section, mir.debug_strings, frag_info.id_offset, &id_remap, &parser);
- try remapAndAppend(gpa, &execution_modes_section, mir.execution_modes, frag_info.id_offset, &id_remap, &parser);
+ try remapAndAppend(gpa, §ions.ext_inst, mir.extended_instruction_set, frag_info.id_offset, &id_remap, parser);
+ try remapAndAppend(gpa, §ions.globals, mir.globals, frag_info.id_offset, &id_remap, parser);
+
+ try remapFilteredInsts(gpa, §ions.functions, mir.functions, frag_info.id_offset, &id_remap, parser, &resolved_local_ids, .skip_functions);
+ try remapFilteredInsts(gpa, §ions.annotations, mir.annotations, frag_info.id_offset, &id_remap, parser, &resolved_local_ids, .skip_linkage);
+ try remapFilteredInsts(gpa, §ions.debug_names, mir.debug_names, frag_info.id_offset, &id_remap, parser, &resolved_local_ids, .skip_names);
+ try remapAndAppend(gpa, §ions.debug_strings, mir.debug_strings, frag_info.id_offset, &id_remap, parser);
+ try remapAndAppend(gpa, §ions.execution_modes, mir.execution_modes, frag_info.id_offset, &id_remap, parser);
for (mir.entry_points) |ep| {
- try linker.entry_points.append(gpa, .{
- .nav = mir.owner_nav,
- .name = ep.name,
- .cc = ep.cc,
- });
+ try linker.entry_points.append(gpa, .{ .nav = mir.owner_nav, .name = ep.name, .cc = ep.cc });
}
}
+}
- var capabilities_section = Section{};
- defer capabilities_section.deinit(gpa);
- var extensions_section = Section{};
- defer extensions_section.deinit(gpa);
- var memory_model_section = Section{};
- defer memory_model_section.deinit(gpa);
- var entry_points_section = Section{};
- defer entry_points_section.deinit(gpa);
+const FilterMode = enum { skip_functions, skip_linkage, skip_names };
- const cap_pairs = [_]struct { cap: spec.Capability, ext: ?[]const u8 }{
- .{ .cap = .int8, .ext = null },
- .{ .cap = .int16, .ext = null },
- };
- for (cap_pairs) |pair| {
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = pair.cap });
- if (pair.ext) |ext| {
- try extensions_section.emit(gpa, .OpExtension, .{ .name = ext });
+fn remapFilteredInsts(
+ gpa: Allocator,
+ dest: *Section,
+ words: []const Word,
+ id_offset: Word,
+ id_remap: *const std.AutoHashMapUnmanaged(Id, Id),
+ parser: *BinaryModule.Parser,
+ skip_ids: *const std.AutoArrayHashMapUnmanaged(Id, void),
+ mode: FilterMode,
+) error{OutOfMemory}!void {
+ if (words.len == 0) return;
+ var it: BinaryModule.Instruction.Iterator = .init(words, 0);
+ var skip_function = false;
+ while (it.next()) |inst| {
+ switch (mode) {
+ .skip_functions => {
+ if (inst.opcode == .OpFunction) {
+ skip_function = skip_ids.contains(@enumFromInt(inst.operands[1]));
+ }
+ if (skip_function) {
+ if (inst.opcode == .OpFunctionEnd) skip_function = false;
+ continue;
+ }
+ },
+ .skip_linkage => {
+ if (LinkageDecoration.parse(inst)) |ld| {
+ if (skip_ids.contains(ld.target_id)) continue;
+ }
+ },
+ .skip_names => {
+ if (inst.opcode == .OpName and inst.operands.len >= 1) {
+ if (skip_ids.contains(@enumFromInt(inst.operands[0]))) continue;
+ }
+ },
}
+ try remapAndAppendInst(gpa, dest, words, inst, id_offset, id_remap, parser);
}
+}
+
+fn emitPreamble(
+ gpa: Allocator,
+ target: *const std.Target,
+ has_linkage: bool,
+ capabilities: *Section,
+ extensions: *Section,
+ memory_model: *Section,
+) error{OutOfMemory}!void {
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .int8 });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .int16 });
switch (target.os.tag) {
.opengl => {
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .shader });
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .matrix });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .shader });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .matrix });
},
.vulkan => {
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .shader });
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .matrix });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .shader });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .matrix });
if (target.cpu.arch == .spirv64) {
- try extensions_section.emit(gpa, .OpExtension, .{ .name = "SPV_KHR_physical_storage_buffer" });
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .physical_storage_buffer_addresses });
+ try extensions.emit(gpa, .OpExtension, .{ .name = "SPV_KHR_physical_storage_buffer" });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .physical_storage_buffer_addresses });
}
},
.opencl, .amdhsa => {
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .kernel });
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .addresses });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .kernel });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .addresses });
},
else => unreachable,
}
if (target.cpu.arch == .spirv64)
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .int64 });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .int64 });
if (target.cpu.has(.spirv, .int64))
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .int64 });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .int64 });
if (target.cpu.has(.spirv, .float16)) {
- if (target.os.tag == .opencl) try extensions_section.emit(gpa, .OpExtension, .{ .name = "cl_khr_fp16" });
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .float16 });
+ if (target.os.tag == .opencl) try extensions.emit(gpa, .OpExtension, .{ .name = "cl_khr_fp16" });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .float16 });
}
if (target.cpu.has(.spirv, .float64))
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .float64 });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .float64 });
if (target.cpu.has(.spirv, .generic_pointer))
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .generic_pointer });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .generic_pointer });
if (target.cpu.has(.spirv, .vector16))
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .vector16 });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .vector16 });
if (target.cpu.has(.spirv, .storage_push_constant16)) {
- try extensions_section.emit(gpa, .OpExtension, .{ .name = "SPV_KHR_16bit_storage" });
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .storage_push_constant16 });
+ try extensions.emit(gpa, .OpExtension, .{ .name = "SPV_KHR_16bit_storage" });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .storage_push_constant16 });
}
if (target.cpu.has(.spirv, .arbitrary_precision_integers)) {
- try extensions_section.emit(gpa, .OpExtension, .{ .name = "SPV_INTEL_arbitrary_precision_integers" });
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .arbitrary_precision_integers_intel });
+ try extensions.emit(gpa, .OpExtension, .{ .name = "SPV_INTEL_arbitrary_precision_integers" });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .arbitrary_precision_integers_intel });
}
if (target.cpu.has(.spirv, .variable_pointers)) {
- try extensions_section.emit(gpa, .OpExtension, .{ .name = "SPV_KHR_variable_pointers" });
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .variable_pointers_storage_buffer });
- try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .variable_pointers });
+ try extensions.emit(gpa, .OpExtension, .{ .name = "SPV_KHR_variable_pointers" });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .variable_pointers_storage_buffer });
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .variable_pointers });
}
+ if (has_linkage)
+ try capabilities.emit(gpa, .OpCapability, .{ .capability = .linkage });
const addressing_model: spec.AddressingModel = switch (target.os.tag) {
.opengl => .logical,
@@ -414,7 +652,7 @@ fn mergeFragments(linker: *Linker, gpa: Allocator, arena: Allocator) error{OutOf
.amdhsa => .physical64,
else => unreachable,
};
- try memory_model_section.emit(gpa, .OpMemoryModel, .{
+ try memory_model.emit(gpa, .OpMemoryModel, .{
.addressing_model = addressing_model,
.memory_model = switch (target.os.tag) {
.opencl => .open_cl,
@@ -422,17 +660,26 @@ fn mergeFragments(linker: *Linker, gpa: Allocator, arena: Allocator) error{OutOf
else => unreachable,
},
});
+}
+fn emitEntryPoints(
+ linker: *Linker,
+ gpa: Allocator,
+ target: *const std.Target,
+ entry_points_section: *Section,
+ execution_modes_section: *Section,
+ nav_final_ids: *const std.AutoHashMapUnmanaged(InternPool.Nav.Index, Id),
+ uav_final_ids: *const std.AutoHashMapUnmanaged(struct { InternPool.Index, spec.StorageClass }, Id),
+ frag_infos: *const std.ArrayList(FragmentInfo),
+) error{OutOfMemory}!void {
for (linker.entry_points.items) |ep| {
const final_id = nav_final_ids.get(ep.nav) orelse continue;
var interface: std.ArrayList(Id) = .empty;
defer interface.deinit(gpa);
-
var visited: std.AutoHashMapUnmanaged(InternPool.Nav.Index, void) = .empty;
defer visited.deinit(gpa);
-
- try collectEntryPointInterface(linker, ep.nav, &interface, &visited, &nav_final_ids, &uav_final_ids, &frag_infos, gpa);
+ try collectEntryPointInterface(linker, ep.nav, &interface, &visited, nav_final_ids, uav_final_ids, frag_infos, gpa);
const exec_model: spec.ExecutionModel = switch (target.os.tag) {
.vulkan, .opengl => switch (ep.cc) {
@@ -463,11 +710,7 @@ fn mergeFragments(linker: *Linker, gpa: Allocator, arena: Allocator) error{OutOf
.spirv_kernel, .spirv_task => |kernel| {
try execution_modes_section.emit(gpa, .OpExecutionMode, .{
.entry_point = final_id,
- .mode = .{ .local_size = .{
- .x_size = kernel.x,
- .y_size = kernel.y,
- .z_size = kernel.z,
- } },
+ .mode = .{ .local_size = .{ .x_size = kernel.x, .y_size = kernel.y, .z_size = kernel.z } },
});
},
.spirv_fragment => |fragment| {
@@ -515,8 +758,9 @@ fn mergeFragments(linker: *Linker, gpa: Allocator, arena: Allocator) error{OutOf
else => {},
}
}
+}
- const ip = &zcu.intern_pool;
+fn emitSourceInfo(gpa: Allocator, ip: *InternPool, version: u32, debug_strings: *Section) error{OutOfMemory}!void {
var error_info: std.Io.Writer.Allocating = .init(gpa);
defer error_info.deinit();
error_info.writer.writeAll("zig_errors:") catch return error.OutOfMemory;
@@ -535,66 +779,8 @@ fn mergeFragments(linker: *Linker, gpa: Allocator, arena: Allocator) error{OutOf
}.isValidChar,
) catch return error.OutOfMemory;
}
- try debug_strings_section.emit(gpa, .OpSourceExtension, .{
- .extension = error_info.written(),
- });
-
- const zig_version = @import("builtin").zig_version;
- const zig_spirv_compiler_version = comptime (zig_version.major << 12) | (zig_version.minor << 7) | zig_version.patch;
- try debug_strings_section.emit(gpa, .OpSource, .{
- .source_language = .zig,
- .version = zig_spirv_compiler_version,
- .file = null,
- .source = null,
- });
-
- const version: spec.Version = .{
- .major = 1,
- .minor = blk: {
- if (target.cpu.has(.spirv, .v1_6)) break :blk 6;
- if (target.cpu.has(.spirv, .v1_5)) break :blk 5;
- if (target.cpu.has(.spirv, .v1_4)) break :blk 4;
- if (target.cpu.has(.spirv, .v1_3)) break :blk 3;
- if (target.cpu.has(.spirv, .v1_2)) break :blk 2;
- if (target.cpu.has(.spirv, .v1_1)) break :blk 1;
- break :blk 0;
- },
- };
-
- const generator_id: u32 = (spec.zig_generator_id << 16) | zig_spirv_compiler_version;
-
- const buffers = &[_][]const Word{
- capabilities_section.toWords(),
- extensions_section.toWords(),
- ext_inst_section.toWords(),
- memory_model_section.toWords(),
- entry_points_section.toWords(),
- execution_modes_section.toWords(),
- debug_strings_section.toWords(),
- debug_names_section.toWords(),
- annotations_section.toWords(),
- globals_section.toWords(),
- functions_section.toWords(),
- };
-
- var total_size: usize = 0;
- for (buffers) |buffer| {
- total_size += buffer.len;
- }
- const result = try arena.alloc(Word, total_size);
-
- var offset: usize = 0;
- for (buffers) |buffer| {
- @memcpy(result[offset..][0..buffer.len], buffer);
- offset += buffer.len;
- }
-
- return .{
- .words = result,
- .id_bound = next_id,
- .version = version,
- .generator_id = generator_id,
- };
+ try debug_strings.emit(gpa, .OpSourceExtension, .{ .extension = error_info.written() });
+ try debug_strings.emit(gpa, .OpSource, .{ .source_language = .zig, .version = version, .file = null, .source = null });
}
const MergedModule = struct {
@@ -608,6 +794,199 @@ const FragmentInfo = struct {
id_offset: Word,
};
+const LinkageDecoration = struct {
+ target_id: Id,
+ name: []const u8,
+ linkage_type: spec.LinkageType,
+
+ fn parse(inst: BinaryModule.Instruction) ?LinkageDecoration {
+ if (inst.opcode != .OpDecorate) return null;
+ if (inst.operands.len < 3) return null;
+ if (inst.operands[1] != @intFromEnum(spec.Decoration.linkage_attributes)) return null;
+ return .{
+ .target_id = @enumFromInt(inst.operands[0]),
+ .name = std.mem.sliceTo(std.mem.sliceAsBytes(inst.operands[2 .. inst.operands.len - 1]), 0),
+ .linkage_type = @enumFromInt(inst.operands[inst.operands.len - 1]),
+ };
+ }
+};
+
+const Sections = struct {
+ ext_inst: Section = .{},
+ globals: Section = .{},
+ functions: Section = .{},
+ annotations: Section = .{},
+ debug_names: Section = .{},
+ debug_strings: Section = .{},
+ entry_points: Section = .{},
+ execution_modes: Section = .{},
+
+ fn deinit(self: *Sections, gpa: Allocator) void {
+ self.ext_inst.deinit(gpa);
+ self.globals.deinit(gpa);
+ self.functions.deinit(gpa);
+ self.annotations.deinit(gpa);
+ self.debug_names.deinit(gpa);
+ self.debug_strings.deinit(gpa);
+ self.entry_points.deinit(gpa);
+ self.execution_modes.deinit(gpa);
+ }
+
+ const SectionClass = enum { ext_inst, debug_name, debug_string, annotation, global };
+
+ fn classifyPreambleInst(opcode: spec.Opcode) SectionClass {
+ return switch (opcode) {
+ .OpExtInstImport => .ext_inst,
+ .OpName, .OpMemberName => .debug_name,
+ .OpString => .debug_string,
+ .OpDecorate,
+ .OpMemberDecorate,
+ .OpGroupDecorate,
+ .OpGroupMemberDecorate,
+ .OpDecorationGroup,
+ .OpDecorateId,
+ .OpDecorateString,
+ .OpMemberDecorateString,
+ => .annotation,
+ else => .global,
+ };
+ }
+
+ fn getSection(self: *Sections, class: SectionClass) *Section {
+ return switch (class) {
+ .ext_inst => &self.ext_inst,
+ .debug_name => &self.debug_names,
+ .debug_string => &self.debug_strings,
+ .annotation => &self.annotations,
+ .global => &self.globals,
+ };
+ }
+};
+
+fn appendExternalObjects(
+ linker: *Linker,
+ gpa: Allocator,
+ parser: *BinaryModule.Parser,
+ ext_id_offsets: []const Word,
+ sections: *Sections,
+ has_linkage: *bool,
+ keep_entry_points: bool,
+ is_obj: bool,
+ resolved_ids: *const std.AutoArrayHashMapUnmanaged(Id, void),
+) error{OutOfMemory}!void {
+ var export_map: std.StringArrayHashMapUnmanaged(Id) = .empty;
+ defer export_map.deinit(gpa);
+
+ for (linker.external_objects.items, ext_id_offsets) |ext_obj, id_offset| {
+ var it: BinaryModule.Instruction.Iterator = .init(ext_obj.instructions, 0);
+ while (it.next()) |inst| {
+ const ld = LinkageDecoration.parse(inst) orelse continue;
+ if (ld.linkage_type != .@"export") continue;
+ try export_map.put(gpa, ld.name, @enumFromInt(@intFromEnum(ld.target_id) + id_offset));
+ }
+ }
+
+ var per_obj_remaps = try gpa.alloc(std.AutoHashMapUnmanaged(Id, Id), linker.external_objects.items.len);
+ defer {
+ for (per_obj_remaps) |*m| m.deinit(gpa);
+ gpa.free(per_obj_remaps);
+ }
+ for (per_obj_remaps) |*m| m.* = .empty;
+
+ var resolved_linkage_ids: std.AutoArrayHashMapUnmanaged(Id, void) = .empty;
+ defer resolved_linkage_ids.deinit(gpa);
+
+ for (resolved_ids.keys()) |id| {
+ try resolved_linkage_ids.put(gpa, id, {});
+ }
+
+ for (linker.external_objects.items, ext_id_offsets, 0..) |ext_obj, id_offset, obj_idx| {
+ var it: BinaryModule.Instruction.Iterator = .init(ext_obj.instructions, 0);
+ while (it.next()) |inst| {
+ const ld = LinkageDecoration.parse(inst) orelse continue;
+ if (ld.linkage_type != .import) continue;
+ const remapped_import: Id = @enumFromInt(@intFromEnum(ld.target_id) + id_offset);
+
+ if (export_map.get(ld.name)) |export_id| {
+ try per_obj_remaps[obj_idx].put(gpa, ld.target_id, export_id);
+ try resolved_linkage_ids.put(gpa, remapped_import, {});
+ try resolved_linkage_ids.put(gpa, export_id, {});
+ log.debug("cross-object resolve: '{s}' import={d} -> export={d}", .{
+ ld.name, @intFromEnum(remapped_import), @intFromEnum(export_id),
+ });
+ } else {
+ has_linkage.* = true;
+ }
+ }
+ }
+
+ for (linker.external_objects.items, ext_id_offsets, 0..) |ext_obj, id_offset, obj_idx| {
+ var binary = parser.initFromWords(ext_obj.instructions, ext_obj.id_bound) catch
+ return error.OutOfMemory;
+ defer binary.deinit(gpa);
+
+ const id_remap = &per_obj_remaps[obj_idx];
+
+ var preamble_it: BinaryModule.Instruction.Iterator = .init(ext_obj.instructions, 0);
+ while (preamble_it.next()) |inst| {
+ if (inst.offset >= binary.functions_start) break;
+
+ switch (inst.opcode) {
+ .OpCapability,
+ .OpExtension,
+ .OpMemoryModel,
+ .OpSource,
+ .OpSourceExtension,
+ .OpSourceContinued,
+ => continue,
+ .OpEntryPoint => {
+ if (keep_entry_points)
+ try remapAndAppendInst(gpa, §ions.entry_points, ext_obj.instructions, inst, id_offset, id_remap, parser);
+ continue;
+ },
+ .OpExecutionMode, .OpExecutionModeId => {
+ if (keep_entry_points)
+ try remapAndAppendInst(gpa, §ions.execution_modes, ext_obj.instructions, inst, id_offset, id_remap, parser);
+ continue;
+ },
+ else => {},
+ }
+
+ if (LinkageDecoration.parse(inst)) |ld| {
+ const remapped: Id = @enumFromInt(@intFromEnum(ld.target_id) + id_offset);
+ if (resolved_linkage_ids.contains(remapped)) {
+ if (ld.linkage_type == .@"export" and is_obj) {
+ has_linkage.* = true;
+ } else {
+ continue;
+ }
+ }
+ }
+
+ if (inst.opcode == .OpName and inst.operands.len >= 1) {
+ if (id_remap.contains(@enumFromInt(inst.operands[0]))) continue;
+ }
+
+ const dest = sections.getSection(Sections.classifyPreambleInst(inst.opcode));
+ try remapAndAppendInst(gpa, dest, ext_obj.instructions, inst, id_offset, id_remap, parser);
+ }
+
+ var fn_it: BinaryModule.Instruction.Iterator = .init(ext_obj.instructions, binary.functions_start);
+ var skip_function = false;
+ while (fn_it.next()) |inst| {
+ if (inst.opcode == .OpFunction) {
+ skip_function = id_remap.contains(@enumFromInt(inst.operands[1]));
+ }
+ if (!skip_function) {
+ try remapAndAppendInst(gpa, §ions.functions, ext_obj.instructions, inst, id_offset, id_remap, parser);
+ }
+ if (inst.opcode == .OpFunctionEnd) {
+ skip_function = false;
+ }
+ }
+ }
+}
+
fn collectEntryPointInterface(
linker: *Linker,
nav: InternPool.Nav.Index,
@@ -665,61 +1044,74 @@ fn remapAndAppend(
try dest.instructions.ensureUnusedCapacity(gpa, words.len);
- var iter = BinaryModule.Instruction.Iterator.init(words, 0);
- while (iter.next()) |inst| {
- const dest_start = dest.instructions.items.len;
- const inst_words = words[inst.offset..][0..((words[inst.offset] >> 16))];
- dest.instructions.appendSliceAssumeCapacity(inst_words);
- const inst_slice = dest.instructions.items[dest_start..][0..inst_words.len];
-
- const inst_spec = parser.getInstSpec(inst.opcode) orelse continue;
- var offset: usize = 0;
- for (inst_spec.operands) |operand| {
- const cat = operand.kind.category();
- switch (operand.quantifier) {
- .required => {
- if (offset >= inst.operands.len) break;
+ var it: BinaryModule.Instruction.Iterator = .init(words, 0);
+ while (it.next()) |inst| {
+ try remapAndAppendInst(gpa, dest, words, inst, id_offset, id_remap, parser);
+ }
+}
+
+fn remapAndAppendInst(
+ gpa: Allocator,
+ dest: *Section,
+ words: []const Word,
+ inst: BinaryModule.Instruction,
+ id_offset: Word,
+ id_remap: *const std.AutoHashMapUnmanaged(Id, Id),
+ parser: *BinaryModule.Parser,
+) error{OutOfMemory}!void {
+ const inst_words = words[inst.offset..][0..((words[inst.offset] >> 16))];
+ try dest.instructions.ensureUnusedCapacity(gpa, inst_words.len);
+ const dest_start = dest.instructions.items.len;
+ dest.instructions.appendSliceAssumeCapacity(inst_words);
+ const inst_slice = dest.instructions.items[dest_start..][0..inst_words.len];
+
+ const inst_spec = parser.getInstSpec(inst.opcode) orelse return;
+ var offset: usize = 0;
+ for (inst_spec.operands) |operand| {
+ const cat = operand.kind.category();
+ switch (operand.quantifier) {
+ .required => {
+ if (offset >= inst.operands.len) break;
+ if (cat == .id) {
+ remapSingleId(&inst_slice[1 + offset], id_offset, id_remap);
+ offset += 1;
+ } else if (cat == .literal) {
+ offset += operandLiteralWordCount(operand.kind, inst, offset);
+ } else if (cat == .composite) {
+ remapCompositeOperand(operand.kind, inst_slice, offset, id_offset, id_remap);
+ offset += 2;
+ } else {
+ offset += 1;
+ }
+ },
+ .optional => {
+ if (offset >= inst.operands.len) break;
+ if (cat == .id) {
+ remapSingleId(&inst_slice[1 + offset], id_offset, id_remap);
+ offset += 1;
+ } else if (cat == .literal) {
+ offset += operandLiteralWordCount(operand.kind, inst, offset);
+ } else {
+ offset += 1;
+ }
+ },
+ .variadic => {
+ while (offset < inst.operands.len) {
if (cat == .id) {
remapSingleId(&inst_slice[1 + offset], id_offset, id_remap);
offset += 1;
} else if (cat == .literal) {
offset += operandLiteralWordCount(operand.kind, inst, offset);
} else if (cat == .composite) {
- remapCompositeOperand(operand.kind, inst_slice, offset, id_offset, id_remap);
+ if (offset + 1 < inst.operands.len) {
+ remapCompositeOperand(operand.kind, inst_slice, offset, id_offset, id_remap);
+ }
offset += 2;
} else {
offset += 1;
}
- },
- .optional => {
- if (offset >= inst.operands.len) break;
- if (cat == .id) {
- remapSingleId(&inst_slice[1 + offset], id_offset, id_remap);
- offset += 1;
- } else if (cat == .literal) {
- offset += operandLiteralWordCount(operand.kind, inst, offset);
- } else {
- offset += 1;
- }
- },
- .variadic => {
- while (offset < inst.operands.len) {
- if (cat == .id) {
- remapSingleId(&inst_slice[1 + offset], id_offset, id_remap);
- offset += 1;
- } else if (cat == .literal) {
- offset += operandLiteralWordCount(operand.kind, inst, offset);
- } else if (cat == .composite) {
- if (offset + 1 < inst.operands.len) {
- remapCompositeOperand(operand.kind, inst_slice, offset, id_offset, id_remap);
- }
- offset += 2;
- } else {
- offset += 1;
- }
- }
- },
- }
+ }
+ },
}
}
}
diff --git a/src/link/SpirV/dedup_types.zig b/src/link/SpirV/dedup_types.zig
@@ -14,10 +14,6 @@ const Instruction = BinaryModule.Instruction;
/// When merging fragments from parallel codegen, duplicate type definitions
/// may exist. This pass identifies structurally identical types/constants,
/// keeps one canonical instance, and remaps all references to duplicates.
-///
-/// Decorations and names (OpName, OpMemberName) are included in the
-/// equality check: two types that are structurally identical but have
-/// different decorations or names are NOT considered duplicates.
pub fn run(parser: *BinaryModule.Parser, binary: *BinaryModule) !void {
const gpa = parser.gpa;
@@ -32,7 +28,7 @@ pub fn run(parser: *BinaryModule.Parser, binary: *BinaryModule) !void {
while (it.next()) |inst| {
if (inst.offset >= binary.functions_start) break;
switch (inst.opcode) {
- .OpName, .OpMemberName => {},
+ .OpName, .OpMemberName => continue,
else => switch (inst.opcode.class()) {
.annotation => {},
else => continue,
@@ -101,18 +97,11 @@ pub fn run(parser: *BinaryModule.Parser, binary: *BinaryModule) !void {
dec_hashes.items.len = 0;
for (dec_list.items) |dec| {
const dec_words = binary.instructions[dec.offset..][0..dec.len];
- const dec_opcode: Opcode = @enumFromInt(dec_words[0] & 0xFFFF);
var hasher = std.hash.Wyhash.init(0);
hasher.update(std.mem.asBytes(&dec_words[0]));
- // OpName/OpMemberName operands are literals (member index, string),
- // not ids — hash them directly without remapping
- if (dec_opcode == .OpName or dec_opcode == .OpMemberName) {
- hasher.update(std.mem.sliceAsBytes(dec_words[2..]));
- } else {
- for (dec_words[2..]) |w| {
- const w_val = if (id_remap.get(@enumFromInt(w))) |c| @intFromEnum(c) else w;
- hasher.update(std.mem.asBytes(&w_val));
- }
+ for (dec_words[2..]) |w| {
+ const w_val = if (id_remap.get(@enumFromInt(w))) |c| @intFromEnum(c) else w;
+ hasher.update(std.mem.asBytes(&w_val));
}
try dec_hashes.append(gpa, hasher.final());
}
diff --git a/src/link/SpirV/prune_unused.zig b/src/link/SpirV/prune_unused.zig
@@ -35,12 +35,39 @@ pub fn run(parser: *BinaryModule.Parser, binary: *BinaryModule) !void {
var id_offset_buf: std.ArrayList(u16) = .empty;
defer id_offset_buf.deinit(gpa);
- // mark non-prunable preamble instructions alive
+ // Mark non-prunable preamble instructions alive
+ // OpExtInst in the preamble is metadata (e.g. Zig error info) that references
+ // functions. skip it here so it doesn't root dead functions alive.
+ // These instructions are handled as prunable during the rewrite phase.
it = binary.iterateInstructions();
while (it.next()) |inst| {
if (inst.offset >= binary.functions_start) break;
- if (!canPrune(inst.opcode)) {
- markAlive(parser, binary.*, inst, &alive, &id_to_index, &code_offsets, &id_offset_buf) catch {};
+ if (canPrune(inst.opcode) or inst.opcode == .OpExtInst) continue;
+ try markAlive(
+ parser,
+ binary.*,
+ inst,
+ &alive,
+ &id_to_index,
+ &code_offsets,
+ &id_offset_buf,
+ );
+ }
+
+ // mark functions with LinkageAttributes Export alive
+ it = binary.iterateInstructions();
+ while (it.next()) |inst| {
+ if (inst.offset >= binary.functions_start) break;
+ if (inst.opcode == .OpDecorate and inst.operands.len >= 2 and
+ inst.operands[1] == @intFromEnum(spec.Decoration.linkage_attributes))
+ {
+ // Last word after the string is the linkage type; Export = 0.
+ if (inst.operands[inst.operands.len - 1] == @intFromEnum(spec.LinkageType.@"export")) {
+ const target: ResultId = @enumFromInt(inst.operands[0]);
+ if (id_to_index.get(target)) |index| {
+ alive.set(index);
+ }
+ }
}
}
@@ -58,11 +85,15 @@ pub fn run(parser: *BinaryModule.Parser, binary: *BinaryModule) !void {
}
continue;
}
+
+ // mark the function's type operands alive
+ try markAlive(parser, binary.*, inst, &alive, &id_to_index, &code_offsets, &id_offset_buf);
+ continue;
}
// mark operands of alive function contents
if (!canPrune(inst.opcode)) {
- markAlive(parser, binary.*, inst, &alive, &id_to_index, &code_offsets, &id_offset_buf) catch {};
+ try markAlive(parser, binary.*, inst, &alive, &id_to_index, &code_offsets, &id_offset_buf);
}
}
@@ -87,7 +118,9 @@ pub fn run(parser: *BinaryModule.Parser, binary: *BinaryModule) !void {
}
}
- if (canPrune(inst.opcode)) {
+ const is_prunable = canPrune(inst.opcode) or
+ (inst.opcode == .OpExtInst and inst.offset < binary.functions_start);
+ if (is_prunable) {
const inst_spec = parser.getInstSpec(inst.opcode) orelse {
appendInst(&new_words, binary, inst, &new_functions_start);
continue;
@@ -188,11 +221,11 @@ fn markAlive(
_ = fn_it.next();
while (fn_it.next()) |fn_inst| {
if (fn_inst.opcode == .OpFunctionEnd) break;
- markAlive(parser, binary, fn_inst, alive, id_to_index, code_offsets, id_offset_buf) catch {};
+ try markAlive(parser, binary, fn_inst, alive, id_to_index, code_offsets, id_offset_buf);
}
- markAlive(parser, binary, ref_inst, alive, id_to_index, code_offsets, id_offset_buf) catch {};
+ try markAlive(parser, binary, ref_inst, alive, id_to_index, code_offsets, id_offset_buf);
} else {
- markAlive(parser, binary, ref_inst, alive, id_to_index, code_offsets, id_offset_buf) catch {};
+ try markAlive(parser, binary, ref_inst, alive, id_to_index, code_offsets, id_offset_buf);
}
}
}
@@ -218,7 +251,6 @@ fn canPrune(op: Opcode) bool {
.OpString,
.OpName,
.OpMemberName,
- .OpExtInstImport,
.OpVariable,
=> true,
else => false,