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 <zdobersek@igalia.com>
Reviewed-by: Danylo Piliaiev <dpiliaiev@igalia.com>
Reviewed-by: Job Noorman <jnoorman@igalia.com>
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/34076>
This commit is contained in:
Zan Dobersek 2025-03-14 15:23:52 +01:00 committed by Marge Bot
parent 0acf46b973
commit b8cc891e6e
4 changed files with 202 additions and 9 deletions

View file

@ -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"},

View file

@ -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;

View file

@ -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;
}
%}

View file

@ -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;
}