audit: add poll-based arbitrary guest-VA memory-watch cvars
Adds audit_mem_watch_addr (comma-separated hex guest VAs) and audit_mem_watch_size (1/2/4/8 bytes) to watch arbitrary guest memory locations at runtime. Once per vblank, GraphicsSystem::MarkVblank() reads each VA big-endian via Memory::TranslateVirtual and emits an XELOGKERNEL "AUDIT-MEM-WATCH" line on every value change vs the prior frame (same greppable log stream as AUDIT-HLC / AUDIT-MEM-READ). Mechanism is poll / value-change: it captures WHEN a value changes (vblank index) but NOT the writer guest-PC; pair with audit_jit_prolog_pc on a suspected writer to recover the PC. Heap VAs vary per run/engine, so they are supplied at runtime via cvar/CLI. Default empty => disabled => zero overhead and no emulation-behavior change. Validated against the controller vsync "clock A" field (+0x58): the watch fires once per change, advancing monotonically in lockstep with vblanks. Game still boots and runs muted with the cvar unset. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,8 @@
|
||||
#include "xenia/base/clock.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/base/math.h"
|
||||
#include "xenia/base/memory.h"
|
||||
#include "xenia/base/string_util.h"
|
||||
#include "xenia/base/profiling.h"
|
||||
#include "xenia/base/threading.h"
|
||||
#include "xenia/config.h"
|
||||
@@ -36,6 +38,32 @@ DEFINE_bool(
|
||||
"runtime spikes and freezes when playing the game not for the first time.",
|
||||
"GPU");
|
||||
|
||||
// Audit memory-watch: poll-based value-change logger for arbitrary guest VAs.
|
||||
// Reads the configured guest virtual address(es) once per vblank (per frame)
|
||||
// from inside GraphicsSystem::MarkVblank() and emits one XELOGKERNEL
|
||||
// "AUDIT-MEM-WATCH" line whenever the read value changes vs the previous
|
||||
// frame. Greppable on the same log stream as AUDIT-HLC / AUDIT-MEM-READ.
|
||||
// Default empty => disabled => zero overhead. Mechanism is poll/value-change
|
||||
// (NOT a write-trap), so it captures WHEN a value changes (vblank index +
|
||||
// emulator instruction count) but NOT the writer guest-PC. To find the writer
|
||||
// PC, pair this with audit_jit_prolog_pc on the suspected writer function.
|
||||
DEFINE_string(
|
||||
audit_mem_watch_addr, "",
|
||||
"Audit memory-watch — comma-separated list of guest virtual addresses "
|
||||
"(hex, e.g. \"0x40d09a40\" or \"0x40d09a40,0x40929c00\") to poll once per "
|
||||
"vblank. On every value change vs the previous frame, emit one "
|
||||
"XELOGKERNEL AUDIT-MEM-WATCH line (watched VA, old value, new value, "
|
||||
"vblank index, instruction count). Empty (default) disables the watch. "
|
||||
"Poll-based value-change log: tells you WHEN a value changes, not the "
|
||||
"writer PC.",
|
||||
"Auditing");
|
||||
DEFINE_uint32(
|
||||
audit_mem_watch_size, 4,
|
||||
"Audit memory-watch — number of bytes to read at each watched VA (1, 2, "
|
||||
"4, or 8). Read big-endian (guest byte order) and compared as a 64-bit "
|
||||
"value. Default 4.",
|
||||
"Auditing");
|
||||
|
||||
namespace xe {
|
||||
namespace gpu {
|
||||
|
||||
@@ -333,12 +361,117 @@ void GraphicsSystem::DispatchInterruptCallback(uint32_t source, uint32_t cpu) {
|
||||
interrupt_callback_data_, source, cpu);
|
||||
}
|
||||
|
||||
// Audit memory-watch poll. Called once per vblank from MarkVblank(). Parses
|
||||
// the comma-separated VA list from cvars::audit_mem_watch_addr exactly once
|
||||
// (cached), then reads each VA (big-endian, audit_mem_watch_size bytes) and
|
||||
// logs an AUDIT-MEM-WATCH line on every value change vs the previous frame.
|
||||
// Poll/value-change mechanism: captures WHEN, not the writer PC. Zero work
|
||||
// when the cvar is empty.
|
||||
static void AuditMemWatchPoll(Memory* memory, uint64_t vblank_index) {
|
||||
struct WatchEntry {
|
||||
uint32_t va;
|
||||
uint64_t last_value;
|
||||
bool have_last;
|
||||
};
|
||||
static std::vector<WatchEntry> entries;
|
||||
static std::string parsed_spec;
|
||||
static bool parse_failed = false;
|
||||
|
||||
const std::string& spec = cvars::audit_mem_watch_addr;
|
||||
if (spec.empty()) {
|
||||
return;
|
||||
}
|
||||
// (Re)parse only when the cvar string changes (set once at startup).
|
||||
if (spec != parsed_spec) {
|
||||
parsed_spec = spec;
|
||||
entries.clear();
|
||||
parse_failed = false;
|
||||
size_t start = 0;
|
||||
while (start <= spec.size()) {
|
||||
size_t comma = spec.find(',', start);
|
||||
std::string tok = spec.substr(
|
||||
start, comma == std::string::npos ? std::string::npos : comma - start);
|
||||
// Trim whitespace.
|
||||
while (!tok.empty() && (tok.front() == ' ' || tok.front() == '\t'))
|
||||
tok.erase(tok.begin());
|
||||
while (!tok.empty() && (tok.back() == ' ' || tok.back() == '\t'))
|
||||
tok.pop_back();
|
||||
if (!tok.empty()) {
|
||||
uint32_t va = 0;
|
||||
try {
|
||||
va = static_cast<uint32_t>(std::stoul(tok, nullptr, 16));
|
||||
} catch (...) {
|
||||
XELOGKERNEL("AUDIT-MEM-WATCH bad VA token '{}' in '{}'", tok, spec);
|
||||
parse_failed = true;
|
||||
va = 0;
|
||||
}
|
||||
if (va != 0) {
|
||||
entries.push_back({va, 0, false});
|
||||
}
|
||||
}
|
||||
if (comma == std::string::npos) break;
|
||||
start = comma + 1;
|
||||
}
|
||||
XELOGKERNEL("AUDIT-MEM-WATCH armed: {} address(es), size={} bytes",
|
||||
entries.size(),
|
||||
static_cast<uint32_t>(cvars::audit_mem_watch_size));
|
||||
}
|
||||
if (entries.empty()) {
|
||||
return;
|
||||
}
|
||||
(void)parse_failed;
|
||||
|
||||
uint32_t size = static_cast<uint32_t>(cvars::audit_mem_watch_size);
|
||||
for (auto& e : entries) {
|
||||
if (e.va < 0x10000 || e.va >= 0xE0000000) {
|
||||
continue;
|
||||
}
|
||||
uint8_t* host = memory->TranslateVirtual(e.va);
|
||||
if (!host) {
|
||||
continue;
|
||||
}
|
||||
uint64_t value = 0;
|
||||
switch (size) {
|
||||
case 1:
|
||||
value = *host;
|
||||
break;
|
||||
case 2:
|
||||
value = xe::load_and_swap<uint16_t>(host);
|
||||
break;
|
||||
case 8:
|
||||
value = xe::load_and_swap<uint64_t>(host);
|
||||
break;
|
||||
case 4:
|
||||
default:
|
||||
value = xe::load_and_swap<uint32_t>(host);
|
||||
break;
|
||||
}
|
||||
if (!e.have_last) {
|
||||
e.have_last = true;
|
||||
e.last_value = value;
|
||||
XELOGKERNEL(
|
||||
"AUDIT-MEM-WATCH va={:08X} init={:016X} vblank={} (first read)",
|
||||
e.va, value, vblank_index);
|
||||
continue;
|
||||
}
|
||||
if (value != e.last_value) {
|
||||
XELOGKERNEL(
|
||||
"AUDIT-MEM-WATCH va={:08X} old={:016X} new={:016X} vblank={}",
|
||||
e.va, e.last_value, value, vblank_index);
|
||||
e.last_value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GraphicsSystem::MarkVblank() {
|
||||
SCOPE_profile_cpu_f("gpu");
|
||||
|
||||
// Increment vblank counter (so the game sees us making progress).
|
||||
command_processor_->increment_counter();
|
||||
|
||||
// Audit memory-watch (poll-based; no-op unless audit_mem_watch_addr set).
|
||||
AuditMemWatchPoll(memory_, command_processor_->counter());
|
||||
|
||||
// TODO(benvanik): we shouldn't need to do the dispatch here, but there's
|
||||
// something wrong and the CP will block waiting for code that
|
||||
// needs to be run in the interrupt.
|
||||
|
||||
Reference in New Issue
Block a user