From fb4e94c045be1933b8bb2960bd5e6aee586149ae Mon Sep 17 00:00:00 2001 From: Caio Oliveira Date: Sun, 31 May 2026 01:36:11 -0700 Subject: [PATCH] intel/gen: Add a gentool 'check-roundtrip' subcommand Add a round-trip checker as a gentool subcommand: it reads one or more case files of the form 'platform | bytes | assembly' and, per line, checks that the bytes decode to the assembly and/or the assembly encodes to the bytes. For example: ``` tgl | 40 00 03 00 20 82 05 05 04 06 10 02 2a 00 00 00 | add (8) r5 r6 0x0000002a ``` The second separator picks the directions ('|' both, '<' encode-only, '>' decode-only) and the disassembly print mode follows the file-name suffix (_verbose.txt, _translated.txt). Also add the two bits the checker depends on: GEN_PRINT_NO_LABELS, so a lone branch prints numeric jip/uip deltas rather than a synthesized label, and a 'gentool disasm --program-subset' option that decodes a program fragment. Suggested by Alyssa. Assisted-by: Pi coding agent (GPT-5.5) Acked-by: Alyssa Rosenzweig Part-of: --- src/intel/compiler/gen/gen.h | 5 + src/intel/compiler/gen/gen_print.cpp | 3 +- src/intel/compiler/gen/gentool.cpp | 397 ++++++++++++++++++++++++++- 3 files changed, 394 insertions(+), 11 deletions(-) diff --git a/src/intel/compiler/gen/gen.h b/src/intel/compiler/gen/gen.h index 1c3088e611c..8699dac9504 100644 --- a/src/intel/compiler/gen/gen.h +++ b/src/intel/compiler/gen/gen.h @@ -166,6 +166,11 @@ typedef enum gen_print_flags { * implies GEN_PRINT_HEX) into the effective flags. */ GEN_PRINT_IGNORE_ENV = 1 << 4, + + /* Don't synthesize labels for branch targets; print every jip/uip as an + * explicit numeric delta. + */ + GEN_PRINT_NO_LABELS = 1 << 5, } gen_print_flags; typedef struct gen_print_params { diff --git a/src/intel/compiler/gen/gen_print.cpp b/src/intel/compiler/gen/gen_print.cpp index 72c44f13f26..8320ef8f150 100644 --- a/src/intel/compiler/gen/gen_print.cpp +++ b/src/intel/compiler/gen/gen_print.cpp @@ -123,7 +123,8 @@ struct gen_printer { const char *const *annotations = params->annotations; const char *const *label_annotations = params->label_annotations; - labels = collect_labels(insts, num_insts); + if (!(flags & GEN_PRINT_NO_LABELS)) + labels = collect_labels(insts, num_insts); unsigned next_label = 0; int next_error = 0; diff --git a/src/intel/compiler/gen/gentool.cpp b/src/intel/compiler/gen/gentool.cpp index 0d0be71f66d..8abf8540ce9 100644 --- a/src/intel/compiler/gen/gentool.cpp +++ b/src/intel/compiler/gen/gentool.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #include "util/ralloc.h" @@ -48,13 +50,14 @@ print_top_help(FILE *fp) "Usage: gentool COMMAND [OPTIONS] [INPUT]\n" "\n" "Commands:\n" - " asm Assemble Gen instructions into binary output\n" - " disasm Disassemble Gen instruction binaries\n" - " region Show register-region diagrams for a single instruction\n" - " help Show help for a command or help topic\n" + " asm Assemble Gen instructions into binary output\n" + " disasm Disassemble Gen instruction binaries\n" + " region Show register-region diagrams for one instruction\n" + " check-roundtrip Run assemble/disassemble round-trip checks\n" + " help Show help for a command or help topic\n" "\n" "Help topics:\n" - " asm, disasm, region, platforms, syntax, lsc\n" + " asm, disasm, region, check-roundtrip, platforms, syntax, lsc\n" "\n" "Supports Gfx9+ platforms.\n" "\n" @@ -99,6 +102,8 @@ print_disasm_help(FILE *fp) " --translated-sends Print LSC sends as their translated mnemonic instead of\n" " a raw SEND instruction\n" " --hex Prefix each instruction with a hex dump of its encoded bytes\n" + " --program-subset Decode a fragment of a program; keep jip/uip branch\n" + " offsets as explicit numeric deltas instead of labels\n" "\n" "If INPUT is provided positionally, it is used as the input path.\n" "\n" @@ -106,6 +111,26 @@ print_disasm_help(FILE *fp) " gentool disasm -p tgl shader.bin\n"); } +static void +print_check_help(FILE *fp) +{ + fprintf(fp, + "Usage: gentool check-roundtrip CASES.txt [CASES.txt ...]\n" + "\n" + "Run assemble/disassemble round-trip checks.\n" + "\n" + "Each line of a case file is 'platform | bytes | assembly', where the\n" + "second separator can be '|' (check both directions), '<' (encode only)\n" + "or '>' (decode only).\n" + "\n" + "The disassembly print mode is chosen by the file-name suffix\n" + "(_verbose.txt, _translated.txt). Mismatches are printed to stdout; the\n" + "exit status is non-zero if any case fails.\n" + "\n" + "Example:\n" + " gentool check-roundtrip tests/basic.txt tests/basic_verbose.txt\n"); +} + static void print_region_help(FILE *fp) { @@ -753,6 +778,11 @@ cmd_help(int argc, char **argv) return 0; } + if (sub == "check-roundtrip") { + print_check_help(stdout); + return 0; + } + if (sub == "platforms") { print_platforms_help(stdout); return 0; @@ -769,7 +799,7 @@ cmd_help(int argc, char **argv) } fprintf(stderr, "gentool: unknown help topic '%s'\n", argv[1]); - fprintf(stderr, "Available help topics: asm, disasm, region, platforms, syntax, lsc\n"); + fprintf(stderr, "Available help topics: asm, disasm, region, check-roundtrip, platforms, syntax, lsc\n"); return 1; } @@ -901,9 +931,10 @@ cmd_disasm(int argc, char **argv) std::string output_path = "-"; intel_device_info devinfo = {}; bool have_devinfo = false; + bool program_subset = false; gen_print_flags print_flags = GEN_PRINT_NONE; - enum { OPT_TRANSLATED_SENDS = 1000, OPT_HEX }; + enum { OPT_TRANSLATED_SENDS = 1000, OPT_HEX, OPT_PROGRAM_SUBSET }; static const struct option long_options[] = { {"help", no_argument, NULL, 'h'}, @@ -914,6 +945,7 @@ cmd_disasm(int argc, char **argv) {"verbose", no_argument, NULL, 'v'}, {"translated-sends", no_argument, NULL, OPT_TRANSLATED_SENDS}, {"hex", no_argument, NULL, OPT_HEX}, + {"program-subset", no_argument, NULL, OPT_PROGRAM_SUBSET}, {}, }; @@ -949,6 +981,10 @@ cmd_disasm(int argc, char **argv) case OPT_HEX: print_flags = (gen_print_flags)(print_flags | GEN_PRINT_HEX); break; + case OPT_PROGRAM_SUBSET: + program_subset = true; + print_flags = (gen_print_flags)(print_flags | GEN_PRINT_NO_LABELS); + break; case '?': default: failf("unknown option '%s'", argv[optind - 1]); @@ -995,6 +1031,7 @@ cmd_disasm(int argc, char **argv) decode_params.raw_bytes = raw_bytes.data(); decode_params.raw_bytes_size = (int)raw_bytes.size(); decode_params.mem_ctx = mem_ctx; + decode_params.program_subset = program_subset; if (!gen_decode(&decode_params)) { ralloc_free(mem_ctx); failf("decode failed"); @@ -1115,6 +1152,345 @@ cmd_region(int argc, char **argv) return 0; } +/* Round-trip checker for the gen assembler and disassembler. + * + * Each input file holds one test case per line: + * + * tgl | 40 00 03 00 20 82 05 05 04 06 10 02 2a 00 00 00 | add (8) r5 r6 0x0000002a + * + * 1. the 3-letter platform name + * 2. the encoded instruction bytes, in little-endian hex (spaces optional). + * 3. the expected assembly text + * + * The platform and bytes are separated by " | ". The separator between the + * bytes and the assembly selects which directions are checked: + * + * bytes | asm both: bytes decode to asm AND asm encodes to bytes, + * bytes < asm encode only: asm encodes to bytes, + * bytes > asm decode only: bytes decode to asm. + * + * A '#' starts a comment to the end of the line. Blank lines are ignored. + * + * The disassembly print mode is chosen by the file name suffix: "_verbose.txt" + * prints in verbose mode, "_translated.txt" prints SENDs as their translated + * LSC opcode, any other name uses the default mode. + */ + +static std::string_view +rt_trim(std::string_view s) +{ + auto first = s.find_first_not_of(" \t\r\n"); + if (first == std::string_view::npos) + return ""; + + auto last = s.find_last_not_of(" \t\r\n"); + return s.substr(first, last - first + 1); +} + +static std::string_view +rt_rstrip(std::string_view s) +{ + auto last = s.find_last_not_of(" \t\r\n"); + if (last == std::string_view::npos) + return ""; + return s.substr(0, last + 1); +} + +static std::string_view +rt_strip_comment(std::string_view line) +{ + const auto hash = line.find('#'); + return hash == std::string_view::npos ? line : line.substr(0, hash); +} + +static int +rt_hex_nibble(char c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return 10 + (c - 'a'); + if (c >= 'A' && c <= 'F') + return 10 + (c - 'A'); + return -1; +} + +static std::vector +rt_parse_hex_bytes(std::string_view field, const char *path, unsigned lineno) +{ + std::vector bytes; + int hi = -1; + + for (char c : field) { + if (c == ' ' || c == '\t') + continue; + + const int nibble = rt_hex_nibble(c); + if (nibble < 0) + failf("%s:%u: invalid hex digit '%c'", path, lineno, c); + + if (hi < 0) { + hi = nibble; + } else { + bytes.push_back((uint8_t)((hi << 4) | nibble)); + hi = -1; + } + } + + if (hi >= 0) + failf("%s:%u: odd number of hex digits", path, lineno); + + if (bytes.size() != 16) + failf("%s:%u: byte count must be 16, got %zu", + path, lineno, bytes.size()); + + return bytes; +} + +static std::string +rt_format_hex_bytes(const void *data, size_t size) +{ + const uint8_t *bytes = (const uint8_t *)data; + std::string out; + char buf[4]; + + for (size_t i = 0; i < size; i++) { + snprintf(buf, sizeof(buf), "%02x", bytes[i]); + if (i > 0) + out += ' '; + out += buf; + } + + return out; +} + +struct rt_devinfo_cache { + std::unordered_map devinfos; + + const intel_device_info * + get(std::string_view name, const char *path, unsigned lineno) + { + const std::string platform(name); + + const auto existing = devinfos.find(platform); + if (existing != devinfos.end()) + return &existing->second; + + intel_device_info devinfo = {}; + const int pci_id = intel_device_name_to_pci_device_id(platform.c_str()); + if (pci_id < 0) + failf("%s:%u: unknown platform '%s'", path, lineno, platform.c_str()); + + if (!intel_get_device_info_from_pci_id(pci_id, &devinfo)) + failf("%s:%u: no device info for platform '%s'", path, lineno, + platform.c_str()); + + const auto inserted = devinfos.emplace(platform, devinfo); + return &inserted.first->second; + } +}; + +static std::string +rt_disassemble(const intel_device_info *devinfo, + const std::vector &bytes, gen_print_flags flags) +{ + void *mem_ctx = ralloc_context(NULL); + + gen_decode_params decode = {}; + decode.devinfo = devinfo; + decode.raw_bytes = bytes.data(); + decode.raw_bytes_size = (int)bytes.size(); + decode.mem_ctx = mem_ctx; + decode.program_subset = true; + + std::string out; + if (!gen_decode(&decode)) { + ralloc_free(mem_ctx); + return out; + } + + char *str = NULL; + size_t size = 0; + FILE *fp = open_memstream(&str, &size); + + gen_print_params print = {}; + print.devinfo = devinfo; + print.fp = fp; + print.flags = (gen_print_flags)(flags | GEN_PRINT_IGNORE_ENV | + GEN_PRINT_NO_LABELS); + print.insts = decode.insts; + print.num_insts = decode.num_insts; + print.raw_bytes = bytes.data(); + print.raw_bytes_size = (int)bytes.size(); + gen_print(&print); + + fclose(fp); + out = std::string(rt_rstrip(std::string_view(str ? str : "", size))); + free(str); + + ralloc_free(mem_ctx); + return out; +} + +static std::vector +rt_assemble(const intel_device_info *devinfo, std::string_view text) +{ + void *mem_ctx = ralloc_context(NULL); + + gen_parse_params parse = {}; + parse.devinfo = devinfo; + parse.text = text.data(); + parse.text_size = (int)text.size(); + parse.mem_ctx = mem_ctx; + + std::vector bytes; + if (!gen_parse(&parse) || parse.num_insts == 0) { + ralloc_free(mem_ctx); + return bytes; + } + + const int raw_bytes_size = parse.num_insts * (int)sizeof(gen_raw_inst); + + gen_encode_params encode = {}; + encode.devinfo = devinfo; + encode.insts = parse.insts; + encode.num_insts = parse.num_insts; + encode.mem_ctx = mem_ctx; + encode.raw_bytes = rzalloc_size(mem_ctx, raw_bytes_size); + encode.raw_bytes_size = raw_bytes_size; + + if (gen_encode(&encode)) { + const uint8_t *raw = (const uint8_t *)encode.raw_bytes; + bytes.assign(raw, raw + encode.raw_bytes_size); + } + + ralloc_free(mem_ctx); + return bytes; +} + +struct rt_stats { + unsigned pass = 0; + unsigned fail = 0; +}; + +static void +rt_check_line(const char *path, unsigned lineno, std::string_view line, + gen_print_flags flags, rt_devinfo_cache *cache, rt_stats *st) +{ + const auto bar1 = line.find('|'); + const auto sep = bar1 == std::string_view::npos ? + std::string_view::npos : line.find_first_of("|<>", bar1 + 1); + if (sep == std::string_view::npos) + failf("%s:%u: expected 'platform | bytes <|<>> assembly'", path, lineno); + + const bool check_decode = line[sep] != '<'; + const bool check_encode = line[sep] != '>'; + + const auto platform = rt_trim(line.substr(0, bar1)); + const auto hex = line.substr(bar1 + 1, sep - bar1 - 1); + + auto asm_start = sep + 1; + if (asm_start < line.size() && line[asm_start] == ' ') + asm_start++; + const auto asm_text = rt_rstrip(line.substr(asm_start)); + + const intel_device_info *devinfo = cache->get(platform, path, lineno); + const std::vector orig_bytes = rt_parse_hex_bytes(hex, path, lineno); + + bool decode_ok = true, encode_ok = true; + std::string decoded; + std::vector encoded; + + if (check_decode) { + decoded = rt_disassemble(devinfo, orig_bytes, flags); + decode_ok = decoded == asm_text; + } + if (check_encode) { + encoded = rt_assemble(devinfo, asm_text); + encode_ok = encoded == orig_bytes; + } + + if (decode_ok && encode_ok) { + st->pass++; + return; + } + + st->fail++; + printf("%s:%u: round-trip mismatch\n", path, lineno); + if (check_encode) { + printf("ORIGINAL BYTES: %s\n", + rt_format_hex_bytes(orig_bytes.data(), orig_bytes.size()).c_str()); + printf("ENCODED BYTES: %s\n", + rt_format_hex_bytes(encoded.data(), encoded.size()).c_str()); + printf("\n"); + } + if (check_decode) { + printf("ORIGINAL ASSEMBLY: "); + fwrite(asm_text.data(), 1, asm_text.size(), stdout); + printf("\nDECODED ASSEMBLY: %s\n", decoded.c_str()); + printf("\n"); + } +} + +static bool +rt_has_suffix(const char *path, const char *suffix) +{ + const size_t plen = strlen(path); + const size_t slen = strlen(suffix); + return plen >= slen && !strcmp(path + plen - slen, suffix); +} + +static void +rt_check_file(const char *path, rt_devinfo_cache *cache, rt_stats *st) +{ + std::ifstream file(path); + if (!file) + failf("failed to open '%s': %s", path, strerror(errno)); + + gen_print_flags flags = GEN_PRINT_NONE; + if (rt_has_suffix(path, "_verbose.txt")) + flags = GEN_PRINT_VERBOSE; + else if (rt_has_suffix(path, "_translated.txt")) + flags = GEN_PRINT_TRANSLATED_SENDS; + + std::string line; + unsigned lineno = 0; + while (std::getline(file, line)) { + lineno++; + const auto stripped = rt_strip_comment(line); + if (rt_trim(stripped).empty()) + continue; + rt_check_line(path, lineno, stripped, flags, cache, st); + } +} + +static int +cmd_check(int argc, char **argv) +{ + std::vector paths; + + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { + print_check_help(stdout); + return 0; + } + if (argv[i][0] == '-') + failf("unknown option '%s'", argv[i]); + paths.push_back(argv[i]); + } + + if (paths.empty()) + failf("no case files given"); + + rt_stats st; + rt_devinfo_cache cache; + for (const char *path : paths) + rt_check_file(path, &cache, &st); + + printf("Passed %u/%u cases.\n", st.pass, st.pass + st.fail); + return st.fail ? 1 : 0; +} + } /* namespace */ int @@ -1126,9 +1502,10 @@ main(int argc, char **argv) }; static const command cmds[] = { - { "asm", cmd_asm }, - { "disasm", cmd_disasm }, - { "region", cmd_region }, + { "asm", cmd_asm }, + { "disasm", cmd_disasm }, + { "region", cmd_region }, + { "check-roundtrip", cmd_check }, { "help", cmd_help }, };