From b8cc891e6ec4b9a9612f2d5354e57bb7770aa052 Mon Sep 17 00:00:00 2001 From: Zan Dobersek Date: Fri, 14 Mar 2025 15:23:52 +0100 Subject: [PATCH] ir3: allow asm roundtrip testing of compiled shader variants The `asmroundtrip` IR3_SHADER_DEBUG option enables roundtrip testing of ir3 asm facilities by generating disassembly for each compiled shader variant, parsing that disassembly back into ir3 and assembling back into binary, with the expectation that the initial binary and the post-roundtrip binary are identical. This should give some guarantee that any shader that ir3 can produce can also be constructed through assembly and fed back into ir3. When enabled, each shader variant has a parallel roundtrip variant created. At the moment this variant is discarded after validation, but it could replace the initial variant in the future to also test behavior of such roundtrip-generated binary and accompanying metadata. Signed-off-by: Zan Dobersek Reviewed-by: Danylo Piliaiev Reviewed-by: Job Noorman Part-of: --- src/freedreno/ir3/ir3_compiler.c | 1 + src/freedreno/ir3/ir3_compiler.h | 9 +- src/freedreno/ir3/ir3_parser.y | 9 +- src/freedreno/ir3/ir3_shader.c | 192 ++++++++++++++++++++++++++++++- 4 files changed, 202 insertions(+), 9 deletions(-) diff --git a/src/freedreno/ir3/ir3_compiler.c b/src/freedreno/ir3/ir3_compiler.c index 150821cd225..7df4af6a3d8 100644 --- a/src/freedreno/ir3/ir3_compiler.c +++ b/src/freedreno/ir3/ir3_compiler.c @@ -36,6 +36,7 @@ static const struct debug_named_value shader_debug_options[] = { {"expandrpt", IR3_DBG_EXPANDRPT, "Expand rptN instructions"}, {"noaliastex", IR3_DBG_NOALIASTEX, "Don't use alias.tex"}, {"noaliasrt", IR3_DBG_NOALIASRT, "Don't use alias.rt"}, + {"asmroundtrip", IR3_DBG_ASM_ROUNDTRIP, "Disassemble, reassemble and compare every shader"}, #if MESA_DEBUG /* MESA_DEBUG-only options: */ {"schedmsgs", IR3_DBG_SCHEDMSGS, "Enable scheduler debug messages"}, diff --git a/src/freedreno/ir3/ir3_compiler.h b/src/freedreno/ir3/ir3_compiler.h index cbf4a253b5b..cf1491d7e0e 100644 --- a/src/freedreno/ir3/ir3_compiler.h +++ b/src/freedreno/ir3/ir3_compiler.h @@ -369,12 +369,13 @@ enum ir3_shader_debug { IR3_DBG_NOEARLYPREAMBLE = BITFIELD_BIT(17), IR3_DBG_NODESCPREFETCH = BITFIELD_BIT(18), IR3_DBG_EXPANDRPT = BITFIELD_BIT(19), + IR3_DBG_ASM_ROUNDTRIP = BITFIELD_BIT(20), /* MESA_DEBUG-only options: */ - IR3_DBG_SCHEDMSGS = BITFIELD_BIT(20), - IR3_DBG_RAMSGS = BITFIELD_BIT(21), - IR3_DBG_NOALIASTEX = BITFIELD_BIT(22), - IR3_DBG_NOALIASRT = BITFIELD_BIT(23), + IR3_DBG_SCHEDMSGS = BITFIELD_BIT(21), + IR3_DBG_RAMSGS = BITFIELD_BIT(22), + IR3_DBG_NOALIASTEX = BITFIELD_BIT(23), + IR3_DBG_NOALIASRT = BITFIELD_BIT(24), }; extern enum ir3_shader_debug ir3_shader_debug; diff --git a/src/freedreno/ir3/ir3_parser.y b/src/freedreno/ir3/ir3_parser.y index 611627c9f06..00270fb4609 100644 --- a/src/freedreno/ir3/ir3_parser.y +++ b/src/freedreno/ir3/ir3_parser.y @@ -57,6 +57,8 @@ struct ir3 * ir3_parse(struct ir3_shader_variant *v, #define IR3_REG_ABS IR3_REG_FABS #define IR3_REG_NEGATE IR3_REG_FNEG +static pthread_mutex_t ir3_parse_mtx = PTHREAD_MUTEX_INITIALIZER; + static struct ir3_kernel_info *info; static struct ir3_shader_variant *variant; /* NOTE the assembler doesn't really use the ir3_block construction @@ -337,6 +339,8 @@ static void yyerror(const char *error) struct ir3 * ir3_parse(struct ir3_shader_variant *v, struct ir3_kernel_info *k, FILE *f) { + pthread_mutex_lock(&ir3_parse_mtx); + ir3_yyset_lineno(1); ir3_yyset_input(f); #ifdef YYDEBUG @@ -354,7 +358,10 @@ struct ir3 * ir3_parse(struct ir3_shader_variant *v, } ralloc_free(labels); ralloc_free(ir3_parser_dead_ctx); - return variant->ir; + + struct ir3 *ir = variant->ir; + pthread_mutex_unlock(&ir3_parse_mtx); + return ir; } %} diff --git a/src/freedreno/ir3/ir3_shader.c b/src/freedreno/ir3/ir3_shader.c index 56d824afd99..5af3576d208 100644 --- a/src/freedreno/ir3/ir3_shader.c +++ b/src/freedreno/ir3/ir3_shader.c @@ -222,6 +222,174 @@ try_override_shader_variant(struct ir3_shader_variant *v, return true; } +struct disasm_context { + FILE *stream; + uint8_t *mismatch_array; +}; + +static void +disasm_pre_instr_cb(void *cbdata, unsigned n, void *instr) +{ + struct disasm_context *context = (struct disasm_context *)cbdata; + bool mismatch = context->mismatch_array && context->mismatch_array[n]; + uint32_t *dwords = (uint32_t *)instr; + fprintf(context->stream, " %s [%03d] [%08x_%08x] ", + mismatch ? "!!" : " ", n, dwords[1], dwords[0]); +} + +static void +disasm_no_match_cb(FILE *stream, const BITSET_WORD *dwords, size_t size) +{ + fprintf(stream, " XX [000] raw 0x%X%X\n", dwords[0], dwords[1]); +} + +static char * +disasm_collect(struct ir3_shader_variant *v, uint8_t *mismatch_array, + uint32_t *binary_data, uint32_t binary_size) +{ + char *stream_data = NULL; + size_t stream_size = 0; + FILE *stream = open_memstream(&stream_data, &stream_size); + + struct disasm_context context = { + .stream = stream, + .mismatch_array = mismatch_array, + }; + + struct isa_decode_options decode_options = { + .gpu_id = v->ir->compiler->gen * 100, + .show_errors = true, + .branch_labels = true, + .cbdata = &context, + .pre_instr_cb = disasm_pre_instr_cb, + .no_match_cb = disasm_no_match_cb, + }; + + ir3_isa_disasm(binary_data, binary_size, stream, &decode_options); + + fclose(stream); + return stream_data; +} + +static uint16_t +variant_unpadded_binary_size(struct ir3_shader_variant *v) +{ + /* This helper returns the size (in dwords) of variant's binary after + * the padding nops at the end are ignored. + */ + uint16_t size = v->info.sizedwords; + + for (uint16_t i = 0; i < v->info.sizedwords; i += 2) { + uint32_t *dword = &v->bin[v->info.sizedwords - 2 - i]; + if (!!dword[0] || !!dword[1]) + break; + + size -= 2; + } + + return size; +} + +static void +validate_print_disasm(struct ir3_shader_variant *v, uint8_t *mismatch_array) +{ + char *disasm = disasm_collect(v, mismatch_array, + v->bin, variant_unpadded_binary_size(v) * 4); + mesa_loge("\n%s", disasm); + free(disasm); +} + +static bool +validate_roundtrip_variant_binary(struct ir3_shader_variant *rt_v, struct ir3_shader_variant *v) +{ + /* Ignoring padding nops in both variants, compare the binary data. + * If there's a mismatch, print both disassemblies with highlighted + * points of difference. + */ + uint16_t v_sizedwords = variant_unpadded_binary_size(v); + uint16_t rt_v_sizedwords = variant_unpadded_binary_size(rt_v); + if (v_sizedwords == rt_v_sizedwords && + !memcmp(v->bin, rt_v->bin, v_sizedwords * 4)) + return true; + + mesa_loge("validate_roundtrip_variant_binary: mismatch between initial and reassembled binary\n"); + + uint32_t max_sizedwords = MAX2(v_sizedwords, rt_v_sizedwords); + uint8_t *mismatch_array = calloc(max_sizedwords / 2, sizeof(uint8_t)); + + for (uint32_t i = 0; i < max_sizedwords; i += 2) { + if (i >= v_sizedwords || i >= rt_v_sizedwords) { + mismatch_array[i / 2] = 0xff; + continue; + } + + uint32_t *v_dword = &v->bin[i]; + uint32_t *rt_v_dword = &rt_v->bin[i]; + if (v_dword[0] != rt_v_dword[0] || v_dword[1] != rt_v_dword[1]) + mismatch_array[i / 2] = 0xff; + } + + mesa_loge(" disassembly of initial binary:"); + validate_print_disasm(v, mismatch_array); + + mesa_loge(" disassembly of reassembled binary:"); + validate_print_disasm(rt_v, mismatch_array); + + free(mismatch_array); + return false; +} + +static struct ir3_shader_variant * +alloc_variant(struct ir3_shader *shader, const struct ir3_shader_key *key, + struct ir3_shader_variant *nonbinning, void *mem_ctx); + +static struct ir3_shader_variant * +create_roundtrip_variant(struct ir3_shader *shader, struct ir3_shader_variant *v) +{ + struct ir3_shader_variant *rt_v = alloc_variant(shader, &v->key, NULL, NULL); + if (!rt_v) + return NULL; + + /* Dump variant's disassembly into a memory stream, then read and + * parse from that stream to assemble the roundtrip variant. + */ + char *disasm_data = NULL; + size_t disasm_size = 0; + FILE *disasm_stream = open_memstream(&disasm_data, &disasm_size); + ir3_shader_disasm(v, v->bin, disasm_stream); + fflush(disasm_stream); + + struct ir3_kernel_info info; + memset(&info, 0, sizeof(info)); + info.numwg = INVALID_REG; + + fseek(disasm_stream, 0, SEEK_SET); + rt_v->ir = ir3_parse(rt_v, &info, disasm_stream); + + fclose(disasm_stream); + free(disasm_data); + + if (!rt_v->ir) { + mesa_loge("create_roundtrip_variant: failed to parse initial disassembly"); + goto fail; + } + + rt_v->bin = ir3_shader_assemble(rt_v); + if (!rt_v->bin) { + mesa_loge("create_roundtrip_variant: failed to assemble parsed initial disassembly"); + goto fail; + } + + if (!validate_roundtrip_variant_binary(rt_v, v)) + goto fail; + + return rt_v; + +fail: + ralloc_free(rt_v); + return NULL; +} + static void assemble_variant(struct ir3_shader_variant *v, bool internal) { @@ -275,10 +443,6 @@ assemble_variant(struct ir3_shader_variant *v, bool internal) free(stream_data); } } - - /* no need to keep the ir around beyond this point: */ - ir3_destroy(v->ir); - v->ir = NULL; } static bool @@ -298,6 +462,26 @@ compile_variant(struct ir3_shader *shader, struct ir3_shader_variant *v) return false; } + if (ir3_shader_debug & IR3_DBG_ASM_ROUNDTRIP) { + struct ir3_shader_variant *rt_v = create_roundtrip_variant(shader, v); + if (!rt_v) + return false; + + /* TODO: the roundtrip variant could replace the initial variant + * to also test the gathered variant information that's then emitted + * into shader state. Some known problems: + * - parsing from assembly will lack constant data that's filled + * in ir3_nir_lower_load_constant during compilation + * - parsing from assembly will also lack metadata (e.g. writemasks) + * that's used to determine register footprint + */ + ralloc_free(rt_v); + } + + /* no need to keep the ir around beyond this point: */ + ir3_destroy(v->ir); + v->ir = NULL; + return true; }