Commit 96a66d14a1

Robin Voetter <robin@voetter.nl>
2023-05-29 03:30:38
spirv: TypeConstantCache
1 parent 76aa1ff
Changed files (2)
src/codegen/spirv/Module.zig
@@ -125,6 +125,8 @@ sections: struct {
     // OpModuleProcessed - skip for now.
     /// Annotation instructions (OpDecorate etc).
     annotations: Section = .{},
+    /// Type and constant declarations that are generated by the TypeConstantCache.
+    types_and_constants: Section = .{},
     /// Type declarations, constants, global variables
     /// Below this section, OpLine and OpNoLine is allowed.
     types_globals_constants: Section = .{},
@@ -182,6 +184,7 @@ pub fn deinit(self: *Module) void {
     self.sections.debug_strings.deinit(self.gpa);
     self.sections.debug_names.deinit(self.gpa);
     self.sections.annotations.deinit(self.gpa);
+    self.sections.types_and_constants(self.gpa);
     self.sections.types_globals_constants.deinit(self.gpa);
     self.sections.functions.deinit(self.gpa);
 
@@ -334,6 +337,7 @@ pub fn flush(self: *Module, file: std.fs.File) !void {
         self.sections.debug_strings.toWords(),
         self.sections.debug_names.toWords(),
         self.sections.annotations.toWords(),
+        self.sections.types_constants.toWords(),
         self.sections.types_globals_constants.toWords(),
         globals.toWords(),
         self.sections.functions.toWords(),
@@ -883,3 +887,12 @@ pub fn declareEntryPoint(self: *Module, decl_index: Decl.Index, name: []const u8
         .name = try self.arena.dupe(u8, name),
     });
 }
+
+pub fn debugName(self: *Module, target: IdResult, comptime fmt: []const u8, args: anytype) !void {
+    const name = try std.fmt.allocPrint(self.gpa, fmt, args);
+    defer self.gpa.free(name);
+    try debug.emit(self.gpa, .OpName, .{
+        .target = result_id,
+        .name = name,
+    });
+}
src/codegen/spirv/TypeConstantCache.zig
@@ -0,0 +1,292 @@
+//! This file implements an InternPool-like structure that caches
+//! SPIR-V types and constants.
+//! In the case of SPIR-V, the type- and constant instructions
+//! describe the type and constant fully. This means we can save
+//! memory by representing these items directly in spir-v code,
+//! and decoding that when required.
+//! This does not work for OpDecorate instructions though, and for
+//! those we keep some additional metadata.
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const Section = @import("section.zig");
+const Module = @import("Module.zig");
+
+const spec = @import("spec.zig");
+const Opcode = spec.Opcode;
+const IdResult = spec.IdResult;
+
+const Self = @This();
+
+map: std.AutoArrayHashMapUnmanaged(void, void) = .{},
+items: std.MultiArrayList(Item) = .{},
+extra: std.ArrayHashMapUnmanaged(u32) = .{},
+
+const Item = struct {
+    tag: Tag,
+    /// The result-id that this item uses.
+    result_id: IdResult,
+    /// The Tag determines how this should be interpreted.
+    data: u32,
+};
+
+const Tag = enum {
+    /// Simple type that has no additional data.
+    /// data is SimpleType.
+    type_simple,
+    /// Signed integer type
+    /// data is number of bits
+    type_int_signed,
+    /// Unsigned integer type
+    /// data is number of bits
+    type_int_unsigned,
+    /// Floating point type
+    /// data is number of bits
+    type_float,
+    /// Vector type
+    /// data is payload to Key.VectorType
+    type_vector,
+
+    const SimpleType = enum {
+        void,
+        bool,
+    };
+};
+
+pub const Ref = enum(u32) { _ };
+
+/// This union represents something that can be interned. This includes
+/// types and constants. This structure is used for interfacing with the
+/// database: Values described for this structure are ephemeral and stored
+/// in a more memory-efficient manner internally.
+pub const Key = union(enum) {
+    void_ty,
+    bool_ty,
+    int_ty: IntType,
+    float_ty: FloatType,
+    vector_ty: VectorType,
+
+    pub const IntType = std.builtin.Type.Int;
+    pub const FloatType = std.builtin.Type.Float;
+
+    pub const VectorType = struct {
+        component_type: Ref,
+        component_count: u32,
+    };
+
+    fn hash(self: Key) u32 {
+        var hasher = std.hash.Wyhash.init(0);
+        std.hash.autoHash(&hasher, self);
+        return @truncate(u32, hasher.final());
+    }
+
+    fn eql(a: Key, b: Key) u32 {
+        return std.meta.eql(a, b);
+    }
+
+    pub const Adapter = struct {
+        self: *const Self,
+
+        pub fn eql(ctx: @This(), a: Key, b_void: void, b_map_index: u32) bool {
+            _ = b_void;
+            return ctx.self.lookup(@intToEnum(Ref, b_map_index)).eql(a);
+        }
+
+        pub fn hash(ctx: @This(), a: Key) u32 {
+            return ctx.self.hash(a);
+        }
+    };
+
+    fn toSimpleType(self: Key) Tag.SimpleType {
+        return switch (self) {
+            .void_ty => .void,
+            .bool_ty => .bool,
+            else => unreachable,
+        };
+    }
+};
+
+pub fn deinit(self: *Self, spv: Module) void {
+    self.map.deinit(spv.gpa);
+    self.items.deinit(spv.gpa);
+    self.extra.deinit(spv.gpa);
+}
+
+/// Actually materialize the database into spir-v instructions.
+// TODO: This should generate decorations as well as regular instructions.
+// Important is that these are generated in-order, but that should be fine.
+pub fn finalize(self: *Self, spv: *Module) !void {
+    // This function should really be the only one that modifies spv.types_and_constants.
+    // TODO: Make this function return the section instead.
+    std.debug.assert(spv.sections.types_and_constants.instructions.items.len == 0);
+
+    for (self.items.items(.result_id), 0..) |result_id, index| {
+        try self.emit(spv, result_id, @intToEnum(Ref, index));
+    }
+}
+
+fn emit(
+    self: *Self,
+    spv: *Module,
+    result_id: IdResult,
+    ref: Ref,
+) !void {
+    const tc = &spv.sections.types_and_constants;
+    const key = self.lookup(ref);
+    switch (key) {
+        .void_ty => {
+            try tc.emit(spv.gpa, .OpTypeVoid, .{ .id_result = result_id });
+            try spv.debugName(result_id, "void", .{});
+        },
+        .bool_ty => {
+            try tc.emit(spv.gpa, .OpTypeBool, .{ .id_result = result_id });
+            try spv.debugName(result_id, "bool", .{});
+        },
+        .int_ty => |int| {
+            try tc.emit(spv.gpa, .OpTypeInt, .{
+                .id_result = result_id,
+                .width = int.bits,
+                .signedness = switch (int.signedness) {
+                    .unsigned => 0,
+                    .signed => 1,
+                },
+            });
+            const ui: []const u8 = switch (int.signedness) {
+                0 => "u",
+                1 => "i",
+                else => unreachable,
+            };
+            try spv.debugName(result_id, "{s}{}", .{ ui, int.bits });
+        },
+        .float_ty => |float| {
+            try tc.emit(spv.gpa, .OpTypeFloat, .{
+                .id_result = result_id,
+                .width = float.bits,
+            });
+            try spv.debugName(result_id, "f{}", .{float.bits});
+        },
+        .vector_ty => |vector| {
+            try tc.emit(spv.gpa, .OpTypeVector, .{
+                .id_result = result_id,
+                .component_type = self.resultId(vector.component_type),
+                .component_count = vector.component_count,
+            });
+        },
+    }
+}
+
+/// Add a key to this cache. Returns a reference to the key that
+/// was added. The corresponding result-id can be queried using
+/// self.resultId with the result.
+pub fn add(self: *Self, spv: *Module, key: Key) !Ref {
+    const adapter: Key.Adapter = .{ .self = self };
+    const entry = try self.map.getOrPutAdapted(spv.gpa, key, adapter);
+    if (entry.found_existing) {
+        return @intToEnum(Ref, entry.index);
+    }
+    const result_id = spv.allocId();
+    try self.items.ensureUnusedCapacity(spv.gpa, 1);
+    switch (key) {
+        inline .void_ty, .bool_ty => {
+            self.items.appendAssumeCapacity(.{
+                .tag = .type_simple,
+                .result_id = result_id,
+                .data = @enumToInt(key.toSimpleType()),
+            });
+        },
+        .int_ty => |int| {
+            const t: Tag = switch (int.signedness) {
+                .signed => .type_int_signed,
+                .unsigned => .type_int_unsigned,
+            };
+            self.items.appendAssumeCapacity(.{
+                .tag = t,
+                .result_id = result_id,
+                .data = int.bits,
+            });
+        },
+        .float_ty => |float| {
+            self.items.appendAssumeCapacity(.{
+                .tag = .type_float,
+                .result_id = result_id,
+                .data = float.bits,
+            });
+        },
+        .vector_ty => |vec| {
+            const payload = try self.addExtra(vec);
+            self.items.appendAssumeCapacity(.{
+                .tag = .type_vector,
+                .result_id = result_id,
+                .data = payload,
+            });
+        },
+    }
+
+    return @intToEnum(Ref, entry.index);
+}
+
+/// Look op the result-id that corresponds to a particular
+/// ref.
+pub fn resultId(self: Self, ref: Ref) IdResult {
+    return self.items.items(.result_id)[@enumToInt(ref)];
+}
+
+/// Turn a Ref back into a Key.
+pub fn lookup(self: *const Self, ref: Ref) Key {
+    const item = self.items.get(@enumToInt(ref));
+    const data = item.data;
+    return switch (item.tag) {
+        .type_simple => switch (@intToEnum(Tag.SimpleType, data)) {
+            .void => .void_ty,
+            .bool => .bool_ty,
+        },
+        .type_int_signed => .{ .int_ty = .{
+            .signedness = .signed,
+            .bits = @intCast(u16, data),
+        } },
+        .type_int_unsigned => .{ .int_ty = .{
+            .signedness = .unsigned,
+            .bits = @intCast(u16, data),
+        } },
+        .type_float => .{ .float_ty = .{
+            .bits = @intCast(u16, data),
+        } },
+        .type_vector => .{
+            .vector_ty = self.extraData(Key.VectorType, data),
+        },
+    };
+}
+
+fn addExtra(self: *Self, gpa: Allocator, extra: anytype) !u32 {
+    const fields = @typeInfo(@TypeOf(extra)).Struct.fields;
+    try self.extra.ensureUnusedCapacity(gpa, fields.len);
+    try self.addExtraAssumeCapacity(extra);
+}
+
+fn addExtraAssumeCapacity(self: *Self, extra: anytype) !u32 {
+    const payload_offset = @intCast(u32, self.extra.items.len);
+    inline for (@typeInfo(@TypeOf(extra)).Struct.fields) |field| {
+        const field_val = @field(field, field.name);
+        const word = switch (field.type) {
+            u32 => field_val,
+            Ref => @enumToInt(field_val),
+            else => @compileError("Invalid type: " ++ @typeName(field.type)),
+        };
+        self.extra.appendAssumeCapacity(word);
+    }
+    return payload_offset;
+}
+
+fn extraData(self: Self, comptime T: type, offset: u32) T {
+    var result: T = undefined;
+    inline for (@typeInfo(T).Struct.fields, 0..) |field, i| {
+        const word = self.extra.items[offset + i];
+        @field(result, field.name) = switch (field.type) {
+            u32 => word,
+            Ref => @intToEnum(Ref, word),
+            else => @compileError("Invalid type: " ++ @typeName(field.type)),
+        };
+    }
+    return result;
+}