Skip to documentation
SLOP

tiny.choir.backends.gpu.cpu.stage

Reference tiny.choir backends gpu cpu stage

Defined in backends.gpu.cpu.

Lowers the vertex and fragment functions of a gpu-dialect module to host functions, so a CPU rasterizer runs the same stage code a GPU does.

API (3)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callsbackends.gpu.cpustage
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsNo direct callersprivate sourcelib.choir.src.backends.gpu.calls.Plandeinitprivate sourcelib.choir.src.backends.gpu.calls.Planinitprivate sourcelib.choir.src.backends.gpu.cpu.stageloweringErrordialects.ArithDialectgetScalarTypedialects.BuiltinDialect.ModuleOpcreate+2 morebackends.gpu.cpu.stagelowerStagesToHost
Static calls · unresolved targets: 0 · external targets: 9.

Source: lib/choir/src/backends/gpu/cpu/root.zig:2

zig
pub const stage = @import("stage.zig");

Source: lib/choir/src/backends/gpu/cpu/stage.zig

zig
//! Lowers the vertex and fragment functions of a gpu-dialect module to host functions, so a CPU//! rasterizer runs the same stage code a GPU does. Each stage function `name` becomes a host//! function `name(inputs, outputs, context)` over the words `choir_abi.stage` lays out://! - an interface read loads its words, and a write stores them;//! - a builtin loads its word;//! - a sample stores its coordinates to the sample words, calls `tiny_gpu_sample`, and loads the//!   texel it left there, because the sampler a texture is read through is chosen when the//!   texture is bound, after the code exists;//! - a push-constant or uniform read loads its member's words from the context. A uniform read//!   outside group 0, at a binding past `uniform_bindings`, or beyond `uniform_window_bytes` is//!   refused, as is a member the std140 rules of `gpu.stage.Member` do not place.//!//! A vertex function also stores, on entry, which smooth and flat words it writes.//!//! A fragment function that takes a derivative (`dpdx`, `dpdy`, `fwidth`) reads its quad, as//! `choir_abi.stage.quad_suffix` states. It recomputes the derivative's operand at the partner lane//! along each axis it differentiates: each op the operand depends on is lowered once more per//! axis over the partner's input block, when a stage input or `frag_coord` reaches it, and shared//! otherwise, however many derivatives read it. A fine derivative is then the odd lane's value//! minus the even lane's, chosen by two selects on the lane's parity, so it keeps the sign of zero//! a device's subtraction gives.//!//! The recomputation must not observe or change memory, so a derivative's operand may not depend//! on a memref op or on a call that passes or returns anything but scalars. A call on scalars is//! made again with the partner's operands, into the same helper. A derivative inside a region is//! refused, which keeps the quad's four lanes in step, as is a derivative whose operand depends on//! another derivative.const std = @import("std");const abi = @import("choir_abi");const choir = @import("../../../root.zig");const gpu = @import("../../../dialects/gpu/root.zig");const calls = @import("../calls.zig");const ir = choir.ir;const dialects = choir.dialects;const ArithDialect = dialects.ArithDialect;const BuiltinDialect = dialects.BuiltinDialect;const FuncDialect = dialects.FuncDialect;const GpuDialect = gpu.GpuDialect;const MemrefDialect = dialects.MemrefDialect;const Stage = gpu.Stage;const layout = abi.stage;pub const Error = abi.Error || gpu.stage.MemoryError || calls.SignatureError;/// Where a lowering stopped, for a caller that reports why.pub const Refusal = struct {    operation: []const u8 = "",    helper: []const u8 = "",};/// Lowers every stage function and its reachable ordinary callees into a host module the caller/// owns and erases. Kernels and unreachable functions are left out. A refused op names itself.pub fn lowerStagesToHost(    allocator: std.mem.Allocator,    module: *ir.Operation,    refusal: ?*Refusal,) Error!*ir.Operation {    const ctx = module.getContext();    const host_module = BuiltinDialect.ModuleOp.create(ctx, module.location) catch |err| return loweringError(err);    errdefer host_module.op.erase();    const body = ir.inspection.moduleBodyBlock(module) orelse return error.InvalidArtifact;    var plan_refused: []const u8 = "";    var call_plan = calls.Plan.init(allocator, module, &plan_refused) catch |err| switch (err) {        error.OutOfMemory => return error.OutOfMemory,        error.UnsupportedStageMemory => {            if (refusal) |why| why.operation = plan_refused;            return error.UnsupportedStageMemory;        },        error.UnsupportedHelperSignature => {            if (refusal) |why| {                why.operation = FuncDialect.FuncOp.operation_name;                why.helper = plan_refused;            }            return error.UnsupportedHelperSignature;        },        error.UnsupportedOperation => {            if (refusal) |why| why.operation = FuncDialect.CallOp.operation_name;            return error.UnsupportedOperation;        },    };    defer call_plan.deinit();    var lowerer = Lowerer{        .allocator = allocator,        .ctx = ctx,        .host_body = host_module.getBodyBlock(),        .refusal = refusal,        .f32_type = ArithDialect.getScalarType(ctx, .f32) catch |err| return loweringError(err),        .io_type = undefined,        .context_type = undefined,    };    lowerer.io_type = MemrefDialect.getMemrefTypeDynamic(ctx, lowerer.f32_type, .host) catch |err| return loweringError(err);    lowerer.context_type = lowerer.io_type;    for (call_plan.helpers.items) |helper| try lowerer.lowerHelperFunction(FuncDialect.FuncOp{ .op = helper });    var stages: u32 = 0;    var ops = body.getOperations();    while (ops.next()) |op| {        if (!std.mem.eql(u8, op.name.name, FuncDialect.FuncOp.operation_name)) continue;        const stage = gpu.stage.stageOf(op) orelse continue;        try lowerer.lowerFunction(FuncDialect.FuncOp{ .op = op }, stage);        stages += 1;    }    if (stages == 0) return error.InvalidArtifact;    ir.verifyOperation(host_module.op, ir.verify.default_options) catch return error.CompilationFailed;    return host_module.op;}const Lowerer = struct {    allocator: std.mem.Allocator,    ctx: *ir.Context,    host_body: *ir.Block,    refusal: ?*Refusal,    f32_type: ir.Type,    io_type: ir.Type,    context_type: ir.Type,    sample_declared: bool = false,    /// The current function's buffers and context.    inputs: *ir.Value = undefined,    outputs: *ir.Value = undefined,    context: *ir.Value = undefined,    stage: Stage = .vertex,    helper_body: bool = false,    loc: ir.Location = .unknown,    /// For a quad function, the word of `inputs` its input reads start at: the lane's own block,    /// or a partner's while an op is lowered again over it.    base: ?*ir.Value = null,    /// The axis whose partner the lowering reads, while it lowers an op again.    partner: ?Axis = null,    /// Per axis of a quad function: the partner block's first word, whether the lane is odd along    /// the axis, and each needed value as the partner computes it.    partners: [2]Partner = undefined,    /// Ops lowered again for a partner, per axis, across every function.    partner_ops: [2]u32 = .{ 0, 0 },    fn lowerFunction(self: *Lowerer, source: FuncDialect.FuncOp, stage: Stage) Error!void {        if (source.getArguments().len != 0 or source.getNumResults() != 0) return self.refuse(source.op);        const name = source.getName() orelse return error.InvalidArtifact;        const body = source.getEntryBlock();        var needs = Needs.init(self.allocator);        defer needs.deinit();        if (stage == .fragment) try needs.collect(body);        if (needs.axes != 0) return self.lowerQuadFunction(source, name, &needs);        const host = FuncDialect.FuncOp.create(            self.ctx,            source.op.location,            name,            &.{ self.io_type, self.io_type, self.context_type },            &.{},        ) catch |err| return loweringError(err);        self.host_body.addOperation(host.op) catch |err| return loweringError(err);        const arguments = host.getArguments();        self.inputs = arguments[0];        self.outputs = arguments[1];        self.context = arguments[2];        self.stage = stage;        self.helper_body = false;        self.loc = source.op.location;        self.base = null;        const entry = host.getEntryBlock();        if (stage == .vertex) try self.storeMasks(entry, body);        var mapping = ir.Mapping.init(self.allocator);        defer mapping.deinit();        try self.cloneBlock(body, entry, &mapping, true);    }    /// Lowers a fragment function that takes derivatives to `name ++ quad_suffix` over its quad.    /// After each op it needs for an axis, it lowers the op again over that axis's partner block    /// when the op varies across the quad, and maps it to its own values otherwise.    fn lowerQuadFunction(self: *Lowerer, source: FuncDialect.FuncOp, name: []const u8, needs: *const Needs) Error!void {        var name_buffer: [max_symbol_bytes]u8 = undefined;        const symbol = std.fmt.bufPrint(&name_buffer, "{s}{s}", .{ name, layout.quad_suffix }) catch return self.refuse(source.op);        const index_type = ArithDialect.getIndexType(self.ctx) catch |err| return loweringError(err);        const host = FuncDialect.FuncOp.create(            self.ctx,            source.op.location,            symbol,            &.{ self.io_type, index_type, self.io_type, self.context_type },            &.{},        ) catch |err| return loweringError(err);        self.host_body.addOperation(host.op) catch |err| return loweringError(err);        const arguments = host.getArguments();        self.inputs = arguments[0];        self.outputs = arguments[2];        self.context = arguments[3];        self.stage = .fragment;        self.helper_body = false;        self.loc = source.op.location;        const entry = host.getEntryBlock();        const lane = arguments[1];        const block_words = try self.indexConstant(entry, layout.io_words);        self.base = try self.binary(entry, ArithDialect.MulOp, lane, block_words);        var mappings: [2]ir.Mapping = .{ ir.Mapping.init(self.allocator), ir.Mapping.init(self.allocator) };        defer for (&mappings) |*partner_mapping| partner_mapping.deinit();        for (&self.partners, &mappings, [_]Axis{ .x, .y }) |*partner, *partner_mapping, axis| {            partner.mapping = partner_mapping;            if (needs.axes & Needs.bit(axis) == 0) continue;            const bit = try self.indexConstant(entry, Needs.bit(axis));            const zero = try self.indexConstant(entry, 0);            const along = try self.binary(entry, ArithDialect.AndOp, lane, bit);            const odd = ArithDialect.CmpOp.create(self.ctx, self.loc, .ne, along, zero) catch |err| return loweringError(err);            try add(entry, odd.op);            partner.odd = odd.getResult();            const partner_lane = try self.binary(entry, ArithDialect.XorOp, lane, bit);            partner.base = try self.binary(entry, ArithDialect.MulOp, partner_lane, block_words);        }        var mapping = ir.Mapping.init(self.allocator);        defer mapping.deinit();        var ops = source.getEntryBlock().getOperations();        while (ops.next()) |op| {            try self.lowerOperation(op, entry, &mapping, true);            const axes = needs.of(op);            for ([_]Axis{ .x, .y }) |axis| {                if (axes & Needs.bit(axis) != 0) try self.lowerForPartner(op, entry, &mapping, axis);            }        }    }    /// Maps `op`'s results in the partner mapping of `axis`: lowered again over the partner's    /// block when a stage input or `frag_coord` reaches it, or its own lowering otherwise.    fn lowerForPartner(self: *Lowerer, op: *ir.Operation, dest: *ir.Block, mapping: *ir.Mapping, axis: Axis) Error!void {        const partner = &self.partners[@backingInt(axis)];        if (isDerivative(op.name.name) or touchesMemory(op)) return self.refuse(op);        if (!self.varies(op, mapping, partner.mapping)) {            for (0..op.getNumResults()) |index| {                const result = op.getResult(index) orelse return error.InvalidArtifact;                try mapValue(partner.mapping, result, mapping.lookupValue(result) orelse return error.InvalidArtifact);            }            return;        }        const own_base = self.base;        self.base = partner.base;        self.partner = axis;        defer {            self.base = own_base;            self.partner = null;        }        try self.lowerOperation(op, dest, partner.mapping, false);        self.partner_ops[@backingInt(axis)] += 1;    }    /// Whether `op` computes a different value at a partner lane: it reads a stage input or    /// `frag_coord`, holds regions, or takes an operand the partner computes differently.    fn varies(self: *Lowerer, op: *ir.Operation, mapping: *ir.Mapping, partner_mapping: *ir.Mapping) bool {        _ = self;        const name = op.name.name;        if (std.mem.eql(u8, name, GpuDialect.StageInputOp.operation_name)) return true;        if (std.mem.eql(u8, name, GpuDialect.FragCoordOp.operation_name)) return true;        if (op.regions.items.len != 0) return true;        for (op.operand_values) |operand| {            if (partner_mapping.lookupValue(operand) != mapping.lookupValue(operand)) return true;        }        return false;    }    fn lowerDerivative(self: *Lowerer, op: *ir.Operation, dest: *ir.Block, mapping: *ir.Mapping) Error!void {        const value = op.operand_values[0];        const name = op.name.name;        const result = if (std.mem.eql(u8, name, GpuDialect.DpdxOp.operation_name))            try self.difference(dest, mapping, value, .x)        else if (std.mem.eql(u8, name, GpuDialect.DpdyOp.operation_name))            try self.difference(dest, mapping, value, .y)        else blk: {            const along_x = try self.unary(dest, ArithDialect.AbsOp, try self.difference(dest, mapping, value, .x));            const along_y = try self.unary(dest, ArithDialect.AbsOp, try self.difference(dest, mapping, value, .y));            break :blk try self.binary(dest, ArithDialect.AddOp, along_x, along_y);        };        try mapValue(mapping, op.getResult(0) orelse return error.InvalidArtifact, result);    }    /// The fine derivative of `value` along `axis`: the odd lane's value minus the even lane's.    fn difference(self: *Lowerer, dest: *ir.Block, mapping: *ir.Mapping, value: *ir.Value, axis: Axis) Error!*ir.Value {        const partner = &self.partners[@backingInt(axis)];        const own = mapping.lookupValue(value) orelse return error.InvalidArtifact;        const theirs = partner.mapping.lookupValue(value) orelse return error.InvalidArtifact;        const odd = try self.select(dest, partner.odd, own, theirs);        const even = try self.select(dest, partner.odd, theirs, own);        return self.binary(dest, ArithDialect.SubOp, odd, even);    }    fn select(self: *Lowerer, dest: *ir.Block, condition: *ir.Value, if_true: *ir.Value, if_false: *ir.Value) Error!*ir.Value {        const op = ArithDialect.SelectOp.create(self.ctx, self.loc, condition, if_true, if_false) catch |err| return loweringError(err);        try add(dest, op.op);        return op.getResult();    }    fn binary(self: *Lowerer, dest: *ir.Block, comptime Op: type, lhs: *ir.Value, rhs: *ir.Value) Error!*ir.Value {        const op = Op.create(self.ctx, self.loc, lhs, rhs) catch |err| return loweringError(err);        try add(dest, op.op);        return op.getResult();    }    fn unary(self: *Lowerer, dest: *ir.Block, comptime Op: type, operand: *ir.Value) Error!*ir.Value {        const op = Op.create(self.ctx, self.loc, operand) catch |err| return loweringError(err);        try add(dest, op.op);        return op.getResult();    }    fn lowerHelperFunction(self: *Lowerer, source: FuncDialect.FuncOp) Error!void {        const name = source.getName() orelse return error.InvalidArtifact;        const host = FuncDialect.FuncOp.create(            self.ctx,            source.op.location,            name,            source.getInputTypes() orelse return error.InvalidArtifact,            source.getResultTypes(),        ) catch |err| return loweringError(err);        self.host_body.addOperation(host.op) catch |err| return loweringError(err);        self.helper_body = true;        self.base = null;        self.loc = source.op.location;        var mapping = ir.Mapping.init(self.allocator);        defer mapping.deinit();        for (source.getArguments(), host.getArguments()) |arg, mapped| try mapValue(&mapping, arg, mapped);        try self.cloneBlock(source.getEntryBlock(), host.getEntryBlock(), &mapping, true);    }    /// Stores the masks of smooth and flat words the function's interface writes reach.    fn storeMasks(self: *Lowerer, dest: *ir.Block, source: *ir.Block) Error!void {        var masks = Masks{};        try masks.collect(source);        try self.storeBits(dest, @truncate(masks.smooth), layout.smooth_mask_low);        try self.storeBits(dest, @truncate(masks.smooth >> 32), layout.smooth_mask_high);        try self.storeBits(dest, @truncate(masks.flat), layout.flat_mask_low);        try self.storeBits(dest, @truncate(masks.flat >> 32), layout.flat_mask_high);    }    fn storeBits(self: *Lowerer, dest: *ir.Block, bits: u32, word: u32) Error!void {        const u32_type = ArithDialect.getScalarType(self.ctx, .u32) catch |err| return loweringError(err);        const constant = ArithDialect.ConstantOp.createInt(self.ctx, self.loc, u32_type, bits) catch |err| return loweringError(err);        try add(dest, constant.op);        try self.storeWord(dest, constant.getResult(), self.outputs, word);    }    fn cloneBlock(self: *Lowerer, source: *ir.Block, dest: *ir.Block, mapping: *ir.Mapping, function_body: bool) Error!void {        var ops = source.getOperations();        while (ops.next()) |op| try self.lowerOperation(op, dest, mapping, function_body);    }    fn lowerOperation(self: *Lowerer, op: *ir.Operation, dest: *ir.Block, mapping: *ir.Mapping, function_body: bool) Error!void {        const name = op.name.name;        if (self.partner != null and touchesMemory(op)) return self.refuse(op);        if (isDerivative(name)) {            if (!function_body or self.partner != null or self.base == null) return self.refuse(op);            try self.lowerDerivative(op, dest, mapping);        } else if (std.mem.eql(u8, name, FuncDialect.ReturnOp.operation_name)) {            if (!function_body or (!self.helper_body and op.operand_values.len != 0)) return self.refuse(op);            const operands = self.allocator.alloc(*ir.Value, op.operand_values.len) catch return error.OutOfMemory;            defer self.allocator.free(operands);            for (op.operand_values, operands) |operand, *mapped| mapped.* = mapping.lookupOrDefaultValue(operand);            const ret = FuncDialect.ReturnOp.create(self.ctx, op.location, operands) catch |err| return loweringError(err);            try add(dest, ret.op);        } else if (std.mem.eql(u8, name, GpuDialect.StageInputOp.operation_name)) {            try self.lowerInput(op, dest, mapping);        } else if (std.mem.eql(u8, name, GpuDialect.StageOutputOp.operation_name)) {            try self.lowerOutput(op, dest, mapping);        } else if (std.mem.eql(u8, name, GpuDialect.PositionOp.operation_name)) {            for (op.operand_values, 0..) |operand, component| {                try self.storeWord(dest, mapping.lookupOrDefaultValue(operand), self.outputs, layout.position + @as(u32, @intCast(component)));            }        } else if (std.mem.eql(u8, name, GpuDialect.FragCoordOp.operation_name)) {            for (0..4) |component| {                const result = op.getResult(component) orelse return error.InvalidArtifact;                const value = try self.loadInput(dest, layout.frag_coord + @as(u32, @intCast(component)), result.type);                try mapValue(mapping, result, value);            }        } else if (std.mem.eql(u8, name, GpuDialect.VertexIndexOp.operation_name)) {            try self.lowerBuiltin(op, dest, mapping, layout.vertex_index);        } else if (std.mem.eql(u8, name, GpuDialect.InstanceIndexOp.operation_name)) {            try self.lowerBuiltin(op, dest, mapping, layout.instance_index);        } else if (std.mem.eql(u8, name, GpuDialect.FrontFacingOp.operation_name)) {            try self.lowerFrontFacing(op, dest, mapping);        } else if (std.mem.eql(u8, name, GpuDialect.SampledTextureOp.operation_name)) {            try self.lowerTexture(op, dest, mapping);        } else if (std.mem.eql(u8, name, MemrefDialect.AllocaOp.operation_name)) {            const source_alloca = MemrefDialect.AllocaOp{ .op = op };            const params = MemrefDialect.parseMemrefParams(                source_alloca.getResult().type.getDialectParamKey() orelse return self.refuse(op),            ) orelse return self.refuse(op);            if (params.addr_space != .local or source_alloca.getDynamicSize() != null) return self.refuse(op);            const size = params.size orelse return self.refuse(op);            if (size == 0) return self.refuse(op);            const elem = self.ctx.getDialectTypeFromName(params.element_type_name) catch return self.refuse(op);            const host_type = MemrefDialect.getMemrefType1DWithAttrs(self.ctx, size, elem, .host, .{                .alignment = params.alignment,                .exclusive = params.exclusive,                .indexing = params.indexing,            }) catch |err| return loweringError(err);            const host_alloca = MemrefDialect.AllocaOp.createStatic(self.ctx, op.location, host_type) catch |err| return loweringError(err);            try add(dest, host_alloca.op);            try mapValue(mapping, source_alloca.getResult(), host_alloca.getResult());        } else if (std.mem.eql(u8, name, GpuDialect.PushConstantOp.operation_name)) {            const member = (GpuDialect.PushConstantOp{ .op = op }).member() orelse return error.InvalidArtifact;            if (!member.placed(layout.push_words * 4)) return self.refuse(op);            try self.lowerBlockRead(op, dest, mapping, layout.push + member.offset / 4);        } else if (std.mem.eql(u8, name, GpuDialect.UniformOp.operation_name)) {            const uniform = GpuDialect.UniformOp{ .op = op };            const member = uniform.member() orelse return error.InvalidArtifact;            const group = uniform.getGroup() orelse return error.InvalidArtifact;            const binding = uniform.getBinding() orelse return error.InvalidArtifact;            if (group != 0 or binding >= layout.uniform_bindings) return self.refuse(op);            if (!member.placed(layout.uniform_window_bytes)) return self.refuse(op);            try self.lowerBlockRead(op, dest, mapping, layout.uniformWord(binding, member.offset));        } else if (std.mem.eql(u8, name, GpuDialect.SampleOp.operation_name) or            std.mem.eql(u8, name, GpuDialect.SampleLodOp.operation_name))        {            try self.lowerSample(op, dest, mapping);        } else if (std.mem.eql(u8, op.name.getDialectNamespace(), GpuDialect.name) or op.successors.items.len != 0) {            return self.refuse(op);        } else {            try self.cloneOperation(op, dest, mapping);        }    }    fn cloneOperation(self: *Lowerer, op: *ir.Operation, dest: *ir.Block, mapping: *ir.Mapping) Error!void {        const has_regions = op.regions.items.len != 0;        const cloned = op.cloneWithoutRegionsMapped(mapping, .{ .clone_operands = !has_regions }) catch |err| return loweringError(err);        errdefer cloned.erase();        try add(dest, cloned);        for (op.regions.items, 0..) |*source_region, region_index| {            const dest_region = &cloned.regions.items[region_index];            var source_block = source_region.blocks.head;            while (source_block) |block| : (source_block = block.next) {                const cloned_block = dest_region.addBlock() catch |err| return loweringError(err);                mapping.mapBlock(block, cloned_block) catch return error.OutOfMemory;                for (block.arguments.items) |argument| {                    const cloned_argument = cloned_block.addArgument(argument.type, .unknown) catch |err| return loweringError(err);                    try mapValue(mapping, argument, cloned_argument);                }            }            source_block = source_region.blocks.head;            while (source_block) |block| : (source_block = block.next) {                const cloned_block = mapping.lookupBlock(block) orelse return error.InvalidArtifact;                try self.cloneBlock(block, cloned_block, mapping, false);            }        }        if (has_regions) {            var operands: [max_region_operands]*ir.Value = undefined;            if (op.operand_values.len > operands.len) return self.refuse(op);            for (op.operand_values, operands[0..op.operand_values.len]) |operand, *mapped| {                mapped.* = mapping.lookupOrDefaultValue(operand);            }            cloned.replaceOperands(operands[0..op.operand_values.len]) catch |err| return loweringError(err);        }    }    fn lowerInput(self: *Lowerer, op: *ir.Operation, dest: *ir.Block, mapping: *ir.Mapping) Error!void {        const location = (GpuDialect.StageInputOp{ .op = op }).getLocation() orelse return error.InvalidArtifact;        for (0..op.getNumResults()) |component| {            const result = op.getResult(component) orelse return error.InvalidArtifact;            const region = switch (self.stage) {                .vertex => layout.smooth,                .fragment => try self.regionOf(op, result.type),            };            const word = layout.slot(region, location, @intCast(component));            try mapValue(mapping, result, try self.loadInput(dest, word, result.type));        }    }    fn lowerOutput(self: *Lowerer, op: *ir.Operation, dest: *ir.Block, mapping: *ir.Mapping) Error!void {        const location = (GpuDialect.StageOutputOp{ .op = op }).getLocation() orelse return error.InvalidArtifact;        for (op.operand_values, 0..) |operand, component| {            const region = switch (self.stage) {                .vertex => try self.regionOf(op, operand.type),                .fragment => layout.smooth,            };            const word = layout.slot(region, location, @intCast(component));            try self.storeWord(dest, mapping.lookupOrDefaultValue(operand), self.outputs, word);        }    }    /// Loads each result of a block read from consecutive context words from `first` on. Every    /// result must share one 4-byte scalar type.    fn lowerBlockRead(self: *Lowerer, op: *ir.Operation, dest: *ir.Block, mapping: *ir.Mapping, first: u32) Error!void {        const kind = scalarKind(op.getResult(0).?.type) orelse return self.refuse(op);        switch (kind) {            .f32, .u32, .i32 => {},            else => return self.refuse(op),        }        for (0..op.getNumResults()) |component| {            const result = op.getResult(component) orelse return error.InvalidArtifact;            if (scalarKind(result.type) != kind) return self.refuse(op);            const word = first + @as(u32, @intCast(component));            std.debug.assert(word < layout.context_words);            try mapValue(mapping, result, try self.loadFrom(dest, self.context, word, result.type));        }    }    fn lowerBuiltin(self: *Lowerer, op: *ir.Operation, dest: *ir.Block, mapping: *ir.Mapping, word: u32) Error!void {        const result = op.getResult(0) orelse return error.InvalidArtifact;        try mapValue(mapping, result, try self.loadInput(dest, word, result.type));    }    fn lowerFrontFacing(self: *Lowerer, op: *ir.Operation, dest: *ir.Block, mapping: *ir.Mapping) Error!void {        const result = op.getResult(0) orelse return error.InvalidArtifact;        const u32_type = ArithDialect.getScalarType(self.ctx, .u32) catch |err| return loweringError(err);        const bits = try self.loadInput(dest, layout.front_facing, u32_type);        const zero = ArithDialect.ConstantOp.createInt(self.ctx, op.location, u32_type, 0) catch |err| return loweringError(err);        try add(dest, zero.op);        const facing = ArithDialect.CmpOp.create(self.ctx, op.location, .ne, bits, zero.getResult()) catch |err| return loweringError(err);        try add(dest, facing.op);        try mapValue(mapping, result, facing.getResult());    }    /// A texture becomes the key a sample passes, since the host resolves the binding.    fn lowerTexture(self: *Lowerer, op: *ir.Operation, dest: *ir.Block, mapping: *ir.Mapping) Error!void {        const texture = GpuDialect.SampledTextureOp{ .op = op };        const group = texture.getGroup() orelse return error.InvalidArtifact;        const binding = texture.getBinding() orelse return error.InvalidArtifact;        if (group > 0xff or binding > 0xffff) return self.refuse(op);        const i32_type = ArithDialect.getScalarType(self.ctx, .i32) catch |err| return loweringError(err);        const key = ArithDialect.ConstantOp.createInt(self.ctx, op.location, i32_type, layout.textureKey(group, binding)) catch |err| return loweringError(err);        try add(dest, key.op);        try mapValue(mapping, texture.getResult(), key.getResult());    }    fn lowerSample(self: *Lowerer, op: *ir.Operation, dest: *ir.Block, mapping: *ir.Mapping) Error!void {        const operands = op.operand_values;        if (operands.len != 3 and operands.len != 4) return error.InvalidArtifact;        const key = mapping.lookupValue(operands[0]) orelse return self.refuse(op);        try self.storeWord(dest, mapping.lookupOrDefaultValue(operands[1]), self.outputs, layout.sample);        try self.storeWord(dest, mapping.lookupOrDefaultValue(operands[2]), self.outputs, layout.sample + 1);        const lod = if (operands.len == 4) mapping.lookupOrDefaultValue(operands[3]) else blk: {            const zero = ArithDialect.ConstantOp.createFloat(self.ctx, op.location, self.f32_type, 0) catch |err| return loweringError(err);            try add(dest, zero.op);            break :blk zero.getResult();        };        try self.storeWord(dest, lod, self.outputs, layout.sample + 2);        try self.declareSample(key.type);        const call = FuncDialect.CallOp.create(self.ctx, op.location, layout.sample_symbol, &.{ self.context, key, self.outputs }, &.{}) catch |err| return loweringError(err);        try add(dest, call.op);        for (0..4) |component| {            const result = op.getResult(component) orelse return error.InvalidArtifact;            try mapValue(mapping, result, try self.loadWord(dest, self.outputs, layout.sample + @as(u32, @intCast(component)), result.type));        }    }    fn declareSample(self: *Lowerer, key_type: ir.Type) Error!void {        if (self.sample_declared) return;        const declaration = FuncDialect.FuncOp.createDeclaration(            self.ctx,            self.loc,            layout.sample_symbol,            &.{ self.context_type, key_type, self.io_type },            &.{},        ) catch |err| return loweringError(err);        try add(self.host_body, declaration.op);        self.sample_declared = true;    }    /// The region an interface value travels in between the stages: smooth for a float, flat for    /// an integer.    fn regionOf(self: *Lowerer, op: *ir.Operation, typ: ir.Type) Error!u32 {        return switch (scalarKind(typ) orelse return self.refuse(op)) {            .f32 => layout.smooth,            .u32, .i32 => layout.flat,            else => self.refuse(op),        };    }    fn loadWord(self: *Lowerer, dest: *ir.Block, buffer: *ir.Value, word: u32, result_type: ir.Type) Error!*ir.Value {        std.debug.assert(word < layout.io_words);        return self.loadFrom(dest, buffer, word, result_type);    }    /// Loads `word` of the input block the lowering reads: the only one, or a lane's of a quad.    fn loadInput(self: *Lowerer, dest: *ir.Block, word: u32, result_type: ir.Type) Error!*ir.Value {        std.debug.assert(word < layout.io_words);        const base = self.base orelse return self.loadFrom(dest, self.inputs, word, result_type);        const index = try self.binary(dest, ArithDialect.AddOp, base, try self.indexConstant(dest, word));        return self.loadAt(dest, self.inputs, index, result_type);    }    /// Loads `word` of `buffer` as `result_type`, bitcasting a word that holds an integer.    fn loadFrom(self: *Lowerer, dest: *ir.Block, buffer: *ir.Value, word: u32, result_type: ir.Type) Error!*ir.Value {        return self.loadAt(dest, buffer, try self.indexConstant(dest, word), result_type);    }    fn loadAt(self: *Lowerer, dest: *ir.Block, buffer: *ir.Value, index: *ir.Value, result_type: ir.Type) Error!*ir.Value {        const load = MemrefDialect.LoadOp.create(self.ctx, self.loc, buffer, index, self.f32_type) catch |err| return loweringError(err);        try add(dest, load.op);        const loaded = load.op.getResult(0) orelse return error.CompilationFailed;        if (scalarKind(result_type) == .f32) return loaded;        const cast = ArithDialect.BitcastOp.create(self.ctx, self.loc, loaded, result_type) catch |err| return loweringError(err);        try add(dest, cast.op);        return cast.getResult();    }    fn storeWord(self: *Lowerer, dest: *ir.Block, value: *ir.Value, buffer: *ir.Value, word: u32) Error!void {        std.debug.assert(word < layout.io_words);        const stored = switch (scalarKind(value.type) orelse return error.UnsupportedOperation) {            .f32 => value,            .u32, .i32 => blk: {                const cast = ArithDialect.BitcastOp.create(self.ctx, self.loc, value, self.f32_type) catch |err| return loweringError(err);                try add(dest, cast.op);                break :blk cast.getResult();            },            else => return error.UnsupportedOperation,        };        const index = try self.indexConstant(dest, word);        const store = MemrefDialect.StoreOp.create(self.ctx, self.loc, stored, buffer, index) catch |err| return loweringError(err);        try add(dest, store.op);    }    fn indexConstant(self: *Lowerer, dest: *ir.Block, value: u32) Error!*ir.Value {        const index_type = ArithDialect.getIndexType(self.ctx) catch |err| return loweringError(err);        const constant = ArithDialect.ConstantOp.createInt(self.ctx, self.loc, index_type, value) catch |err| return loweringError(err);        try add(dest, constant.op);        return constant.getResult();    }    fn refuse(self: *Lowerer, op: *ir.Operation) Error {        if (self.refusal) |refusal| refusal.operation = op.name.name;        return error.UnsupportedOperation;    }};/// Operands an op with regions may carry, such as the condition of an `scf.if`.const max_region_operands = 16;/// Bytes a quad function's symbol may take.const max_symbol_bytes = 256;const Axis = enum(u1) { x, y };const Partner = struct {    base: *ir.Value = undefined,    odd: *ir.Value = undefined,    mapping: *ir.Mapping = undefined,};/// Whether `op` may read or write memory whose contents its operands do not carry: a memref op,/// or a call that passes or returns anything but scalars. `calls.Plan` admits a helper only when it/// reads no stage state and touches only its own local arrays, which live for one call, so a/// partner may call one again on its own scalars.fn touchesMemory(op: *ir.Operation) bool {    if (std.mem.eql(u8, op.name.getDialectNamespace(), MemrefDialect.name)) return true;    if (!std.mem.eql(u8, op.name.name, FuncDialect.CallOp.operation_name)) return false;    for (op.operand_values) |operand| {        if (scalarKind(operand.type) == null) return true;    }    for (0..op.getNumResults()) |index| {        const result = op.getResult(index) orelse return true;        if (scalarKind(result.type) == null) return true;    }    return false;}fn isDerivative(name: []const u8) bool {    return std.mem.eql(u8, name, GpuDialect.DpdxOp.operation_name) or        std.mem.eql(u8, name, GpuDialect.DpdyOp.operation_name) or        std.mem.eql(u8, name, GpuDialect.FwidthOp.operation_name);}/// The axes along which some derivative of a fragment body reads each op of the body's block:/// the op computes the derivative's operand, or a value that operand depends on.const Needs = struct {    allocator: std.mem.Allocator,    axes_of: std.AutoHashMapUnmanaged(*ir.Operation, u2) = .empty,    /// Every axis some derivative takes.    axes: u2 = 0,    fn init(allocator: std.mem.Allocator) Needs {        return .{ .allocator = allocator };    }    fn deinit(self: *Needs) void {        self.axes_of.deinit(self.allocator);    }    fn bit(axis: Axis) u2 {        return @as(u2, 1) << @backingInt(axis);    }    fn of(self: *const Needs, op: *ir.Operation) u2 {        return self.axes_of.get(op) orelse 0;    }    /// Walks `block` from its last op to its first, so an op's marks are complete before it    /// passes them to the ops it reads. Only derivatives in `block` itself seed marks; one in a    /// region is refused when it is lowered.    fn collect(self: *Needs, block: *ir.Block) Error!void {        var cursor = block.operations.tail;        while (cursor) |pointer| {            const op: *ir.Operation = @ptrCast(@alignCast(pointer));            cursor = op.prev_op;            const name = op.name.name;            const seed: u2 = if (std.mem.eql(u8, name, GpuDialect.DpdxOp.operation_name))                bit(.x)            else if (std.mem.eql(u8, name, GpuDialect.DpdyOp.operation_name))                bit(.y)            else if (std.mem.eql(u8, name, GpuDialect.FwidthOp.operation_name))                bit(.x) | bit(.y)            else                0;            self.axes |= seed;            const axes = self.of(op) | seed;            if (axes == 0) continue;            for (op.operand_values) |operand| try self.mark(operand, block, axes);            if (op.regions.items.len == 0) continue;            var reads = Reads{ .needs = self, .block = block, .axes = axes };            _ = op.walk(.{ .order = .pre_order }, &reads, Reads.visit) catch |err| return loweringError(err);        }    }    /// Marks the op of `block` that defines `value`, if any, with `axes`.    fn mark(self: *Needs, value: *ir.Value, block: *ir.Block, axes: u2) Error!void {        const pointer = value.getDefiningOp() orelse return;        const op: *ir.Operation = @ptrCast(@alignCast(pointer));        if (op.parent_block != block) return;        const entry = self.axes_of.getOrPut(self.allocator, op) catch return error.OutOfMemory;        if (!entry.found_existing) entry.value_ptr.* = 0;        entry.value_ptr.* |= axes;    }    /// Marks the values of the outer block that ops inside a region read.    const Reads = struct {        needs: *Needs,        block: *ir.Block,        axes: u2,        fn visit(reads: *Reads, op: *ir.Operation) !ir.Operation.WalkResult {            for (op.operand_values) |operand| try reads.needs.mark(operand, reads.block, reads.axes);            return .advance;        }    };};/// The smooth and flat words a vertex function's outputs reach, one bit per word.const Masks = struct {    smooth: u64 = 0,    flat: u64 = 0,    fn collect(self: *Masks, block: *ir.Block) Error!void {        var ops = block.getOperations();        while (ops.next()) |op| {            if (std.mem.eql(u8, op.name.name, GpuDialect.StageOutputOp.operation_name)) {                const location = (GpuDialect.StageOutputOp{ .op = op }).getLocation() orelse return error.InvalidArtifact;                for (op.operand_values, 0..) |operand, component| {                    const bit = @as(u64, 1) << @intCast(location * layout.location_components + component);                    switch (scalarKind(operand.type) orelse return error.UnsupportedOperation) {                        .f32 => self.smooth |= bit,                        .u32, .i32 => self.flat |= bit,                        else => return error.UnsupportedOperation,                    }                }            }            for (op.regions.items) |*region| {                var nested = region.blocks.head;                while (nested) |inner| : (nested = inner.next) try self.collect(inner);            }        }    }};fn scalarKind(typ: ir.Type) ?dialects.arith.ScalarKind {    const type_name = typ.getDialectTypeName() orelse return null;    return dialects.arith.scalarKindFromTypeName(type_name);}fn mapValue(mapping: *ir.Mapping, from: *ir.Value, to: *ir.Value) Error!void {    mapping.mapValue(from, to) catch return error.OutOfMemory;}fn add(block: *ir.Block, op: *ir.Operation) Error!void {    block.addOperation(op) catch |err| return loweringError(err);}fn loweringError(err: anyerror) Error {    return switch (err) {        error.OutOfMemory => error.OutOfMemory,        else => error.CompilationFailed,    };}

Complete call list for backends.gpu.cpu.stage.lowerStagesToHost

7 direct calls.

Audit

Definitions4
Public names6
Members2
Version26.7.0
Revisiondaab053ee433