diff --git a/src/asahi/compiler/agx_compile.c b/src/asahi/compiler/agx_compile.c index 8b857402dc1..7ab8fd9960a 100644 --- a/src/asahi/compiler/agx_compile.c +++ b/src/asahi/compiler/agx_compile.c @@ -7,6 +7,7 @@ #include "agx_compile.h" #include "asahi/clc/asahi_clc.h" +#include "asahi/isa/disasm.h" #include "asahi/layout/layout.h" #include "asahi/lib/agx_abi.h" #include "compiler/nir/nir_builder.h" @@ -3647,8 +3648,41 @@ agx_compile_function_nir(nir_shader *nir, nir_function_impl *impl, } } - ralloc_free(ctx); + /* + * Disassemble if the environment variable is set. Also disassemble to + * /dev/null in debug builds as a smoke test - a legally encoded shader + * should not hit any disassembler errors. + */ + bool dump_shaders = agx_should_dump(nir, AGX_DBG_SHADERS); +#ifndef NDEBUG + bool selftest = !dump_shaders; +#else + bool selftest = false; +#endif + if (dump_shaders || selftest) { + FILE *fp = selftest ? fopen("/dev/null", "w") : stdout; + bool errors = + agx2_disassemble(binary->data + offset, binary->size - offset, fp); + + if (errors) { + /* In a self-test, disassemble again to get something the user can look + * at to figure out what went wrong. + */ + if (selftest) { + agx2_disassemble(binary->data + offset, binary->size - offset, + stderr); + } + + unreachable("Disassembly error hit."); + } + + if (selftest) { + fclose(fp); + } + } + + ralloc_free(ctx); return offset; } diff --git a/src/asahi/compiler/agx_compiler.h b/src/asahi/compiler/agx_compiler.h index 3481cf7901a..a0fefa9d8de 100644 --- a/src/asahi/compiler/agx_compiler.h +++ b/src/asahi/compiler/agx_compiler.h @@ -6,13 +6,13 @@ #pragma once +#include "asahi/isa/agx_minifloat.h" #include "compiler/nir/nir.h" #include "util/half_float.h" #include "util/u_dynarray.h" #include "util/u_math.h" #include "util/u_worklist.h" #include "agx_compile.h" -#include "agx_minifloat.h" #include "agx_opcodes.h" #ifdef __cplusplus diff --git a/src/asahi/compiler/agx_optimizer.c b/src/asahi/compiler/agx_optimizer.c index 62682e2760d..f4625f1bc4e 100644 --- a/src/asahi/compiler/agx_optimizer.c +++ b/src/asahi/compiler/agx_optimizer.c @@ -6,7 +6,6 @@ #include "util/macros.h" #include "agx_builder.h" #include "agx_compiler.h" -#include "agx_minifloat.h" #include "agx_opcodes.h" /* AGX peephole optimizer responsible for instruction combining. It operates in diff --git a/src/asahi/compiler/meson.build b/src/asahi/compiler/meson.build index 27ac76af59b..fbc65283fac 100644 --- a/src/asahi/compiler/meson.build +++ b/src/asahi/compiler/meson.build @@ -92,6 +92,7 @@ libasahi_compiler = static_library( 'asahi_compiler', [libasahi_agx_files, agx_opcodes_c, agx_nir_algebraic_c], dependencies: [idep_nir, idep_agx_opcodes_h, idep_agx_builder_h, idep_mesautil, idep_libagx], + link_with : [libagx2_disasm], c_args : [no_override_init_args, '-Wno-c2x-extensions'], gnu_symbol_visibility : 'hidden', build_by_default : false, diff --git a/src/asahi/isa/__init__.py b/src/asahi/isa/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/asahi/compiler/agx_minifloat.h b/src/asahi/isa/agx_minifloat.h similarity index 97% rename from src/asahi/compiler/agx_minifloat.h rename to src/asahi/isa/agx_minifloat.h index d0aea2e70ec..b4181e0568a 100644 --- a/src/asahi/compiler/agx_minifloat.h +++ b/src/asahi/isa/agx_minifloat.h @@ -6,6 +6,8 @@ #pragma once #include +#include +#include #include "util/macros.h" /* AGX includes an 8-bit floating-point format for small dyadic immediates, diff --git a/src/asahi/isa/disasm-internal.h b/src/asahi/isa/disasm-internal.h new file mode 100644 index 00000000000..0fda6475446 --- /dev/null +++ b/src/asahi/isa/disasm-internal.h @@ -0,0 +1,258 @@ +/* + * Copyright 2025 Valve Corporation + * SPDX-License-Identifier: MIT + */ +#include +#include +#include +#include +#include +#include "asahi/isa/agx_minifloat.h" +#include "util/bitset.h" +#include "util/u_math.h" +#include "disasm.h" + +struct disasm_ctx { + FILE *fp; + bool any_operands; + bool error; +}; + +static inline uint64_t +bits(BITSET_WORD *word, unsigned start, unsigned size, unsigned shift) +{ + return ((uint64_t)__bitset_extract(word, start, size)) << shift; +} + +typedef void (*disasm_instr)(struct disasm_ctx *ctx, BITSET_WORD *code); + +struct disasm_spec { + const char *display; + disasm_instr disassemble; + unsigned length_bit; + unsigned length_short; + unsigned length_long; + BITSET_WORD mask[4]; + BITSET_WORD exact[4]; + BITSET_WORD known[4]; +}; + +static void +introduce_operand(struct disasm_ctx *ctx) +{ + if (ctx->any_operands) { + fprintf(ctx->fp, ", "); + } else { + fprintf(ctx->fp, " "); + } + + ctx->any_operands = true; +} + +enum operand_kind { + KIND_NONE = 0, + KIND_REG, + KIND_UNIFORM, + KIND_CF, + KIND_TS, + KIND_SS, + KIND_IMM, + KIND_FIMM, +}; + +struct operand_desc { + enum operand_kind kind; + int32_t value; + unsigned hint, count; + bool optional, size16, size32, size64, abs, neg, sx, cache, lu; +}; + +static void +_print_operand(struct disasm_ctx *ctx, struct operand_desc d) +{ + if (d.kind == KIND_NONE) { + if (!d.optional) { + introduce_operand(ctx); + fprintf(ctx->fp, "_"); + } + + return; + } + + unsigned size = d.size64 ? 64 : d.size32 ? 32 : 16; + bool cache = d.cache || d.hint == 2; + bool lu = d.lu || d.hint == 3; + unsigned count = MAX2(d.count, 1); + + introduce_operand(ctx); + + if (lu && cache) { + fprintf(ctx->fp, "XXX invalid cache+lu set\n"); + ctx->error = true; + } + + if (lu) + fprintf(ctx->fp, "^"); + if (cache) + fprintf(ctx->fp, "$"); + + const char *prefixes[][2] = { + [KIND_REG] = {"r", "dr"}, [KIND_UNIFORM] = {"u", "du"}, + [KIND_CF] = {"cf"}, [KIND_TS] = {"ts"}, + [KIND_SS] = {"ss"}, + }; + + if (d.kind == KIND_IMM) { + fprintf(ctx->fp, "%d", d.value); + } else if (d.kind == KIND_FIMM) { + float f = agx_minifloat_decode(d.value); + + /* Match python's float printing */ + if (f == (int)f) + fprintf(ctx->fp, "%g.0", agx_minifloat_decode(d.value)); + else + fprintf(ctx->fp, "%g", agx_minifloat_decode(d.value)); + } else if (d.kind == KIND_CF || d.kind == KIND_TS || d.kind == KIND_SS) { + fprintf(ctx->fp, "%s%u", prefixes[d.kind][0], d.value); + } else { + for (unsigned i = 0; i < count; ++i) { + if (i) + fprintf(ctx->fp, "_"); + + unsigned reg = d.value + (i * (size / 16)); + unsigned whole = reg >> 1; + unsigned part = reg & 1; + const char *prefix = prefixes[d.kind][size == 64]; + + if (size == 16) { + fprintf(ctx->fp, "%s%u%c", prefix, whole, "lh"[part]); + } else { + if (part != 0) { + fprintf(ctx->fp, "# 32-bit must be expected, but got raw %u\n", + reg); + ctx->error = true; + } + + fprintf(ctx->fp, "%s%u", prefix, whole); + } + } + } + + if (d.abs) + fprintf(ctx->fp, ".abs"); + if (d.neg) + fprintf(ctx->fp, ".neg"); + if (d.sx) + fprintf(ctx->fp, ".sx"); +} + +static void +print_immediate(struct disasm_ctx *ctx, bool is_signed, uint32_t value) +{ + introduce_operand(ctx); + fprintf(ctx->fp, is_signed ? "%d" : "%u", value); +} + +static void +_print_enum(struct disasm_ctx *ctx, const char **arr, unsigned n, + unsigned value) +{ + if (value >= n || arr[value] == NULL) { + introduce_operand(ctx); + fprintf(ctx->fp, "XXX: Unknown enum value %u", value); + ctx->error = true; + } else if (arr[value][0] != 0) { + introduce_operand(ctx); + fprintf(ctx->fp, "%s", arr[value]); + } +} + +#define print_enum(ctx, arr, value) \ + _print_enum(ctx, arr, ARRAY_SIZE(arr), value) + +static void +print_modifier(struct disasm_ctx *ctx, const char *display, unsigned value) +{ + if (value) { + introduce_operand(ctx); + fprintf(ctx->fp, "%s", display); + } +} + +static signed +_disassemble_instr(BITSET_WORD *code, FILE *fp, struct disasm_spec *specs, + unsigned nr_specs, unsigned offset, bool verbose) +{ + BITSET_WORD tmp[4]; + memcpy(tmp, code, sizeof(tmp)); + struct disasm_spec *spec = NULL; + unsigned n = 0; + bool match = false; + BITSET_WORD masked[4] = {0}; + + for (unsigned i = 0; i < nr_specs; ++i) { + spec = &specs[i]; + + n = spec->length_short; + if (BITSET_TEST(tmp, spec->length_bit)) { + n = spec->length_long; + } + + match = true; + for (unsigned i = 0; i < ARRAY_SIZE(tmp); ++i) { + masked[i] = tmp[i]; + + uint32_t bytes_left = (n - (i * 4)); + if (bytes_left < 4) { + masked[i] &= ((1 << (bytes_left * 8)) - 1); + } + + if ((masked[i] & spec->mask[i]) != spec->exact[i]) { + match = false; + break; + } + } + + if (match) + break; + } + + struct disasm_ctx ctx = {.fp = fp}; + + if (match) { + BITSET_WORD unknown[4]; + __bitset_andnot(unknown, masked, spec->known, ARRAY_SIZE(masked)); + int i; + BITSET_FOREACH_SET(i, unknown, n * 8) { + fprintf(fp, "# XXX: Unknown bit set %u\n", i); + ctx.error = true; + } + } else { + n = 2; + } + + if (verbose) { + fprintf(fp, "%4x: ", offset); + for (unsigned i = 0; i < n; ++i) { + fprintf(fp, "%02x", __bitset_extract(masked, i * 8, 8)); + } + for (unsigned i = n; i < 11; ++i) { + fprintf(fp, " "); + } + fprintf(fp, " "); + } + + if (!match) { + fprintf(fp, "\n"); + return -n; + } + + fprintf(fp, "%s", spec->display); + + if (spec->disassemble) + spec->disassemble(&ctx, masked); + + fprintf(fp, "\n"); + + return ctx.error ? -n : n; +} diff --git a/src/asahi/isa/disasm.h b/src/asahi/isa/disasm.h new file mode 100644 index 00000000000..6f9082f52f6 --- /dev/null +++ b/src/asahi/isa/disasm.h @@ -0,0 +1,33 @@ +/* + * Copyright 2025 Valve Corporation + * SPDX-License-Identifier: MIT + */ +#include +#include +#include "util/bitset.h" + +signed agx2_disassemble_instr(BITSET_WORD *code, FILE *fp, unsigned offset, + bool verbose); + +static inline bool +agx2_disassemble(uint8_t *code, size_t max_len, FILE *fp) +{ + int i = 0; + bool errors = false; + + while (i < max_len) { + /* Break on trap */ + if (code[i + 0] == 0x08 && code[i + 1] == 0x00) + break; + + signed ret = + agx2_disassemble_instr((BITSET_WORD *)(code + i), fp, i, true); + if (ret < 0) + fprintf(fp, "XXX error here\n"); + + errors |= ret < 0; + i += abs(ret); + } + + return errors; +} diff --git a/src/asahi/isa/gen-disasm.py b/src/asahi/isa/gen-disasm.py new file mode 100644 index 00000000000..faa0107d930 --- /dev/null +++ b/src/asahi/isa/gen-disasm.py @@ -0,0 +1,168 @@ +# Copyright 2016 Intel Corporation +# Copyright 2016 Broadcom +# Copyright 2020 Collabora, Ltd. +# Copyright 2025 Valve Corporation +# SPDX-License-Identifier: MIT + +from mako.template import Template +from mako import exceptions +from isa import isa + +# XXX: deduplicate +def to_alphanum(name): + substitutions = { + ' ': '_', + '/': '_', + '[': '', + ']': '', + '(': '', + ')': '', + '-': '_', + ':': '', + '.': '', + ',': '', + '=': '', + '>': '', + '#': '', + '&': '', + '*': '', + '"': '', + '+': '', + '\'': '', + } + + for i, j in substitutions.items(): + name = name.replace(i, j) + + return name + +def safe_name(name): + name = to_alphanum(name) + if not name[0].isalpha(): + name = '_' + name + + return name.lower() + +def chunk(x): + chunks = [hex((x >> (32 * i)) & ((1 << 32) - 1)) for i in range(4)] + while len(chunks) > 1 and chunks[-1] == '0x0': + chunks.pop() + + return f'{{{", ".join(chunks)}}}' + +def instr_is_trivial(I): + return len(I.dests + I.sources + I.immediates + I.modifiers) == 0 + +def extract_bits(x, bitset="r"): + parts = [] + n = 0 + + for r in x.bits.ranges: + parts.append(f'bits({bitset}, {r.start}, {r.size}, {n})') + n += r.size + + return ' | '.join(parts) + +template = """ +#include "disasm-internal.h" + +% for en in isa.enums: +UNUSED static const char *enum_${safe_name(isa.enums[en].kind)}[] = { +% for n, v in isa.enums[en].values.items(): + [${n}] = "${v.display or ''}", +% endfor +}; +% endfor + +% for k, enc in isa.encodings.items(): +static void +print_${k}(struct disasm_ctx *ctx, BITSET_WORD *orig, uint64_t v, bool source) +{ + BITSET_WORD r[] = { v, v >> 32 }; +% for i, case in enumerate(enc.cases): + ${"} else " if i > 0 else ""}if ((v & ${hex(case.exact.mask)}ull) == ${hex(case.exact.value)}ull) { +% for mod in case.modifiers: +% if mod.name == 'hint': + if (${extract_bits(mod)} == 0) { + fprintf(ctx->fp, "# missing hint"); + } + +% endif +% endfor + _print_operand(ctx, (struct operand_desc){ +% if case.kind is not None: + .kind = KIND_${case.kind.name.upper()}, +% if case.kind.kind == 'signed': + .value = util_sign_extend(${extract_bits(case.kind)} << ${case.kind.shift}, ${case.kind.bits.size()}), +% else: + .value = ${extract_bits(case.kind)} << ${case.kind.shift}, +% endif +% endif +% for mod in case.modifiers: +% if mod.name == 'mask': + .count = util_bitcount(${extract_bits(mod)}), +% elif mod.name == 'count': + .count = (${extract_bits(mod)}) ?: 4, +% elif mod.name == 'zx': + .sx = ${extract_bits(mod)} ^ 1, +% else: + .${mod.name} = ${extract_bits(mod)}, +% endif +% endfor +% for prop, v in case.properties.items(): + .${prop} = ${v}, +% endfor + .optional = source, + }); +% endfor + } else { + fprintf(ctx->fp, "# XXX: Invalid value 0x%"PRIx64" for ${k}", v); + ctx->error = true; + } +} + +% endfor + +% for k, instr in isa.instrs.items(): +% if not instr_is_trivial(instr): +static void +print_${instr.name}(struct disasm_ctx *ctx, BITSET_WORD *r) +{ +% for i, op in enumerate(instr.dests): + print_${op.kind}(ctx, r, ${extract_bits(op)}, false); +% endfor +% for i, op in enumerate(instr.sources): + print_${op.kind}(ctx, r, ${extract_bits(op)}, true); +% endfor +% for imm in instr.immediates: + print_immediate(ctx, ${'true' if imm.kind == 'signed' else 'false'}, ${extract_bits(imm)}); +% endfor +% for mod in instr.modifiers: +% if mod.name != 'length': +% if mod.kind in isa.enums: + print_enum(ctx, enum_${mod.kind}, ${extract_bits(mod)}); +% else: + print_modifier(ctx, "${mod.display}", ${extract_bits(mod)}); +% endif +% endif +% endfor +} + +% endif +% endfor +struct disasm_spec decodings[] = { +% for k, instr in isa.instrs.items(): + { "${instr.display if instr_is_trivial(instr) else f'{instr.display:<8}'}", ${'NULL' if instr_is_trivial(instr) else f'print_{instr.name}'}, ${instr.length_bit}, ${instr.length[0]}, ${instr.length[-1]}, ${chunk(instr.exact.mask)}, ${chunk(instr.exact.value)}, ${chunk(instr.mask())} }, +% endfor +}; + +signed +agx2_disassemble_instr(BITSET_WORD *code, FILE *fp, unsigned offset, bool verbose) +{ + return _disassemble_instr(code, fp, decodings, ARRAY_SIZE(decodings), offset, verbose); +}""" + +try: + print(Template(template).render(isa = isa, safe_name = safe_name, chunk = chunk, instr_is_trivial = instr_is_trivial, extract_bits = extract_bits)) +except: + print(exceptions.text_error_template().render()) diff --git a/src/asahi/isa/isa.py b/src/asahi/isa/isa.py new file mode 100644 index 00000000000..6667a37a0f0 --- /dev/null +++ b/src/asahi/isa/isa.py @@ -0,0 +1,321 @@ +# Copyright 2025 Valve Corporation +# SPDX-License-Identifier: MIT + +from dataclasses import dataclass +from functools import reduce +import xml.etree.ElementTree as ET +import sys +import copy +import os + +def ensure(cond, msg): + if not cond: + print(msg) + sys.exit(1) + +class BitRange: + def __init__(self, start, end): + assert(start <= end) + self.start = start + self.end = end + + if self.start >= 0: + self.size = end - start + 1 + self.size_mask = (1 << self.size) - 1 + self.mask = self.size_mask << start + + def parse(spec: str): + if ':' in spec: + start, end = [int(x) for x in spec.split(':')] + return BitRange(start, end) + else: + return BitRange(int(spec), int(spec)) + + def specialize(self, length): + return BitRange(self.start % length, self.end % length) + + def __repr__(self): + if self.start == self.end: + return f'{self.start}' + else: + return f'{self.start}:{self.end}' + +class Bits: + def __init__(self, spec = None): + if spec is not None: + self.ranges = [BitRange.parse(r) for r in spec.split(' ')] + else: + self.ranges = [] + + def size(self) -> int: + return sum([x.size for x in self.ranges]) + + def mask(self) -> int: + masks = [r.mask for r in self.ranges] + union = 0 + for mask in masks: + union |= mask + + # No overlap + assert(sum(masks) == union) + return union + + def extract(self, x): + accum = 0 + n = 0 + + for r in self.ranges: + accum |= ((x >> r.start) & r.size_mask) << n + n += r.size + + return accum + + def specialize(self, length): + obj = copy.copy(self) + obj.ranges = [r.specialize(length) for r in obj.ranges] + return obj + + def __repr__(self): + return ' '.join([str(r) for r in self.ranges]) + +def parse_bitstring(bits): + value = 0 + + # Reverse to deal with the convention + bits = bits[::-1] + + for i, bit in enumerate(bits): + assert(bit in ['0', '1']) + if bit == '1': + value |= (1 << i) + + return value + +# Spread the bits of value out according to the 1s in mask. +# Similar to pdep in x86. +def deposit_bits(mask, value): + accum = 0 + value_i = 0 + + assert(mask < (1 << 128)) + for m in range(128): + if mask & (1 << m): + if (value & (1 << value_i)) != 0: + accum |= (1 << m) + + value_i += 1 + + assert((accum & ~mask) == 0) + return accum + +def substitute(text, substitutions): + if text in substitutions: + return substitutions[text] + else: + return text + +class Exact: + def __init__(self, el = None, subst = None): + bits = 0 + if el is not None: + self.mask = Bits(el.attrib['bit']).mask() + if el.tag != 'zero': + text = substitute(el.text, subst) + bits = parse_bitstring(text) + assert(len(text) == self.mask.bit_count()) + else: + self.mask = 0 + + self.value = deposit_bits(self.mask, bits) + + def union(self, other): + self.value |= other.value + self.mask |= other.mask + +class EnumValue: + def __init__(self, el): + self.name = el.text + self.label = el.attrib.get('label') + self.default = bool(el.attrib.get('default', False)) + self.display = None if self.default else self.name + +class Enum: + def __init__(self, el): + self.name = el.attrib['name'] + self.kind = el.attrib['kind'] + self.values = {} + + for child in el: + self.values[int(child.attrib['value'])] = EnumValue(child) + +class Immediate: + def __init__(self, el, name): + self.name = name + self.kind = el.attrib.get('kind', 'int') + self.bits = Bits(el.attrib['bit']) + self.shift = int(el.attrib.get('shift', 0)) + +class Operand: + def __init__(self, el, is_dest): + self.kind = el.attrib['kind'] + self.bits = Bits(el.attrib['bit']) + self.is_dest = is_dest + + def specialize(self, length): + obj = copy.copy(self) + obj.bits = obj.bits.specialize(length) + return obj + +class EncodingCase: + def __init__(self, el): + self.exact = Exact() + self.modifiers = [] + self.kind = None + self.properties = {} + + for ch in el: + if ch.tag in ["exact", "zero"]: + self.exact.union(Exact(ch, {})) + elif ch.tag == "immediate": + assert(self.kind is None) + self.kind = Immediate(ch, ch.attrib['name']) + elif ch.tag == "modifier": + self.modifiers.append(Modifier(ch, ch.attrib['name'])) + elif ch.tag == "property": + self.properties[ch.attrib['name']] = ch.text or '1' + + def union(self, other): + self.exact.union(other.exact) + self.modifiers = other.modifiers + self.modifiers + self.properties.update(other.properties) + + assert(self.kind is None or other.kind is None) + self.kind = self.kind or other.kind + +class Encoding: + def __init__(self, el): + base = EncodingCase(el) + cases = [EncodingCase(ch) for ch in el if ch.tag == 'case'] + + if len(cases) == 0: + self.cases = [base] + else: + self.cases = cases + for c in self.cases: + c.union(base) + +class Modifier: + def __init__(self, el, name): + self.name = name + self.display = el.get('display', name) + self.kind = el.get('kind', 'int') + self.bits = Bits(el.attrib['bit']) + +class Instruction: + def __init__(self, xml, name, display, substitutions, defs): + length = [int(x) for x in xml.attrib['length'].split('/')] + exact = Exact() + mods = [] + dests = [] + srcs = [] + imms = [] + + for el in xml: + if el.tag in ["exact", "zero"]: + exact.union(Exact(el, substitutions)) + elif el.tag == "immediate": + imms.append(Immediate(el, el.attrib['name'])) + elif el.tag == "src": + srcs.append(Operand(el, False)) + elif el.tag == "dest": + dests.append(Operand(el, True)) + elif el.tag == "modifier": + mods.append(Modifier(el, el.attrib['name'])) + elif el.tag in defs: + d = defs[el.tag] + if isinstance(d, Operand): + spec = d.specialize(length[-1] * 8) + if spec.is_dest: + dests.append(spec) + else: + srcs.append(spec) + elif isinstance(d, Modifier): + mods.append(d) + elif isinstance(d, Immediate): + imms.append(d) + else: + assert(0) + elif el.tag == 'ins': + assert(xml.tag == 'group') + else: + print(name) + assert(0) + + # Infer length bit if needed + if len(length) > 1 and not any([x.name == 'length' for x in mods]): + mods.append(defs['length']) + + self.length_spec = None + self.length_bit = 0 + for mod in mods: + if mod.name == 'length': + self.length_spec = mod + self.length_bit = mod.bits.ranges[0].start + self.name = name + self.display = display or name + self.length = length + self.exact = exact + self.modifiers = mods + self.dests = dests + self.sources = srcs + self.immediates = imms + + def mask(self): + mask = self.exact.mask + parts = self.modifiers + self.immediates + self.sources + self.dests + bits = [x.bits.mask() for x in parts] + return self.exact.mask | reduce(lambda x, y: x | y, bits, 0) + +class ISA: + def __init__(self, el): + self.enums = {} + self.instrs = {} + self.encodings = {} + defs = {} + + for child in el: + name = child.get('name') + + if child.tag == "define": + if 'src' in child.attrib: + defs[child.attrib['src']] = Operand(child, False) + elif 'dest' in child.attrib: + defs[child.attrib['dest']] = Operand(child, True) + elif 'modifier' in child.attrib: + name = child.attrib['modifier'] + defs[child.attrib['modifier']] = Modifier(child, name) + elif 'immediate' in child.attrib: + name = child.attrib['immediate'] + defs[child.attrib['immediate']] = Immediate(child, name) + else: + assert(0) + elif child.tag == "encoding": + self.encodings[child.attrib['name']] = Encoding(child) + elif child.tag == "enum": + enum = Enum(child) + self.enums[enum.kind] = enum + elif child.tag == "ins": + ins = Instruction(child, name, child.get('display'), {}, defs) + ensure(ins.name not in self.instrs, f'Multiple instructions named {ins.name}') + self.instrs[ins.name] = ins + elif child.tag == "group": + ensure(any([c.tag == "ins" for c in child]), f'No instructions in group {name}') + + for c in child: + if c.tag == "ins": + ins = Instruction(child, c.attrib['name'], c.get('display'), c.attrib, defs) + ensure(ins.name not in self.instrs, f'Multiple instructions named {ins.name}') + self.instrs[ins.name] = ins + +xmlfile = os.path.join(os.path.dirname(__file__), 'AGX2.xml') +isa = ISA(ET.parse(xmlfile).getroot()) diff --git a/src/asahi/isa/meson.build b/src/asahi/isa/meson.build new file mode 100644 index 00000000000..3f9eb85622d --- /dev/null +++ b/src/asahi/isa/meson.build @@ -0,0 +1,41 @@ +# Copyright (C) 2020-2021 Collabora + +agx2_disasm_c = custom_target( + input : ['gen-disasm.py', 'AGX2.xml'], + output : 'agx2_disasm.c', + command : [prog_python, '@INPUT@'], + capture : true, + depend_files : files('isa.py'), +) + +libagx2_disasm = static_library( + 'agx2_disasm', + agx2_disasm_c, + include_directories : [inc_include, inc_src], + c_args : [no_override_init_args], + gnu_symbol_visibility : 'hidden', + build_by_default : true, +) + +if with_tests + test( + 'agx2_disasm', + executable( + 'agx2_disasm_test', + files('test/test-disassembler.c'), + c_args : [c_msvc_compat_args, no_override_init_args], + gnu_symbol_visibility : 'hidden', + include_directories : [inc_include, inc_src], + link_with : [libagx2_disasm], + ), + suite : ['asahi'], + args : files('test/cases.txt'), + ) + + test( + 'agx2_python_disasm', + prog_python, + args : files('test-disasm.py', 'test/cases.txt'), + suite : ['asahi'] + ) +endif diff --git a/src/asahi/isa/test-disasm.py b/src/asahi/isa/test-disasm.py new file mode 100644 index 00000000000..192f10bda6e --- /dev/null +++ b/src/asahi/isa/test-disasm.py @@ -0,0 +1,46 @@ +# Copyright 2025 Valve Corporation +# SPDX-License-Identifier: MIT + +import sys +import os +import textwrap +import binascii +from test.disasm import disasm + +errors = False +new_cases = [] + +for case in open(sys.argv[1]).read().strip().split('\n'): + first = case.split(' ')[0] + ref = case[len(first):].strip() + + # Extract bytes + lst = [int(x, 16) for x in textwrap.wrap(first, 2)] + + # Make sure we don't depend on zeroes in upper bits + padded = lst + [0xCA, 0xFE, 0xBA, 0xBE] * 16 + raw = sum([x << (8 * i) for i, x in enumerate(padded)]) + + error = False + length, asm = disasm(raw) + error |= (length != len(lst)) + + actual = f'{str(binascii.hexlify(bytes(lst)).decode()):<20} {asm}\n' + new_cases.append(actual) + error |= (actual[:-1] != case) + + if error: + print(case) + print(actual) + errors |= error + +# If there were errors, optionally update expectations. +if len(sys.argv) >= 3: + assert(sys.argv[2] in ['-u', '--update']) + open(sys.argv[1], 'w').write(''.join(new_cases)) + +if errors: + print('Fail.') + sys.exit(1) +else: + print('Pass.') diff --git a/src/asahi/isa/test/__init__.py b/src/asahi/isa/test/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/asahi/isa/test/disasm.py b/src/asahi/isa/test/disasm.py new file mode 100644 index 00000000000..fdc9c5de22f --- /dev/null +++ b/src/asahi/isa/test/disasm.py @@ -0,0 +1,158 @@ +# Copyright 2025 Valve Corporation +# SPDX-License-Identifier: MIT + +from isa import isa +import math +import sys +import binascii + +def match_instr(raw): + for key in isa.instrs: + instr = isa.instrs[key] + length = instr.length[instr.length_spec.bits.extract(raw) if instr.length_spec is not None else 0] + r = raw & ((1 << (length * 8)) - 1) + if (r & instr.exact.mask) == instr.exact.value: + return length, instr, r + + return 2, None, raw + +def map_value(v, kind, length): + if kind in isa.enums: + val = isa.enums[kind].values.get(v) + return f'unk{v}' if val is None else val.display + elif kind == 'signed': + return str(-((1 << length) - v) if (v & (1 << (length - 1))) else v) + else: + return str(v) + +def map_mod(v, mod): + if mod.kind in isa.enums: + return map_value(v, mod.kind, 0) + else: + assert(mod.bits.size() == 1) + return mod.display if v != 0 else None + +def decode_float_immediate(n): + sign = -1.0 if n & 0x80 else 1.0 + e = (n & 0x70) >> 4 + f = n & 0xF + if e == 0: + return sign * f / 64.0 + else: + return sign * float(0x10 | f) * (2.0 ** (e - 7)) + +def operand(desc, length): + count = desc.get('count', 1) + if count == 0: + count = 4 + if 'mask' in desc: + count = desc['mask'].bit_count() + if 'zx' in desc: + desc['sx'] = desc['zx'] ^ 1 + + size = 64 if desc.get('size64', 0) != 0 else 32 if desc.get('size32', 0) != 0 else 16 + base = desc.get('base') + kind = desc.get('kind') + cache = desc.get('cache', False) + discard = desc.get('discard', False) + + hint = desc.get('hint') + if hint is not None: + if hint == 3: + discard = True + elif hint == 2: + cache = True + else: + assert(hint == 1) + + assert(not (cache and discard)) + hint = '^' if discard else '$' if cache else '' + suffixes = ''.join(['.' + x for x in ['abs', 'neg', 'sx'] if desc.get(x, 0) != 0]) + + if kind == None: + return None + elif kind in ['cf', 'ts', 'ss']: + assert(count == 1) + return f'{kind}{base}' + suffixes + elif kind == 'fimm': + return str(decode_float_immediate(base)) + suffixes + elif kind in ['imm', 'signed']: + return map_value(base, kind, length) + suffixes + + def m(offs): + assert(size == 16 or size == 32 or size == 64) + reg = base + (offs * (size // 16)) + whole = reg >> 1 + part = reg & 1 + prefix = kind[0] + + if size == 16: + part_str = 'h' if part != 0 else 'l' + return f'{prefix}{whole}{part_str}' + else: + assert(part == 0) + return f'{"d" if size == 64 else ""}{prefix}{whole}' + + return hint + '_'.join([m(x) for x in range(count)]) + suffixes + +def map_op(v, kind, raw, ins, length): + for c in isa.encodings[kind].cases: + if (v & c.exact.mask) == c.exact.value: + desc = {} + if c.kind is not None: + length = c.kind.bits.size() + desc['kind'] = c.kind.name + desc['base'] = c.kind.bits.extract(v) << c.kind.shift + if c.kind.kind == 'signed': + desc['kind'] = 'signed' + + for mod in c.modifiers: + desc[mod.name] = mod.bits.extract(v) + + for prop in c.properties: + desc[prop] = int(c.properties[prop]) + + return operand(desc, length) + +def disasm(raw_in): + length, instr, raw = match_instr(raw_in) + if instr is None: + return length, "" + + imms = [map_value(imm.bits.extract(raw), imm.kind, imm.bits.size()) for imm in instr.immediates] + mods = [map_mod(mod.bits.extract(raw), mod) for mod in instr.modifiers if mod.name != 'length'] + dests = [map_op(d.bits.extract(raw), d.kind, raw, instr, d.bits.size()) for d in instr.dests] + srcs = [map_op(s.bits.extract(raw), s.kind, raw, instr, s.bits.size()) for s in instr.sources] + + dests = ['_' if x is None else str(x) for x in dests] + srcs = [str(x) for x in srcs if x is not None] + imms = [str(x) for x in imms if x is not None] + mods = [str(x) for x in mods if x is not None] + end = ', '.join(dests + srcs + imms + mods) + if end != "": + end = " " + end + + asm = f'{instr.display:<8}{end}'.strip() + + unexpected = raw & ~instr.mask() + unexpected = [i for i in range(length * 8) if unexpected & (1 << i)] + if len(unexpected) != 0: + print(f"Unexpected set bits: {unexpected}") + length = -length + + return length, asm + +if __name__ == '__main__': + prog = open(sys.argv[1], 'rb').read() + print("") + i = 0 + while i < len(prog): + lst = prog[i:i + 16] + raw = sum([int(x) << (8 * i) for i, x in enumerate(lst)]) + length, asm = disasm(raw) + lst = lst[0:length] + print(f'{hex(i)[2:]:>4}: {str(binascii.hexlify(lst).decode()):<20} {asm}') + if asm.strip() == 'trap': + break + i += length + print("") diff --git a/src/asahi/isa/test/test-disassembler.c b/src/asahi/isa/test/test-disassembler.c new file mode 100644 index 00000000000..7c2450b87a5 --- /dev/null +++ b/src/asahi/isa/test/test-disassembler.c @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2021 Collabora, Ltd. + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include "asahi/isa/disasm.h" +#include "util/macros.h" + +static inline uint8_t +parse_nibble(const char c) +{ + return (c >= 'a') ? 10 + (c - 'a') : (c - '0'); +} + +/* Given a little endian 8 byte hexdump, parse out the 64-bit value */ +static uint64_t +parse_hex(const char *in) +{ + uint64_t v = 0; + + for (unsigned i = 0; i < 8; ++i) { + uint8_t byte = (parse_nibble(in[0]) << 4) | parse_nibble(in[1]); + v |= ((uint64_t)byte) << (8 * i); + + /* Skip the space after the byte */ + in += 3; + } + + return v; +} + +int +main(int argc, const char **argv) +{ + if (argc < 2) { + fprintf(stderr, "Expected case list\n"); + return 1; + } + + FILE *fp = fopen(argv[1], "r"); + + if (fp == NULL) { + fprintf(stderr, "Could not open the case list"); + return 1; + } + + char line[128]; + unsigned nr_fail = 0, nr_pass = 0; + + while (fgets(line, sizeof(line), fp) != NULL) { + char *output = NULL; + size_t sz = 0; + size_t len = strlen(line); + + /* Skip empty lines */ + if (len <= 1) + continue; + + /* Parse the hex */ + uint8_t code[16]; + const char *cursor = line; + const char *end = line + len; + uint32_t i = 0; + while ((cursor + 2) <= end && cursor[0] != ' ') { + assert(i < ARRAY_SIZE(code)); + code[i] = (parse_nibble(cursor[0]) << 4) | parse_nibble(cursor[1]); + cursor += 2; + ++i; + } + + /* Skip spacing */ + while (cursor < end && (*cursor) == ' ') + ++cursor; + + FILE *outputp = open_memstream(&output, &sz); + signed ret = agx2_disassemble_instr((void *)code, outputp, 0, false); + unsigned instr_len = abs(ret); + fclose(outputp); + + /* Rest of the line is the reference assembly */ + bool fail = strcmp(cursor, output); + fail |= (instr_len != i); + fail |= (ret < 0); + + if (fail) { + /* Extra spaces after Got to align with Expected */ + printf("Got %sExpected %s\n", output, cursor); + + if (instr_len != i) { + printf("Got length %d, expected length %u\n", instr_len, i); + } + + if (ret < 0) { + printf("Got an error.\n"); + } + + nr_fail++; + } else { + nr_pass++; + } + + free(output); + } + + printf("Passed %u/%u tests.\n", nr_pass, nr_pass + nr_fail); + fclose(fp); + + return nr_fail ? 1 : 0; +} diff --git a/src/asahi/lib/decode.c b/src/asahi/lib/decode.c index de2a1587010..82fb2b78855 100644 --- a/src/asahi/lib/decode.c +++ b/src/asahi/lib/decode.c @@ -16,6 +16,7 @@ #include #include +#include "asahi/isa/disasm.h" #include "util/u_hexdump.h" #include "decode.h" @@ -24,7 +25,8 @@ struct libagxdecode_config lib_config; static void agx_disassemble(void *_code, size_t maxlen, FILE *fp) { - /* stub */ + bool errors = agx2_disassemble(_code, maxlen, fp); + assert(!errors); } FILE *agxdecode_dump_stream; diff --git a/src/asahi/lib/meson.build b/src/asahi/lib/meson.build index dfff7e9945b..c9e46f41418 100644 --- a/src/asahi/lib/meson.build +++ b/src/asahi/lib/meson.build @@ -36,6 +36,7 @@ libasahi_decode = static_library( dependencies : [dep_valgrind, idep_mesautil], c_args : [no_override_init_args, '-Wno-c2x-extensions'], gnu_symbol_visibility : 'hidden', + link_with : [libagx2_disasm], build_by_default : false, ) @@ -45,6 +46,7 @@ libasahi_decode_shared = shared_library( dependencies : [dep_valgrind, idep_mesautil], c_args : [no_override_init_args, '-Wno-c2x-extensions'], build_by_default : with_tools.contains('asahi'), + link_with : [libagx2_disasm], ) libagx_shaders = custom_target( diff --git a/src/asahi/meson.build b/src/asahi/meson.build index aaa7da43ccd..9956819d807 100644 --- a/src/asahi/meson.build +++ b/src/asahi/meson.build @@ -9,6 +9,8 @@ inc_asahi = include_directories([ '.', 'layout', 'lib', 'genxml', 'compiler' ]) +subdir('isa') + if with_gallium_asahi or with_asahi_vk or with_tools.contains('asahi') subdir('genxml') subdir('libagx')