Files
virtualjaguar-libretro/test/tools/trace_diff.c
Joseph MattielloandClaude Fable 5 7df363aa49 fix(trace): make trace loss and dump failure impossible to miss
Final-review findings. A 300-frame yarc run emits ~2,578 events/frame, so
the default 1M ring holds ~390 frames while both worked examples showed
--frames 1800: the ring wrapped and threw away most of the run while
trace_probe reported success and trace_dump printed nothing unusual. The
loss was always recoverable from the data — the oldest surviving record's
seq IS the number evicted — so trace_dump and trace_diff now say so on
stderr, and the examples carry a measured VJ_TRACE_RING with a note that
the event rate is ROM-dependent.

vjtrace_dump() checked no fwrite and not fclose, so a truncated dump
returned success — the opposite of the invariant vjtrace_snapshot()
enforces 200 lines below it. It now mirrors that: any short write removes
the partial file and returns -1, so a VJTR file that exists is complete.

A --watch whose ADDR+LEN wrapped past $FFFFFFFF produced lo > hi, a watch
that could never fire, and a run that reported success. Rejected now.

The vj-debug skill's menu_step_probe build line predated that probe
gaining trace_probe_attach and no longer linked; it does now.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-12 01:49:11 -04:00

356 lines
12 KiB
C

