1const std = @import("std");
2const Allocator = std.mem.Allocator;
3const Path = std.Build.Cache.Path;
4const assert = std.debug.assert;
5const log = std.log.scoped(.link);
6const zig_version = @import("builtin").zig_version;
7const Zcu = @import("../Zcu.zig");
8const InternPool = @import("../InternPool.zig");
9const Compilation = @import("../Compilation.zig");
10const link = @import("../link.zig");
11const Air = @import("../Air.zig");
12const Type = @import("../Type.zig");
13const codegen = @import("../codegen.zig");
14const CodeGen = @import("../codegen/spirv/CodeGen.zig");
15const BinaryModule = @import("SpirV/BinaryModule.zig");
16const lower_invocation_globals = @import("SpirV/lower_invocation_globals.zig");
17const dedup_types = @import("SpirV/dedup_types.zig");
18const prune_unused = @import("SpirV/prune_unused.zig");
19const spec = @import("../codegen/spirv/spec.zig");
20const Section = @import("../codegen/spirv/Section.zig");
21const Id = spec.Id;
22const Word = spec.Word;
23const Mir = @import("../codegen/spirv/Mir.zig");
24
25const Linker = @This();
26
27base: link.File,
28fragments: std.array_hash_map.Auto(InternPool.Nav.Index, Mir) = .empty,
29pending_navs: std.ArrayList(InternPool.Nav.Index) = .empty,
30entry_points: std.ArrayList(EntryPointDecl) = .empty,
31external_objects: std.ArrayList(ExternalObject) = .empty,
32
33const EntryPointDecl = struct {
34 nav: InternPool.Nav.Index,
35 name: []const u8,
36 cc: std.builtin.CallingConvention,
37};
38
39const ExternalObject = struct {
40 instructions: []const Word,
41 id_bound: u32,
42};
43
44pub fn createEmpty(
45 arena: Allocator,
46 comp: *Compilation,
47 emit: Path,
48 options: link.File.OpenOptions,
49) !*Linker {
50 const io = comp.io;
51 const target = &comp.root_mod.resolved_target.result;
52
53 assert(!comp.config.use_lld); // Caught by Compilation.Config.resolve
54 assert(!comp.config.use_llvm); // Caught by Compilation.Config.resolve
55 assert(target.ofmt == .spirv); // Caught by Compilation.Config.resolve
56 switch (target.cpu.arch) {
57 .spirv32, .spirv64 => {},
58 else => unreachable, // Caught by Compilation.Config.resolve.
59 }
60 switch (target.os.tag) {
61 .opencl, .opengl, .vulkan => {},
62 else => unreachable, // Caught by Compilation.Config.resolve.
63 }
64
65 const linker = try arena.create(Linker);
66 linker.* = .{
67 .base = .{
68 .tag = .spirv,
69 .comp = comp,
70 .emit = emit,
71 .gc_sections = options.gc_sections orelse false,
72 .print_gc_sections = options.print_gc_sections,
73 .stack_size = options.stack_size orelse 0,
74 .allow_shlib_undefined = options.allow_shlib_undefined orelse false,
75 .file = null,
76 .build_id = options.build_id,
77 },
78 };
79 errdefer linker.deinit();
80
81 linker.base.file = try emit.root_dir.handle.createFile(io, emit.sub_path, .{
82 .truncate = true,
83 .read = true,
84 });
85
86 return linker;
87}
88
89pub fn open(
90 arena: Allocator,
91 comp: *Compilation,
92 emit: Path,
93 options: link.File.OpenOptions,
94) !*Linker {
95 return createEmpty(arena, comp, emit, options);
96}
97
98pub fn deinit(linker: *Linker) void {
99 const gpa = linker.base.comp.gpa;
100 for (linker.fragments.values()) |*mir| {
101 mir.deinit(gpa);
102 }
103 linker.fragments.deinit(gpa);
104 linker.pending_navs.deinit(gpa);
105 linker.entry_points.deinit(gpa);
106 for (linker.external_objects.items) |obj| {
107 gpa.free(obj.instructions);
108 }
109 linker.external_objects.deinit(gpa);
110}
111
112pub fn loadInput(linker: *Linker, input: link.Input) !void {
113 switch (input) {
114 .object => |obj| {
115 const comp = linker.base.comp;
116 const gpa = comp.gpa;
117 const io = comp.io;
118 const diags = &comp.link_diags;
119
120 const stat = obj.file.stat(io) catch |err|
121 return diags.fail("failed to stat SPIR-V object '{f}': {t}", .{ obj.path, err });
122 const file_size = std.math.cast(usize, stat.size) orelse
123 return diags.fail("SPIR-V object '{f}' is too large", .{obj.path});
124 if (file_size < 5 * @sizeOf(Word))
125 return diags.fail("SPIR-V object '{f}' is too small to contain a valid header", .{obj.path});
126 if (file_size % @sizeOf(Word) != 0)
127 return diags.fail("SPIR-V object '{f}' size is not a multiple of the word size", .{obj.path});
128
129 const word_count = file_size / @sizeOf(Word);
130 const all_words = try gpa.alloc(Word, word_count);
131 defer gpa.free(all_words);
132
133 const bytes = std.mem.sliceAsBytes(all_words);
134 const n_read = obj.file.readPositionalAll(io, bytes, 0) catch |err|
135 return diags.fail("failed to read SPIR-V object '{f}': {t}", .{ obj.path, err });
136 if (n_read != bytes.len)
137 return diags.fail("SPIR-V object '{f}': incomplete read", .{obj.path});
138
139 const needs_swap = all_words[0] == @byteSwap(spec.magic_number);
140 if (needs_swap) {
141 for (all_words) |*w| w.* = @byteSwap(w.*);
142 }
143
144 if (all_words[0] != spec.magic_number)
145 return diags.fail("SPIR-V object '{f}': invalid magic number", .{obj.path});
146
147 const id_bound = all_words[3];
148 const instructions = try gpa.dupe(Word, all_words[5..]);
149 errdefer gpa.free(instructions);
150
151 // OpCapability instructions appear at the top of the module
152 // so we can stop scanning as soon as we hit anything else.
153 var it: BinaryModule.Instruction.Iterator = .init(instructions, 0);
154 const has_linkage = while (it.next()) |inst| switch (inst.opcode) {
155 .OpCapability => {
156 const cap: spec.Capability = @fromBackingInt(@intCast(inst.operands[0]));
157 if (cap == .linkage) break true;
158 },
159 else => break false,
160 } else false;
161 if (!has_linkage) {
162 return diags.fail("SPIR-V object '{f}' is missing the Linkage capability and cannot be linked", .{obj.path});
163 }
164
165 try linker.external_objects.append(gpa, .{
166 .instructions = instructions,
167 .id_bound = id_bound,
168 });
169 },
170 else => {
171 const diags = &linker.base.comp.link_diags;
172 return diags.fail("unsupported link input for SPIR-V target", .{});
173 },
174 }
175}
176
177pub fn updateFunc(
178 linker: *Linker,
179 pt: Zcu.PerThread,
180 func_index: InternPool.Index,
181 mir: *codegen.AnyMir,
182) !void {
183 const gpa = linker.base.comp.gpa;
184 const nav = pt.zcu.funcInfo(func_index).owner_nav;
185
186 if (linker.fragments.getPtr(nav)) |existing| {
187 existing.deinit(gpa);
188 }
189
190 try linker.fragments.put(gpa, nav, mir.spirv);
191 mir.spirv = .{
192 .extended_instruction_set = &.{},
193 .globals = &.{},
194 .functions = &.{},
195 .annotations = &.{},
196 .debug_names = &.{},
197 .debug_strings = &.{},
198 .execution_modes = &.{},
199 .id_bound = 0,
200 .owner_nav = mir.spirv.owner_nav,
201 .kind = mir.spirv.kind,
202 .decl_result_id = .none,
203 .nav_refs = &.{},
204 .uav_refs = &.{},
205 .decl_deps = &.{},
206 .internal_globals = &.{},
207 .entry_points = &.{},
208 };
209}
210
211pub fn updateNav(linker: *Linker, pt: Zcu.PerThread, nav: InternPool.Nav.Index) link.Error!void {
212 const ip = &pt.zcu.intern_pool;
213 log.debug("deferring nav {f}({d}) to flush", .{ ip.getNav(nav).fqn.fmt(ip), nav });
214
215 const gpa = linker.base.comp.gpa;
216 linker.pending_navs.append(gpa, nav) catch return error.OutOfMemory;
217}
218
219pub fn updateExports(
220 linker: *Linker,
221 pt: Zcu.PerThread,
222 export_indices: []const Zcu.Export.Index,
223) link.Error!void {
224 const zcu = pt.zcu;
225 const ip = &zcu.intern_pool;
226 const gpa = linker.base.comp.gpa;
227 for (export_indices) |exp_index| {
228 const exp = exp_index.ptr(zcu);
229 const nav_index = switch (exp.exported) {
230 .nav => |nav| nav,
231 .uav => @panic("TODO: implement Linker linker code for exporting a constant value"),
232 };
233 const nav_ty = ip.getNav(nav_index).resolved.?.type;
234 if (ip.isFunctionType(nav_ty)) {
235 const cc = Type.fromInterned(nav_ty).fnCallingConvention(zcu);
236 try linker.entry_points.append(gpa, .{
237 .nav = nav_index,
238 .name = exp.opts.name.toSlice(ip),
239 .cc = cc,
240 });
241 }
242 }
243}
244
245pub fn flush(
246 linker: *Linker,
247 arena: Allocator,
248 tid: Zcu.PerThread.Id,
249 prog_node: std.Progress.Node,
250) link.Error!void {
251 const sub_prog_node = prog_node.start("Flush Module", 0);
252 defer sub_prog_node.end();
253
254 const comp = linker.base.comp;
255 const diags = &comp.link_diags;
256 const gpa = comp.gpa;
257 const io = comp.io;
258
259 if (comp.zcu) |zcu| {
260 const active = zcu.activate(tid);
261 defer active.deactivate();
262 const pt = active.pt;
263 for (linker.pending_navs.items) |nav| {
264 if (linker.fragments.contains(nav)) continue;
265
266 const mir = CodeGen.generateNav(pt, nav) catch |err| switch (err) {
267 error.OutOfMemory => return error.OutOfMemory,
268 error.AlreadyReported => continue,
269 error.Canceled => return error.Canceled,
270 };
271
272 linker.fragments.put(gpa, nav, mir) catch return error.OutOfMemory;
273 }
274 linker.pending_navs.clearRetainingCapacity();
275 }
276
277 const merged = mergeFragments(linker, gpa, arena) catch |err| switch (err) {
278 error.OutOfMemory => return error.OutOfMemory,
279 };
280
281 var binary = linkModule(arena, merged.words, merged.id_bound, sub_prog_node) catch |err| switch (err) {
282 error.OutOfMemory => |e| return e,
283 else => |other| {
284 return diags.fail("error while linking: {s}", .{@errorName(other)});
285 },
286 };
287 defer binary.deinit(arena);
288
289 const header = [_]Word{
290 spec.magic_number,
291 merged.version.toWord(),
292 merged.generator_id,
293 binary.id_bound,
294 0,
295 };
296
297 var file_writer = linker.base.file.?.writer(io, &.{});
298 file_writer.interface.writeSliceEndian(Word, &header, .little) catch |err| switch (err) {
299 error.WriteFailed => return diags.fail("failed to write: {t}", .{file_writer.err.?}),
300 };
301 file_writer.interface.writeSliceEndian(Word, binary.instructions, .little) catch |err| switch (err) {
302 error.WriteFailed => return diags.fail("failed to write: {t}", .{file_writer.err.?}),
303 };
304 file_writer.end() catch |err| switch (err) {
305 error.WriteFailed => return diags.fail("failed to write: {t}", .{file_writer.err.?}),
306 else => |e| return diags.fail("failed to write: {t}", .{e}),
307 };
308}
309
310fn linkModule(arena: Allocator, words: []const Word, id_bound: u32, progress: std.Progress.Node) !BinaryModule {
311 var parser = try BinaryModule.Parser.init(arena);
312 defer parser.deinit();
313 var binary = try parser.initFromWords(words, id_bound);
314 try prune_unused.run(&parser, &binary);
315 try dedup_types.run(&parser, &binary);
316 try lower_invocation_globals.run(&parser, &binary, progress);
317 return binary;
318}
319
320fn mergeFragments(linker: *Linker, gpa: Allocator, arena: Allocator) error{OutOfMemory}!MergedModule {
321 const comp = linker.base.comp;
322 const target = &comp.root_mod.resolved_target.result;
323 const maybe_ip: ?*InternPool = if (comp.zcu) |zcu| &zcu.intern_pool else null;
324 const is_obj = comp.config.output_mode == .Obj;
325
326 var next_id: Word = 1;
327
328 var nav_final_ids: std.AutoHashMapUnmanaged(InternPool.Nav.Index, Id) = .empty;
329 defer nav_final_ids.deinit(gpa);
330
331 var uav_final_ids: std.AutoHashMapUnmanaged(struct { InternPool.Index, spec.StorageClass }, Id) = .empty;
332 defer uav_final_ids.deinit(gpa);
333
334 var frag_infos: std.ArrayList(FragmentInfo) = .empty;
335 defer frag_infos.deinit(gpa);
336 try frag_infos.ensureTotalCapacity(gpa, @intCast(linker.fragments.count()));
337
338 for (linker.fragments.keys(), linker.fragments.values()) |nav, *mir| {
339 const id_offset = next_id - 1;
340 frag_infos.appendAssumeCapacity(.{ .id_offset = id_offset });
341 if (mir.decl_result_id != .none) {
342 try nav_final_ids.put(gpa, nav, @fromBackingInt(@intCast(@backingInt(mir.decl_result_id) + id_offset)));
343 }
344 next_id += mir.id_bound - 1;
345 }
346
347 for (linker.fragments.values(), frag_infos.items) |*mir, frag_info| {
348 for (mir.nav_refs) |ref| {
349 if (!nav_final_ids.contains(ref.nav)) {
350 try nav_final_ids.put(gpa, ref.nav, @fromBackingInt(@intCast(@backingInt(ref.local_id) + frag_info.id_offset)));
351 }
352 }
353 for (mir.uav_refs) |ref| {
354 const key = .{ ref.val, ref.storage_class };
355 if (!uav_final_ids.contains(key)) {
356 try uav_final_ids.put(gpa, key, @fromBackingInt(@intCast(@backingInt(ref.local_id) + frag_info.id_offset)));
357 }
358 }
359 }
360
361 // Resolve Zig extern navs against external objects.
362 var ext_id_offsets: std.ArrayList(Word) = .empty;
363 defer ext_id_offsets.deinit(gpa);
364 try ext_id_offsets.ensureTotalCapacity(gpa, linker.external_objects.items.len);
365
366 var unresolved_extern_count: u32 = 0;
367 var resolved_ids: std.array_hash_map.Auto(Id, void) = .empty;
368 defer resolved_ids.deinit(gpa);
369
370 if (maybe_ip) |ip| {
371 var extern_name_map: std.array_hash_map.String(InternPool.Nav.Index) = .empty;
372 defer extern_name_map.deinit(gpa);
373
374 var nav_it = nav_final_ids.iterator();
375 while (nav_it.next()) |entry| {
376 const nav = ip.getNav(entry.key_ptr.*);
377 if (!nav.resolved.?.is_extern_decl) continue;
378 const name = if (nav.getExtern(ip)) |e| e.name.toSlice(ip) else nav.fqn.toSlice(ip);
379 try extern_name_map.put(gpa, name, entry.key_ptr.*);
380 }
381
382 for (linker.external_objects.items) |ext_obj| {
383 const id_offset = next_id - 1;
384 ext_id_offsets.appendAssumeCapacity(id_offset);
385
386 var it: BinaryModule.Instruction.Iterator = .init(ext_obj.instructions, 0);
387 while (it.next()) |inst| {
388 const ld = LinkageDecoration.parse(inst) orelse continue;
389 if (ld.linkage_type != .@"export") continue;
390 const remapped_id: Id = @fromBackingInt(@intCast(@backingInt(ld.target_id) + id_offset));
391
392 if (extern_name_map.get(ld.name)) |nav_index| {
393 log.debug("extern resolve: '{s}' -> ext_fn_id={d}", .{ ld.name, @backingInt(remapped_id) });
394 nav_final_ids.getPtr(nav_index).?.* = remapped_id;
395 _ = extern_name_map.swapRemove(ld.name);
396 try resolved_ids.put(gpa, remapped_id, {});
397 }
398 }
399 next_id += ext_obj.id_bound - 1;
400 }
401
402 unresolved_extern_count = @intCast(extern_name_map.count());
403 } else {
404 for (linker.external_objects.items) |ext_obj| {
405 ext_id_offsets.appendAssumeCapacity(next_id - 1);
406 next_id += ext_obj.id_bound - 1;
407 }
408 }
409
410 var parser = BinaryModule.Parser.init(gpa) catch return error.OutOfMemory;
411 defer parser.deinit();
412 var sections: Sections = .{};
413 defer sections.deinit(gpa);
414
415 try mergeZigFragments(linker, gpa, &parser, &sections, frag_infos.items, &nav_final_ids, &uav_final_ids, &resolved_ids, maybe_ip);
416
417 var has_linkage = false;
418 try appendExternalObjects(linker, gpa, &parser, ext_id_offsets.items, &sections, &has_linkage, linker.fragments.count() == 0, is_obj, &resolved_ids);
419
420 if (is_obj) {
421 for (linker.entry_points.items) |ep| {
422 if (ep.cc != .spirv_device) continue;
423 const final_id = nav_final_ids.get(ep.nav) orelse continue;
424 try sections.annotations.emit(gpa, .OpDecorate, .{
425 .target = final_id,
426 .decoration = .{ .linkage_attributes = .{ .name = ep.name, .linkage_type = .@"export" } },
427 });
428 has_linkage = true;
429 }
430 if (unresolved_extern_count > 0) has_linkage = true;
431 }
432
433 var capabilities_section = Section{};
434 defer capabilities_section.deinit(gpa);
435 var extensions_section = Section{};
436 defer extensions_section.deinit(gpa);
437 var memory_model_section = Section{};
438 defer memory_model_section.deinit(gpa);
439
440 try emitPreamble(
441 gpa,
442 target,
443 has_linkage,
444 &capabilities_section,
445 &extensions_section,
446 &memory_model_section,
447 );
448 try emitEntryPoints(
449 linker,
450 gpa,
451 target,
452 &sections.entry_points,
453 &sections.execution_modes,
454 &nav_final_ids,
455 &uav_final_ids,
456 &frag_infos,
457 );
458
459 const zig_packed_version = (zig_version.major << 12) | (zig_version.minor << 7) | zig_version.patch;
460 if (maybe_ip) |ip| {
461 try emitSourceInfo(gpa, ip, zig_packed_version, &sections.debug_strings);
462 }
463
464 const version: spec.Version = .{
465 .major = 1,
466 .minor = blk: {
467 if (target.cpu.has(.spirv, .v1_6)) break :blk 6;
468 if (target.cpu.has(.spirv, .v1_5)) break :blk 5;
469 if (target.cpu.has(.spirv, .v1_4)) break :blk 4;
470 if (target.cpu.has(.spirv, .v1_3)) break :blk 3;
471 if (target.cpu.has(.spirv, .v1_2)) break :blk 2;
472 if (target.cpu.has(.spirv, .v1_1)) break :blk 1;
473 break :blk 0;
474 },
475 };
476
477 const buffers = &[_][]const Word{
478 capabilities_section.toWords(),
479 extensions_section.toWords(),
480 sections.ext_inst.toWords(),
481 memory_model_section.toWords(),
482 sections.entry_points.toWords(),
483 sections.execution_modes.toWords(),
484 sections.debug_strings.toWords(),
485 sections.debug_names.toWords(),
486 sections.annotations.toWords(),
487 sections.globals.toWords(),
488 sections.functions.toWords(),
489 };
490
491 var total_size: usize = 0;
492 for (buffers) |buffer| total_size += buffer.len;
493 const result = try arena.alloc(Word, total_size);
494
495 var offset: usize = 0;
496 for (buffers) |buffer| {
497 @memcpy(result[offset..][0..buffer.len], buffer);
498 offset += buffer.len;
499 }
500
501 return .{
502 .words = result,
503 .id_bound = next_id,
504 .version = version,
505 .generator_id = (spec.zig_generator_id << 16) | zig_packed_version,
506 };
507}
508
509fn mergeZigFragments(
510 linker: *Linker,
511 gpa: Allocator,
512 parser: *BinaryModule.Parser,
513 sections: *Sections,
514 frag_infos: []const FragmentInfo,
515 nav_final_ids: *const std.AutoHashMapUnmanaged(InternPool.Nav.Index, Id),
516 uav_final_ids: *const std.AutoHashMapUnmanaged(struct { InternPool.Index, spec.StorageClass }, Id),
517 resolved_ids: *const std.array_hash_map.Auto(Id, void),
518 maybe_ip: ?*InternPool,
519) error{OutOfMemory}!void {
520 for (linker.fragments.values(), frag_infos) |*mir, frag_info| {
521 var id_remap: std.AutoHashMapUnmanaged(Id, Id) = .empty;
522 defer id_remap.deinit(gpa);
523
524 var resolved_local_ids: std.array_hash_map.Auto(Id, void) = .empty;
525 defer resolved_local_ids.deinit(gpa);
526
527 for (mir.nav_refs) |ref| {
528 if (nav_final_ids.get(ref.nav)) |final_id| {
529 try id_remap.put(gpa, ref.local_id, final_id);
530 if (maybe_ip) |ip| {
531 const nav = ip.getNav(ref.nav);
532 if (nav.resolved.?.is_extern_decl and resolved_ids.contains(final_id)) {
533 try resolved_local_ids.put(gpa, ref.local_id, {});
534 }
535 }
536 }
537 }
538 for (mir.uav_refs) |ref| {
539 if (uav_final_ids.get(.{ ref.val, ref.storage_class })) |final_id| {
540 try id_remap.put(gpa, ref.local_id, final_id);
541 }
542 }
543
544 try remapAndAppend(gpa, &sections.ext_inst, mir.extended_instruction_set, frag_info.id_offset, &id_remap, parser);
545 try remapAndAppend(gpa, &sections.globals, mir.globals, frag_info.id_offset, &id_remap, parser);
546
547 try remapFilteredInsts(gpa, &sections.functions, mir.functions, frag_info.id_offset, &id_remap, parser, &resolved_local_ids, .skip_functions);
548 try remapFilteredInsts(gpa, &sections.annotations, mir.annotations, frag_info.id_offset, &id_remap, parser, &resolved_local_ids, .skip_linkage);
549 try remapFilteredInsts(gpa, &sections.debug_names, mir.debug_names, frag_info.id_offset, &id_remap, parser, &resolved_local_ids, .skip_names);
550 try remapAndAppend(gpa, &sections.debug_strings, mir.debug_strings, frag_info.id_offset, &id_remap, parser);
551 try remapAndAppend(gpa, &sections.execution_modes, mir.execution_modes, frag_info.id_offset, &id_remap, parser);
552
553 for (mir.entry_points) |ep| {
554 try linker.entry_points.append(gpa, .{ .nav = mir.owner_nav, .name = ep.name, .cc = ep.cc });
555 }
556 }
557}
558
559const FilterMode = enum { skip_functions, skip_linkage, skip_names };
560
561fn remapFilteredInsts(
562 gpa: Allocator,
563 dest: *Section,
564 words: []const Word,
565 id_offset: Word,
566 id_remap: *const std.AutoHashMapUnmanaged(Id, Id),
567 parser: *BinaryModule.Parser,
568 skip_ids: *const std.array_hash_map.Auto(Id, void),
569 mode: FilterMode,
570) error{OutOfMemory}!void {
571 if (words.len == 0) return;
572 var it: BinaryModule.Instruction.Iterator = .init(words, 0);
573 var skip_function = false;
574 while (it.next()) |inst| {
575 switch (mode) {
576 .skip_functions => {
577 if (inst.opcode == .OpFunction) {
578 skip_function = skip_ids.contains(@fromBackingInt(@intCast(inst.operands[1])));
579 }
580 if (skip_function) {
581 if (inst.opcode == .OpFunctionEnd) skip_function = false;
582 continue;
583 }
584 },
585 .skip_linkage => {
586 if (LinkageDecoration.parse(inst)) |ld| {
587 if (skip_ids.contains(ld.target_id)) continue;
588 }
589 },
590 .skip_names => {
591 if (inst.opcode == .OpName and inst.operands.len >= 1) {
592 if (skip_ids.contains(@fromBackingInt(@intCast(inst.operands[0])))) continue;
593 }
594 },
595 }
596 try remapAndAppendInst(gpa, dest, words, inst, id_offset, id_remap, parser);
597 }
598}
599
600fn emitPreamble(
601 gpa: Allocator,
602 target: *const std.Target,
603 has_linkage: bool,
604 capabilities: *Section,
605 extensions: *Section,
606 memory_model: *Section,
607) !void {
608 var caps: std.EnumSet(spec.Capability) = .empty;
609 var exts: std.StringHashMapUnmanaged(void) = .empty;
610 defer exts.deinit(gpa);
611
612 switch (target.os.tag) {
613 .opengl, .vulkan => caps.insert(.shader),
614 .opencl, .amdhsa => {
615 caps.insert(.kernel);
616 caps.insert(.addresses);
617 },
618 else => unreachable,
619 }
620 if (target.cpu.arch == .spirv64) {
621 caps.insert(.int64);
622 if (target.os.tag == .vulkan) {
623 caps.insert(.physical_storage_buffer_addresses);
624 try exts.put(gpa, "SPV_KHR_physical_storage_buffer", {});
625 }
626 }
627 if (has_linkage) caps.insert(.linkage);
628
629 inline for (@typeInfo(spec.Capability).@"enum".field_names) |cap_name| {
630 if (@hasField(std.Target.spirv.Feature, cap_name)) {
631 if (target.cpu.has(.spirv, @field(std.Target.spirv.Feature, cap_name)))
632 caps.insert(@field(spec.Capability, cap_name));
633 }
634 }
635 inline for (@typeInfo(spec.Extension).@"enum".field_names) |ext_name| {
636 switch (@field(spec.Extension, ext_name)) {
637 .v1_0, .v1_1, .v1_2, .v1_3, .v1_4, .v1_5, .v1_6 => {},
638 else => if (@hasField(std.Target.spirv.Feature, ext_name)) {
639 if (target.cpu.has(.spirv, @field(std.Target.spirv.Feature, ext_name)))
640 try exts.put(gpa, ext_name, {});
641 },
642 }
643 }
644
645 var cit = caps.iterator();
646 while (cit.next()) |cap| try capabilities.emit(gpa, .OpCapability, .{ .capability = cap });
647 var eit = exts.iterator();
648 while (eit.next()) |e| try extensions.emit(gpa, .OpExtension, .{ .name = e.key_ptr.* });
649
650 const addressing_model: spec.AddressingModel = switch (target.os.tag) {
651 .opengl => .logical,
652 .vulkan => switch (target.cpu.arch) {
653 .spirv32 => .logical,
654 .spirv64 => .physical_storage_buffer64,
655 else => unreachable,
656 },
657 .opencl => switch (target.cpu.arch) {
658 .spirv32 => .physical32,
659 .spirv64 => .physical64,
660 else => unreachable,
661 },
662 .amdhsa => .physical64,
663 else => unreachable,
664 };
665 try memory_model.emit(gpa, .OpMemoryModel, .{
666 .addressing_model = addressing_model,
667 .memory_model = switch (target.os.tag) {
668 .opencl => .open_cl,
669 .vulkan, .opengl => .glsl450,
670 .amdhsa => unreachable, // TODO
671 else => unreachable,
672 },
673 });
674}
675
676fn emitEntryPoints(
677 linker: *Linker,
678 gpa: Allocator,
679 target: *const std.Target,
680 entry_points_section: *Section,
681 execution_modes_section: *Section,
682 nav_final_ids: *const std.AutoHashMapUnmanaged(InternPool.Nav.Index, Id),
683 uav_final_ids: *const std.AutoHashMapUnmanaged(struct { InternPool.Index, spec.StorageClass }, Id),
684 frag_infos: *const std.ArrayList(FragmentInfo),
685) error{OutOfMemory}!void {
686 for (linker.entry_points.items) |ep| {
687 const final_id = nav_final_ids.get(ep.nav) orelse continue;
688
689 var interface: std.ArrayList(Id) = .empty;
690 defer interface.deinit(gpa);
691 var visited: std.AutoHashMapUnmanaged(InternPool.Nav.Index, void) = .empty;
692 defer visited.deinit(gpa);
693 try collectEntryPointInterface(linker, ep.nav, &interface, &visited, nav_final_ids, uav_final_ids, frag_infos, gpa);
694
695 const exec_model: spec.ExecutionModel = switch (target.os.tag) {
696 .vulkan, .opengl => switch (ep.cc) {
697 .spirv_vertex => .vertex,
698 .spirv_fragment => .fragment,
699 .spirv_kernel => .gl_compute,
700 .spirv_task => .task_ext,
701 .spirv_mesh => .mesh_ext,
702 .spirv_device => continue,
703 else => unreachable,
704 },
705 .opencl => switch (ep.cc) {
706 .spirv_kernel => .kernel,
707 .spirv_device => continue,
708 else => unreachable,
709 },
710 else => unreachable,
711 };
712
713 try entry_points_section.emit(gpa, .OpEntryPoint, .{
714 .execution_model = exec_model,
715 .entry_point = final_id,
716 .name = ep.name,
717 .interface = interface.items,
718 });
719
720 switch (ep.cc) {
721 .spirv_kernel, .spirv_task => |kernel| {
722 try execution_modes_section.emit(gpa, .OpExecutionMode, .{
723 .entry_point = final_id,
724 .mode = .{ .local_size = .{ .x_size = kernel.x, .y_size = kernel.y, .z_size = kernel.z } },
725 });
726 },
727 .spirv_fragment => |fragment| {
728 try execution_modes_section.emit(gpa, .OpExecutionMode, .{
729 .entry_point = final_id,
730 .mode = if (target.os.tag == .vulkan) .origin_upper_left else .origin_lower_left,
731 });
732 if (fragment.pixel_centered_integer) {
733 try execution_modes_section.emit(gpa, .OpExecutionMode, .{
734 .entry_point = final_id,
735 .mode = .pixel_center_integer,
736 });
737 }
738 const exec_mode: ?spec.ExecutionMode.Extended = switch (fragment.depth_assumption) {
739 .none => null,
740 .greater => .depth_greater,
741 .less => .depth_less,
742 .unchanged => .depth_unchanged,
743 };
744 if (exec_mode) |mode| {
745 try execution_modes_section.emit(gpa, .OpExecutionMode, .{
746 .entry_point = final_id,
747 .mode = mode,
748 });
749 }
750 },
751 .spirv_mesh => |mesh| {
752 try execution_modes_section.emit(gpa, .OpExecutionMode, .{
753 .entry_point = final_id,
754 .mode = .{ .output_vertices = .{ .vertex_count = mesh.max_vertices } },
755 });
756 try execution_modes_section.emit(gpa, .OpExecutionMode, .{
757 .entry_point = final_id,
758 .mode = .{ .output_primitives_ext = .{ .primitive_count = mesh.max_primitives } },
759 });
760 try execution_modes_section.emit(gpa, .OpExecutionMode, .{
761 .entry_point = final_id,
762 .mode = .{ .local_size = .{ .x_size = mesh.x, .y_size = mesh.y, .z_size = mesh.z } },
763 });
764 try execution_modes_section.emit(gpa, .OpExecutionMode, .{
765 .entry_point = final_id,
766 .mode = switch (mesh.stage_output) {
767 .output_points => .output_points,
768 .output_lines => .output_lines_ext,
769 .output_triangles => .output_triangles_ext,
770 },
771 });
772 },
773 else => {},
774 }
775 }
776}
777
778fn emitSourceInfo(gpa: Allocator, ip: *InternPool, version: u32, debug_strings: *Section) error{OutOfMemory}!void {
779 var error_info: std.Io.Writer.Allocating = .init(gpa);
780 defer error_info.deinit();
781 error_info.writer.writeAll("zig_errors:") catch return error.OutOfMemory;
782 for (ip.global_error_set.getNamesFromMainThread()) |name| {
783 error_info.writer.writeByte(':') catch return error.OutOfMemory;
784 std.Uri.Component.percentEncode(
785 &error_info.writer,
786 name.toSlice(ip),
787 struct {
788 fn isValidChar(c: u8) bool {
789 return switch (c) {
790 0, '%', ':' => false,
791 else => true,
792 };
793 }
794 }.isValidChar,
795 ) catch return error.OutOfMemory;
796 }
797 try debug_strings.emit(gpa, .OpSourceExtension, .{ .extension = error_info.written() });
798 try debug_strings.emit(gpa, .OpSource, .{ .source_language = .zig, .version = version, .file = null, .source = null });
799}
800
801const MergedModule = struct {
802 words: []const Word,
803 id_bound: Word,
804 version: spec.Version,
805 generator_id: u32,
806};
807
808const FragmentInfo = struct {
809 id_offset: Word,
810};
811
812const LinkageDecoration = struct {
813 target_id: Id,
814 name: []const u8,
815 linkage_type: spec.LinkageType,
816
817 fn parse(inst: BinaryModule.Instruction) ?LinkageDecoration {
818 if (inst.opcode != .OpDecorate) return null;
819 if (inst.operands.len < 3) return null;
820 if (inst.operands[1] != @backingInt(spec.Decoration.linkage_attributes)) return null;
821 return .{
822 .target_id = @fromBackingInt(@intCast(inst.operands[0])),
823 .name = std.mem.sliceTo(std.mem.sliceAsBytes(inst.operands[2 .. inst.operands.len - 1]), 0),
824 .linkage_type = @fromBackingInt(@intCast(inst.operands[inst.operands.len - 1])),
825 };
826 }
827};
828
829const Sections = struct {
830 ext_inst: Section = .{},
831 globals: Section = .{},
832 functions: Section = .{},
833 annotations: Section = .{},
834 debug_names: Section = .{},
835 debug_strings: Section = .{},
836 entry_points: Section = .{},
837 execution_modes: Section = .{},
838
839 fn deinit(self: *Sections, gpa: Allocator) void {
840 self.ext_inst.deinit(gpa);
841 self.globals.deinit(gpa);
842 self.functions.deinit(gpa);
843 self.annotations.deinit(gpa);
844 self.debug_names.deinit(gpa);
845 self.debug_strings.deinit(gpa);
846 self.entry_points.deinit(gpa);
847 self.execution_modes.deinit(gpa);
848 }
849
850 const SectionClass = enum { ext_inst, debug_name, debug_string, annotation, global };
851
852 fn classifyPreambleInst(opcode: spec.Opcode) SectionClass {
853 return switch (opcode) {
854 .OpExtInstImport => .ext_inst,
855 .OpName, .OpMemberName => .debug_name,
856 .OpString => .debug_string,
857 .OpDecorate,
858 .OpMemberDecorate,
859 .OpGroupDecorate,
860 .OpGroupMemberDecorate,
861 .OpDecorationGroup,
862 .OpDecorateId,
863 .OpDecorateString,
864 .OpMemberDecorateString,
865 => .annotation,
866 else => .global,
867 };
868 }
869
870 fn getSection(self: *Sections, class: SectionClass) *Section {
871 return switch (class) {
872 .ext_inst => &self.ext_inst,
873 .debug_name => &self.debug_names,
874 .debug_string => &self.debug_strings,
875 .annotation => &self.annotations,
876 .global => &self.globals,
877 };
878 }
879};
880
881fn appendExternalObjects(
882 linker: *Linker,
883 gpa: Allocator,
884 parser: *BinaryModule.Parser,
885 ext_id_offsets: []const Word,
886 sections: *Sections,
887 has_linkage: *bool,
888 keep_entry_points: bool,
889 is_obj: bool,
890 resolved_ids: *const std.array_hash_map.Auto(Id, void),
891) error{OutOfMemory}!void {
892 var export_map: std.array_hash_map.String(Id) = .empty;
893 defer export_map.deinit(gpa);
894
895 for (linker.external_objects.items, ext_id_offsets) |ext_obj, id_offset| {
896 var it: BinaryModule.Instruction.Iterator = .init(ext_obj.instructions, 0);
897 while (it.next()) |inst| {
898 const ld = LinkageDecoration.parse(inst) orelse continue;
899 if (ld.linkage_type != .@"export") continue;
900 try export_map.put(gpa, ld.name, @fromBackingInt(@intCast(@backingInt(ld.target_id) + id_offset)));
901 }
902 }
903
904 var per_obj_remaps = try gpa.alloc(std.AutoHashMapUnmanaged(Id, Id), linker.external_objects.items.len);
905 defer {
906 for (per_obj_remaps) |*m| m.deinit(gpa);
907 gpa.free(per_obj_remaps);
908 }
909 for (per_obj_remaps) |*m| m.* = .empty;
910
911 var resolved_linkage_ids: std.array_hash_map.Auto(Id, void) = .empty;
912 defer resolved_linkage_ids.deinit(gpa);
913
914 for (resolved_ids.keys()) |id| {
915 try resolved_linkage_ids.put(gpa, id, {});
916 }
917
918 for (linker.external_objects.items, ext_id_offsets, 0..) |ext_obj, id_offset, obj_idx| {
919 var it: BinaryModule.Instruction.Iterator = .init(ext_obj.instructions, 0);
920 while (it.next()) |inst| {
921 const ld = LinkageDecoration.parse(inst) orelse continue;
922 if (ld.linkage_type != .import) continue;
923 const remapped_import: Id = @fromBackingInt(@intCast(@backingInt(ld.target_id) + id_offset));
924
925 if (export_map.get(ld.name)) |export_id| {
926 try per_obj_remaps[obj_idx].put(gpa, ld.target_id, export_id);
927 try resolved_linkage_ids.put(gpa, remapped_import, {});
928 try resolved_linkage_ids.put(gpa, export_id, {});
929 log.debug("cross-object resolve: '{s}' import={d} -> export={d}", .{
930 ld.name, @backingInt(remapped_import), @backingInt(export_id),
931 });
932 } else {
933 has_linkage.* = true;
934 }
935 }
936 }
937
938 for (linker.external_objects.items, ext_id_offsets, 0..) |ext_obj, id_offset, obj_idx| {
939 var binary = parser.initFromWords(ext_obj.instructions, ext_obj.id_bound) catch
940 return error.OutOfMemory;
941 defer binary.deinit(gpa);
942
943 const id_remap = &per_obj_remaps[obj_idx];
944
945 var preamble_it: BinaryModule.Instruction.Iterator = .init(ext_obj.instructions, 0);
946 while (preamble_it.next()) |inst| {
947 if (inst.offset >= binary.functions_start) break;
948
949 switch (inst.opcode) {
950 .OpCapability,
951 .OpExtension,
952 .OpMemoryModel,
953 .OpSource,
954 .OpSourceExtension,
955 .OpSourceContinued,
956 => continue,
957 .OpEntryPoint => {
958 if (keep_entry_points)
959 try remapAndAppendInst(gpa, &sections.entry_points, ext_obj.instructions, inst, id_offset, id_remap, parser);
960 continue;
961 },
962 .OpExecutionMode, .OpExecutionModeId => {
963 if (keep_entry_points)
964 try remapAndAppendInst(gpa, &sections.execution_modes, ext_obj.instructions, inst, id_offset, id_remap, parser);
965 continue;
966 },
967 else => {},
968 }
969
970 if (LinkageDecoration.parse(inst)) |ld| {
971 const remapped: Id = @fromBackingInt(@intCast(@backingInt(ld.target_id) + id_offset));
972 if (resolved_linkage_ids.contains(remapped)) {
973 if (ld.linkage_type == .@"export" and is_obj) {
974 has_linkage.* = true;
975 } else {
976 continue;
977 }
978 }
979 }
980
981 if (inst.opcode == .OpName and inst.operands.len >= 1) {
982 if (id_remap.contains(@fromBackingInt(@intCast(inst.operands[0])))) continue;
983 }
984
985 const dest = sections.getSection(Sections.classifyPreambleInst(inst.opcode));
986 try remapAndAppendInst(gpa, dest, ext_obj.instructions, inst, id_offset, id_remap, parser);
987 }
988
989 var fn_it: BinaryModule.Instruction.Iterator = .init(ext_obj.instructions, binary.functions_start);
990 var skip_function = false;
991 while (fn_it.next()) |inst| {
992 if (inst.opcode == .OpFunction) {
993 skip_function = id_remap.contains(@fromBackingInt(@intCast(inst.operands[1])));
994 }
995 if (!skip_function) {
996 try remapAndAppendInst(gpa, &sections.functions, ext_obj.instructions, inst, id_offset, id_remap, parser);
997 }
998 if (inst.opcode == .OpFunctionEnd) {
999 skip_function = false;
1000 }
1001 }
1002 }
1003}
1004
1005fn collectEntryPointInterface(
1006 linker: *Linker,
1007 nav: InternPool.Nav.Index,
1008 interface: *std.ArrayList(Id),
1009 visited: *std.AutoHashMapUnmanaged(InternPool.Nav.Index, void),
1010 nav_final_ids: *const std.AutoHashMapUnmanaged(InternPool.Nav.Index, Id),
1011 uav_final_ids: *const std.AutoHashMapUnmanaged(struct { InternPool.Index, spec.StorageClass }, Id),
1012 frag_infos: *const std.ArrayList(FragmentInfo),
1013 gpa: Allocator,
1014) error{OutOfMemory}!void {
1015 const visited_gop = try visited.getOrPut(gpa, nav);
1016 if (visited_gop.found_existing) return;
1017
1018 const frag_index = linker.fragments.getIndex(nav) orelse return;
1019 const mir = &linker.fragments.values()[frag_index];
1020 const id_offset = frag_infos.items[frag_index].id_offset;
1021
1022 if (mir.kind == .global) {
1023 if (nav_final_ids.get(nav)) |final_id| {
1024 try interface.append(gpa, final_id);
1025 }
1026 }
1027
1028 for (mir.uav_refs) |ref| {
1029 if (ref.kind == .global) {
1030 if (uav_final_ids.get(.{ ref.val, ref.storage_class })) |final_id| {
1031 try interface.append(gpa, final_id);
1032 }
1033 }
1034 }
1035
1036 for (mir.internal_globals) |local_id| {
1037 const global_id: Id = @fromBackingInt(@intCast(@backingInt(local_id) + id_offset));
1038 try interface.append(gpa, global_id);
1039 }
1040
1041 for (mir.decl_deps) |dep| {
1042 try collectEntryPointInterface(linker, dep.nav, interface, visited, nav_final_ids, uav_final_ids, frag_infos, gpa);
1043 }
1044}
1045
1046fn remapAndAppend(
1047 gpa: Allocator,
1048 dest: *Section,
1049 words: []const Word,
1050 id_offset: Word,
1051 id_remap: *const std.AutoHashMapUnmanaged(Id, Id),
1052 parser: *BinaryModule.Parser,
1053) error{OutOfMemory}!void {
1054 if (words.len == 0) return;
1055
1056 try dest.instructions.ensureUnusedCapacity(gpa, words.len);
1057
1058 var it: BinaryModule.Instruction.Iterator = .init(words, 0);
1059 while (it.next()) |inst| {
1060 try remapAndAppendInst(gpa, dest, words, inst, id_offset, id_remap, parser);
1061 }
1062}
1063
1064fn remapAndAppendInst(
1065 gpa: Allocator,
1066 dest: *Section,
1067 words: []const Word,
1068 inst: BinaryModule.Instruction,
1069 id_offset: Word,
1070 id_remap: *const std.AutoHashMapUnmanaged(Id, Id),
1071 parser: *BinaryModule.Parser,
1072) error{OutOfMemory}!void {
1073 const inst_words = words[inst.offset..][0..((words[inst.offset] >> 16))];
1074 try dest.instructions.ensureUnusedCapacity(gpa, inst_words.len);
1075 const dest_start = dest.instructions.items.len;
1076 dest.instructions.appendSliceAssumeCapacity(inst_words);
1077 const inst_slice = dest.instructions.items[dest_start..][0..inst_words.len];
1078
1079 const inst_spec = parser.getInstSpec(inst.opcode) orelse return;
1080 var offset: usize = 0;
1081 for (inst_spec.operands) |operand| {
1082 const cat = operand.kind.category();
1083 switch (operand.quantifier) {
1084 .required, .optional => {
1085 if (offset >= inst.operands.len) break;
1086 offset += remapOperand(operand.kind, cat, inst, inst_slice, offset, id_offset, id_remap);
1087 },
1088 .variadic => {
1089 while (offset < inst.operands.len) {
1090 offset += remapOperand(operand.kind, cat, inst, inst_slice, offset, id_offset, id_remap);
1091 }
1092 },
1093 }
1094 }
1095}
1096
1097fn remapOperand(
1098 kind: spec.OperandKind,
1099 cat: spec.OperandCategory,
1100 inst: BinaryModule.Instruction,
1101 inst_slice: []Word,
1102 offset: usize,
1103 id_offset: Word,
1104 id_remap: *const std.AutoHashMapUnmanaged(Id, Id),
1105) usize {
1106 switch (cat) {
1107 .id => {
1108 remapSingleId(&inst_slice[1 + offset], id_offset, id_remap);
1109 return 1;
1110 },
1111 .literal => return operandLiteralWordCount(kind, inst, offset),
1112 .composite => {
1113 remapCompositeOperand(kind, inst_slice, offset, id_offset, id_remap);
1114 return 2;
1115 },
1116 .bit_enum => {
1117 const mask = inst_slice[1 + offset];
1118 var consumed: usize = 1;
1119 for (kind.enumerants()) |e| {
1120 if ((mask & e.value) == 0) continue;
1121 for (e.parameters) |param_kind| {
1122 if (offset + consumed >= inst.operands.len) return consumed;
1123 consumed += remapOperand(
1124 param_kind,
1125 param_kind.category(),
1126 inst,
1127 inst_slice,
1128 offset + consumed,
1129 id_offset,
1130 id_remap,
1131 );
1132 }
1133 }
1134 return consumed;
1135 },
1136 .value_enum => {
1137 const value = inst_slice[1 + offset];
1138 var consumed: usize = 1;
1139 for (kind.enumerants()) |e| {
1140 if (e.value != value) continue;
1141 for (e.parameters) |param_kind| {
1142 if (offset + consumed >= inst.operands.len) return consumed;
1143 consumed += remapOperand(
1144 param_kind,
1145 param_kind.category(),
1146 inst,
1147 inst_slice,
1148 offset + consumed,
1149 id_offset,
1150 id_remap,
1151 );
1152 }
1153 break;
1154 }
1155 return consumed;
1156 },
1157 }
1158}
1159
1160fn remapCompositeOperand(
1161 kind: spec.OperandKind,
1162 inst_slice: []Word,
1163 offset: usize,
1164 id_offset: Word,
1165 id_remap: *const std.AutoHashMapUnmanaged(Id, Id),
1166) void {
1167 switch (kind) {
1168 .pair_literal_integer_id_ref => {
1169 remapSingleId(&inst_slice[1 + offset + 1], id_offset, id_remap);
1170 },
1171 .pair_id_ref_literal_integer => {
1172 remapSingleId(&inst_slice[1 + offset], id_offset, id_remap);
1173 },
1174 .pair_id_ref_id_ref => {
1175 remapSingleId(&inst_slice[1 + offset], id_offset, id_remap);
1176 remapSingleId(&inst_slice[1 + offset + 1], id_offset, id_remap);
1177 },
1178 else => {},
1179 }
1180}
1181
1182fn operandLiteralWordCount(kind: spec.OperandKind, inst: BinaryModule.Instruction, offset: usize) usize {
1183 return switch (kind) {
1184 .literal_integer, .literal_float => 1,
1185 .literal_string => blk: {
1186 var count: usize = 0;
1187 var off = offset;
1188 while (off < inst.operands.len) {
1189 const word = inst.operands[off];
1190 count += 1;
1191 off += 1;
1192 if (word & 0xFF000000 == 0 or
1193 word & 0x00FF0000 == 0 or
1194 word & 0x0000FF00 == 0 or
1195 word & 0x000000FF == 0)
1196 {
1197 break;
1198 }
1199 }
1200 break :blk count;
1201 },
1202 .literal_context_dependent_number => inst.operands.len - offset,
1203 .literal_ext_inst_integer => 1,
1204 else => 1,
1205 };
1206}
1207
1208fn remapSingleId(word: *Word, id_offset: Word, id_remap: *const std.AutoHashMapUnmanaged(Id, Id)) void {
1209 const id: Id = @fromBackingInt(@intCast(word.*));
1210 if (id == .none) return;
1211 if (id_remap.get(id)) |final_id| {
1212 word.* = @backingInt(final_id);
1213 } else {
1214 word.* = @backingInt(id) + id_offset;
1215 }
1216}