audit: add poll-based arbitrary guest-VA memory-watch cvars
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 1m37s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 1m37s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
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/clock.h"
|
||||||
#include "xenia/base/logging.h"
|
#include "xenia/base/logging.h"
|
||||||
#include "xenia/base/math.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/profiling.h"
|
||||||
#include "xenia/base/threading.h"
|
#include "xenia/base/threading.h"
|
||||||
#include "xenia/config.h"
|
#include "xenia/config.h"
|
||||||
@@ -36,6 +38,32 @@ DEFINE_bool(
|
|||||||
"runtime spikes and freezes when playing the game not for the first time.",
|
"runtime spikes and freezes when playing the game not for the first time.",
|
||||||
"GPU");
|
"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 xe {
|
||||||
namespace gpu {
|
namespace gpu {
|
||||||
|
|
||||||
@@ -333,12 +361,117 @@ void GraphicsSystem::DispatchInterruptCallback(uint32_t source, uint32_t cpu) {
|
|||||||
interrupt_callback_data_, source, 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() {
|
void GraphicsSystem::MarkVblank() {
|
||||||
SCOPE_profile_cpu_f("gpu");
|
SCOPE_profile_cpu_f("gpu");
|
||||||
|
|
||||||
// Increment vblank counter (so the game sees us making progress).
|
// Increment vblank counter (so the game sees us making progress).
|
||||||
command_processor_->increment_counter();
|
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
|
// 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
|
// something wrong and the CP will block waiting for code that
|
||||||
// needs to be run in the interrupt.
|
// needs to be run in the interrupt.
|
||||||
|
|||||||
Reference in New Issue
Block a user