zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

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:
Msrc/Compilation.zig | 3++-
Msrc/codegen/spirv/CodeGen.zig | 70+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/codegen/spirv/Module.zig | 3+++
Msrc/link.zig | 2+-
Msrc/link/SpirV.zig | 820++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/link/SpirV/dedup_types.zig | 19++++---------------
Msrc/link/SpirV/prune_unused.zig | 50+++++++++++++++++++++++++++++++++++++++++---------
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, &sections, 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, &sections, &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, + &sections.entry_points, + &sections.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, &sections.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, &sections.ext_inst, mir.extended_instruction_set, frag_info.id_offset, &id_remap, parser); + try remapAndAppend(gpa, &sections.globals, mir.globals, frag_info.id_offset, &id_remap, parser); + + try remapFilteredInsts(gpa, &sections.functions, mir.functions, frag_info.id_offset, &id_remap, parser, &resolved_local_ids, .skip_functions); + try remapFilteredInsts(gpa, &sections.annotations, mir.annotations, frag_info.id_offset, &id_remap, parser, &resolved_local_ids, .skip_linkage); + try remapFilteredInsts(gpa, &sections.debug_names, mir.debug_names, frag_info.id_offset, &id_remap, parser, &resolved_local_ids, .skip_names); + try remapAndAppend(gpa, &sections.debug_strings, mir.debug_strings, frag_info.id_offset, &id_remap, parser); + try remapAndAppend(gpa, &sections.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, &sections.entry_points, ext_obj.instructions, inst, id_offset, id_remap, parser); + continue; + }, + .OpExecutionMode, .OpExecutionModeId => { + if (keep_entry_points) + try remapAndAppendInst(gpa, &sections.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, &sections.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,