/* test/tools/trace_diff.c -- structurally diff two "VJTR" vjtrace event
* ring dumps (see src/core/vjtrace.c:vjtrace_dump()).
*
* Standalone C99, no harness dependency: includes src/core/vjtrace.h
* only for the vjtrace_ev struct / event enum (declared outside the
* VJ_TRACE guard for exactly this kind of offline tool).
*
* Comparison key is (type, who, addr) in filtered stream order -- NOT
* frame/seq, since two runs of the same ROM can offset by a frame or
* two and still be "the same shape" event-for-event. Default filter
* excludes VJT_EV_OP_OBJECT (fires once per displayed object, per-
* object noise that swamps everything else); --types LIST replaces
* the filter outright with exactly the named types.
*
* Build:
* cc -O2 -Wall -std=c99 -I. -o test/tools/trace_diff test/tools/trace_diff.c
*
* Usage:
* trace_diff A B [--types LIST] (LIST = comma-separated type names)
*
* Exit: 0 = filtered streams identical, 1 = divergence found,
* 2 = usage error or malformed/truncated input file.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include "src/core/vjtrace.h"
/* Case-insensitive exact-match compare, local rather than strcasecmp():
* Apple libc declares strcasecmp() in <strings.h> unconditionally, but
* glibc hides it under -std=c99 (strict-ANSI suppresses the
* _DEFAULT_SOURCE feature-test macro that guards it), so a build that
* is clean here can warn "implicit declaration" on Linux. */
static int ci_eq(const char *a, const char *b)
{
while (*a && *b) {
char ca = *a, cb = *b;
if (ca >= 'a' && ca <= 'z') ca = (char)(ca - 'a' + 'A');
if (cb >= 'a' && cb <= 'z') cb = (char)(cb - 'a' + 'A');
if (ca != cb) return 0;
a++; b++;
}
return *a == '\0' && *b == '\0';
}
static const char *type_names[VJT_EV__COUNT] = {
"NONE", "IRQ_ASSERT", "IRQ_DISPATCH", "GPU_GO", "GPU_STOP",
"OP_LIST_START", "OP_OBJECT", "OP_GPU_OBJ", "OP_BRANCH", "BLIT_CMD",
"INPUT_EDGE", "WATCH_RD", "WATCH_WR", "SNAPSHOT", "MARK"
};
#define WHO_COUNT 10
static const char *who_names[WHO_COUNT] = {
"UNKNOWN", "JAGUAR", "DSP", "GPU", "TOM", "JERRY",
"M68K", "BLITTER", "OP", "DEBUG"
};
static const char *type_name(unsigned t)
{
static char buf[24];
if (t < VJT_EV__COUNT) return type_names[t];
snprintf(buf, sizeof(buf), "TYPE_%u", t);
return buf;
}
static const char *who_name(unsigned w)
{
static char buf[24];
if (w < WHO_COUNT) return who_names[w];
snprintf(buf, sizeof(buf), "WHO_%u", w);
return buf;
}
static int type_from_name(const char *name)
{
int i;
for (i = 0; i < VJT_EV__COUNT; i++)
if (ci_eq(name, type_names[i]))
return i;
return -1;
}
typedef struct {
char magic[4];
uint32_t version, ev_size, pad;
uint64_t count;
} vjtr_header;
static int read_header(FILE *f, vjtr_header *h)
{
if (fread(h->magic, 1, 4, f) != 4) return 0;
if (fread(&h->version, sizeof(uint32_t), 1, f) != 1) return 0;
if (fread(&h->ev_size, sizeof(uint32_t), 1, f) != 1) return 0;
if (fread(&h->pad, sizeof(uint32_t), 1, f) != 1) return 0;
if (fread(&h->count, sizeof(uint64_t), 1, f) != 1) return 0;
return 1;
}
static void print_record(const char *tag, long idx, const vjtrace_ev *e)
{
printf("%s[%ld] seq=%llu f=%u hl=%u %s who=%s pc=%X addr=%08X val=%X\n",
tag, idx, (unsigned long long)e->seq, (unsigned)e->frame,
(unsigned)e->halfline, type_name(e->type), who_name(e->who),
(unsigned)e->pc, (unsigned)e->addr, (unsigned)e->value);
}
/* Loads FILE, validates the VJTR header, and returns a malloc'd array
* of only the events passing the type filter (types[] of length
* ntypes; NULL/0 means "all types"). *out_n receives the filtered
* count. Returns NULL and leaves *out_n at -1 on malformed input
* (caller exits 2); returns a valid (possibly zero-length, non-NULL
* for n>0) buffer otherwise. */
static vjtrace_ev *load_filtered(const char *path, const int *keep_mask,
long *out_n)
{
FILE *f;
vjtr_header hdr;
vjtrace_ev *buf;
long n = 0;
uint64_t idx;
long header_bytes, filesize, data_bytes;
uint64_t actual_count;
size_t alloc_count;
*out_n = -1;
f = fopen(path, "rb");
if (!f) {
fprintf(stderr, "trace_diff: cannot open '%s': %s\n", path, strerror(errno));
return NULL;
}
if (!read_header(f, &hdr)) {
fprintf(stderr, "trace_diff: '%s': truncated header (not a VJTR file?)\n", path);
fclose(f);
return NULL;
}
if (memcmp(hdr.magic, "VJTR", 4) != 0) {
fprintf(stderr, "trace_diff: '%s': bad magic (not a VJTR ring dump)\n", path);
fclose(f);
return NULL;
}
if (hdr.version != 1) {
fprintf(stderr, "trace_diff: '%s': unsupported version %u (want 1)\n",
path, (unsigned)hdr.version);
fclose(f);
return NULL;
}
if (hdr.ev_size != (uint32_t)sizeof(vjtrace_ev)) {
fprintf(stderr,
"trace_diff: '%s': record size %u does not match this tool's "
"vjtrace_ev (%u)\n", path, (unsigned)hdr.ev_size,
(unsigned)sizeof(vjtrace_ev));
fclose(f);
return NULL;
}
/* hdr.count is attacker/corruption-controlled: a crafted file can
* claim ~2^59 records while containing none, which would wrap
* `count * sizeof(vjtrace_ev)` in a plain size_t multiply into a
* tiny allocation that the read loop below then overruns (task-6
* review Critical #1). Don't trust it -- derive the real record
* count from the file's actual length and require it to match
* exactly before allocating anything sized by it. */
header_bytes = ftell(f);
if (header_bytes < 0) {
fprintf(stderr, "trace_diff: '%s': ftell failed after header\n", path);
fclose(f);
return NULL;
}
if (fseek(f, 0, SEEK_END) != 0) {
fprintf(stderr, "trace_diff: '%s': seek to end failed\n", path);
fclose(f);
return NULL;
}
filesize = ftell(f);
if (filesize < 0) {
fprintf(stderr, "trace_diff: '%s': ftell failed at end\n", path);
fclose(f);
return NULL;
}
if (fseek(f, header_bytes, SEEK_SET) != 0) {
fprintf(stderr, "trace_diff: '%s': seek back to records failed\n", path);
fclose(f);
return NULL;
}
data_bytes = filesize - header_bytes;
if (data_bytes < 0 || (uint64_t)data_bytes % hdr.ev_size != 0) {
fprintf(stderr,
"trace_diff: '%s': file length (%ld bytes of record data) is "
"not a whole number of %u-byte records -- truncated or corrupt\n",
path, data_bytes < 0 ? 0L : data_bytes, (unsigned)hdr.ev_size);
fclose(f);
return NULL;
}
actual_count = (uint64_t)data_bytes / hdr.ev_size;
if (actual_count != hdr.count) {
fprintf(stderr,
"trace_diff: '%s': header claims %llu records but the file "
"actually contains %llu -- truncated or corrupt\n",
path, (unsigned long long)hdr.count, (unsigned long long)actual_count);
fclose(f);
return NULL;
}
if (actual_count > SIZE_MAX / sizeof(vjtrace_ev)) {
fprintf(stderr, "trace_diff: '%s': record count too large to allocate\n", path);
fclose(f);
return NULL;
}
alloc_count = (actual_count > 0) ? (size_t)actual_count : 1; /* n=0 placeholder */
buf = (vjtrace_ev *)malloc(alloc_count * sizeof(vjtrace_ev));
if (!buf) {
fprintf(stderr, "trace_diff: out of memory reading '%s'\n", path);
fclose(f);
return NULL;
}
for (idx = 0; idx < hdr.count; idx++) {
vjtrace_ev ev;
if (fread(&ev, sizeof(ev), 1, f) != 1) {
fprintf(stderr,
"trace_diff: '%s': truncated record stream (header said "
"%llu records, only %llu present)\n",
path, (unsigned long long)hdr.count, (unsigned long long)idx);
free(buf);
fclose(f);
return NULL;
}
if (idx == 0 && ev.seq != 0) {
fprintf(stderr,
"WARNING: '%s': ring wrapped -- the first %llu events "
"(seq 0..%llu) were evicted before the dump. Raise "
"VJ_TRACE_RING and re-run.\n",
path, (unsigned long long)ev.seq,
(unsigned long long)(ev.seq - 1));
}
if (ev.type < VJT_EV__COUNT && !keep_mask[ev.type])
continue;
buf[n++] = ev;
}
fclose(f);
*out_n = n;
return buf;
}
int main(int argc, char **argv)
{
const char *pathA = NULL, *pathB = NULL;
int keep_mask[VJT_EV__COUNT];
int i, t;
long nA = 0, nB = 0;
vjtrace_ev *A, *B;
long divergence = -1;
int len_diverges = 0;
for (i = 0; i < VJT_EV__COUNT; i++)
keep_mask[i] = 1;
keep_mask[VJT_EV_OP_OBJECT] = 0; /* default filter */
for (i = 1; i < argc; i++) {
if (strcmp(argv[i], "--types") == 0 && i + 1 < argc) {
char *list = strdup(argv[++i]);
char *tok;
if (!list) {
fprintf(stderr, "trace_diff: out of memory\n");
return 2;
}
for (t = 0; t < VJT_EV__COUNT; t++)
keep_mask[t] = 0;
tok = strtok(list, ",");
while (tok) {
int ty = type_from_name(tok);
if (ty < 0) {
fprintf(stderr, "trace_diff: unknown type '%s' in --types\n", tok);
free(list);
return 2;
}
keep_mask[ty] = 1;
tok = strtok(NULL, ",");
}
free(list);
} else if (!pathA) {
pathA = argv[i];
} else if (!pathB) {
pathB = argv[i];
} else {
fprintf(stderr, "trace_diff: unexpected arg '%s'\n", argv[i]);
return 2;
}
}
if (!pathA || !pathB) {
fprintf(stderr, "usage: trace_diff A B [--types LIST]\n");
return 2;
}
A = load_filtered(pathA, keep_mask, &nA);
if (nA < 0) return 2;
B = load_filtered(pathB, keep_mask, &nB);
if (nB < 0) { free(A); return 2; }
{
long minlen = (nA < nB) ? nA : nB;
long i2;
for (i2 = 0; i2 < minlen; i2++) {
if (A[i2].type != B[i2].type || A[i2].who != B[i2].who ||
A[i2].addr != B[i2].addr) {
divergence = i2;
break;
}
}
if (divergence < 0 && nA != nB) {
divergence = minlen;
len_diverges = 1;
}
}
if (divergence < 0) {
printf("trace_diff: %ld records compared (filtered), no divergence\n", nA);
free(A);
free(B);
return 0;
}
if (len_diverges)
printf("trace_diff: streams diverge at filtered index %ld -- "
"A has %ld records, B has %ld records\n", divergence, nA, nB);
else
printf("trace_diff: streams diverge at filtered index %ld "
"(key mismatch: type/who/addr)\n", divergence);
printf("== %s ==\n", pathA);
{
long lo, hi, i2;
lo = divergence - 5; if (lo < 0) lo = 0;
hi = divergence + 5; if (hi > nA - 1) hi = nA - 1;
for (i2 = lo; i2 <= hi; i2++)
print_record(i2 == divergence ? ">>>A" : " A", i2, &A[i2]);
}
printf("== %s ==\n", pathB);
{
long lo, hi, i2;
lo = divergence - 5; if (lo < 0) lo = 0;
hi = divergence + 5; if (hi > nB - 1) hi = nB - 1;
for (i2 = lo; i2 <= hi; i2++)
print_record(i2 == divergence ? ">>>B" : " B", i2, &B[i2]);
}
free(A);
free(B);
return 1;
}