Source changes (dormant parity infra, retained from iterate 2.AI/2.AO): - xenia-kernel/exports.rs: nt_create_event manual_reset polarity + related event wiring - xenia-gpu/mmio_region.rs: D1MODE_VBLANK_VLINE_STATUS hardcode parity Also lands the audit-runs/ analysis notes (.md/.txt/.json digests) for the iterate 2.x VSync/0x10e8/0x1004 wedge investigation. Raw trace dumps (.jsonl/.gz/.csv/.stdout) and agent worktrees (.claude/) are gitignored as regenerable local artifacts — see memory + HANDOFF for the running findings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
666 lines
25 KiB
Diff
666 lines
25 KiB
Diff
diff --git a/src/xenia/cpu/backend/x64/x64_emitter.cc b/src/xenia/cpu/backend/x64/x64_emitter.cc
|
|
index 5da8f6adc..cbac9826c 100644
|
|
--- a/src/xenia/cpu/backend/x64/x64_emitter.cc
|
|
+++ b/src/xenia/cpu/backend/x64/x64_emitter.cc
|
|
@@ -13,6 +13,8 @@
|
|
|
|
#include <climits>
|
|
#include <cstring>
|
|
+#include <string>
|
|
+#include <vector>
|
|
|
|
#include "third_party/fmt/include/fmt/format.h"
|
|
#include "xenia/base/assert.h"
|
|
@@ -63,6 +65,76 @@ DEFINE_bool(instrument_call_times, false,
|
|
"Compute time taken for functions, for profiling guest code",
|
|
"x64");
|
|
#endif
|
|
+
|
|
+// AUDIT-061/067: forward decls of probe/watch tables (defined in
|
|
+// ppc_hir_builder.cc).
|
|
+namespace xe {
|
|
+namespace cpu {
|
|
+namespace audit61 {
|
|
+const std::vector<uint32_t>& pcs();
|
|
+} // namespace audit61
|
|
+namespace audit67 {
|
|
+const std::vector<uint32_t>& vals();
|
|
+} // namespace audit67
|
|
+} // namespace cpu
|
|
+} // namespace xe
|
|
+
|
|
+// AUDIT-061: handler for trap codes [200, 232). arg0 carries trap idx
|
|
+// (trap_code - 200), mapping to ::xe::cpu::audit61::pcs()[idx]. Emits one
|
|
+// log line per fire with cr0/cr6 LGE flags + key GPRs + LR + tid.
|
|
+static uint64_t TrapAudit61Branch(void* raw_context, uint64_t idx) {
|
|
+ auto* ctx = reinterpret_cast<xe::cpu::ppc::PPCContext_s*>(raw_context);
|
|
+ const auto& pcs = ::xe::cpu::audit61::pcs();
|
|
+ uint32_t pc = (idx < pcs.size()) ? pcs[static_cast<size_t>(idx)] : 0u;
|
|
+ uint32_t tid = 0;
|
|
+ if (ctx->thread_state) {
|
|
+ tid = ctx->thread_state->thread_id();
|
|
+ }
|
|
+ auto enc = [](uint8_t lt, uint8_t gt, uint8_t eq) {
|
|
+ char buf[4];
|
|
+ buf[0] = lt ? 'L' : '.';
|
|
+ buf[1] = gt ? 'G' : '.';
|
|
+ buf[2] = eq ? 'E' : '.';
|
|
+ buf[3] = '\0';
|
|
+ return std::string(buf);
|
|
+ };
|
|
+ XELOGI(
|
|
+ "AUDIT-061-BR pc={:08X} lr={:08X} cr0={} cr6={} r3={:08X} r4={:08X} "
|
|
+ "r5={:08X} r6={:08X} r31={:08X} tid={}",
|
|
+ pc, static_cast<uint32_t>(ctx->lr),
|
|
+ enc(ctx->cr0.cr0_lt, ctx->cr0.cr0_gt, ctx->cr0.cr0_eq),
|
|
+ enc(ctx->cr6.cr6_all_equal, ctx->cr6.cr6_1, ctx->cr6.cr6_none_equal),
|
|
+ static_cast<uint32_t>(ctx->r[3]), static_cast<uint32_t>(ctx->r[4]),
|
|
+ static_cast<uint32_t>(ctx->r[5]), static_cast<uint32_t>(ctx->r[6]),
|
|
+ static_cast<uint32_t>(ctx->r[31]), tid);
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+// AUDIT-067: handler for trap codes [250, 254). arg0 carries trap idx
|
|
+// (trap_code - 250), mapping to ::xe::cpu::audit67::vals()[idx]. Fired when
|
|
+// a 4-byte guest store sees the configured value. The store-emit site stashed
|
|
+// (pc << 32) | (ea & 0xFFFFFFFF) into ctx->scratch right before the trap.
|
|
+static uint64_t TrapAudit67ValueWatch(void* raw_context, uint64_t idx) {
|
|
+ auto* ctx = reinterpret_cast<xe::cpu::ppc::PPCContext_s*>(raw_context);
|
|
+ const auto& vals = ::xe::cpu::audit67::vals();
|
|
+ uint32_t val =
|
|
+ (idx < vals.size()) ? vals[static_cast<size_t>(idx)] : 0u;
|
|
+ uint32_t pc = static_cast<uint32_t>(ctx->scratch >> 32);
|
|
+ uint32_t dst = static_cast<uint32_t>(ctx->scratch & 0xFFFFFFFFu);
|
|
+ uint32_t tid = 0;
|
|
+ if (ctx->thread_state) {
|
|
+ tid = ctx->thread_state->thread_id();
|
|
+ }
|
|
+ XELOGI(
|
|
+ "AUDIT-067-VAL pc={:08X} lr={:08X} val={:08X} dst={:08X} "
|
|
+ "r3={:08X} r4={:08X} r5={:08X} r6={:08X} r31={:08X} tid={}",
|
|
+ pc, static_cast<uint32_t>(ctx->lr), val, dst,
|
|
+ static_cast<uint32_t>(ctx->r[3]), static_cast<uint32_t>(ctx->r[4]),
|
|
+ static_cast<uint32_t>(ctx->r[5]), static_cast<uint32_t>(ctx->r[6]),
|
|
+ static_cast<uint32_t>(ctx->r[31]), tid);
|
|
+ return 0;
|
|
+}
|
|
+
|
|
namespace xe {
|
|
namespace cpu {
|
|
namespace backend {
|
|
@@ -455,6 +527,20 @@ void X64Emitter::Trap(uint16_t trap_type) {
|
|
// ?
|
|
break;
|
|
default:
|
|
+ // AUDIT-067: trap codes [250, 254) dispatch the value-watch handler.
|
|
+ // arg0 = idx into ::xe::cpu::audit67::vals().
|
|
+ if (trap_type >= 250 && trap_type < 254) {
|
|
+ CallNative(::TrapAudit67ValueWatch,
|
|
+ static_cast<uint64_t>(trap_type - 250));
|
|
+ break;
|
|
+ }
|
|
+ // AUDIT-061: trap codes [200, 232) dispatch the branch-probe handler.
|
|
+ // arg0 = idx into ::xe::cpu::audit61::pcs().
|
|
+ if (trap_type >= 200 && trap_type < 232) {
|
|
+ CallNative(::TrapAudit61Branch,
|
|
+ static_cast<uint64_t>(trap_type - 200));
|
|
+ break;
|
|
+ }
|
|
XELOGW("Unknown trap type {}", trap_type);
|
|
db(0xCC);
|
|
break;
|
|
diff --git a/src/xenia/cpu/cpu_flags.cc b/src/xenia/cpu/cpu_flags.cc
|
|
index 3ff067e15..f78dad157 100644
|
|
--- a/src/xenia/cpu/cpu_flags.cc
|
|
+++ b/src/xenia/cpu/cpu_flags.cc
|
|
@@ -57,3 +57,25 @@ DEFINE_bool(break_condition_truncate, true, "truncate value to 32-bits", "CPU");
|
|
|
|
DEFINE_bool(break_on_debugbreak, true, "int3 on JITed __debugbreak requests.",
|
|
"CPU");
|
|
+
|
|
+// AUDIT-DEMO: smoke marker (memory entry: emulator.cc:225,283). Always-on bool.
|
|
+DEFINE_bool(audit_demo_setup_trace, true,
|
|
+ "Audit smoke marker: log AUDIT-DEMO-SETUP-BEGIN at emulator setup.",
|
|
+ "Audit");
|
|
+
|
|
+// AUDIT-061: comma-separated list of guest PCs to log on each fire.
|
|
+// Format: "0xPC1,0xPC2,..." (max 32 PCs). Each fire emits
|
|
+// AUDIT-061-BR pc=X lr=X cr0=LGE cr6=LGE r3=X r4=X r5=X r6=X r31=X tid=N.
|
|
+// Default empty (off); no perf cost when empty.
|
|
+DEFINE_string(audit_61_branch_probe_pcs, "",
|
|
+ "AUDIT-061: CSV of guest PCs to trace (cr0/cr6 + regs/tid).",
|
|
+ "Audit");
|
|
+
|
|
+// AUDIT-067: comma-separated list of u32 values to watch. When non-empty,
|
|
+// every 4-byte guest store (stw/stwu/stwx/stwux/stmw) emits a runtime
|
|
+// equality check; matches log AUDIT-067-VAL pc=X lr=X val=X dst=X r3..r6 r31 tid=N.
|
|
+// Max 4 values. Default empty (off); zero overhead when empty.
|
|
+DEFINE_string(audit_67_value_watch, "",
|
|
+ "AUDIT-067: CSV of u32 values (max 4) — log every guest "
|
|
+ "store whose value matches.",
|
|
+ "Audit");
|
|
diff --git a/src/xenia/cpu/cpu_flags.h b/src/xenia/cpu/cpu_flags.h
|
|
index 38c4f98ba..5f52647b5 100644
|
|
--- a/src/xenia/cpu/cpu_flags.h
|
|
+++ b/src/xenia/cpu/cpu_flags.h
|
|
@@ -35,4 +35,16 @@ DECLARE_bool(break_condition_truncate);
|
|
|
|
DECLARE_bool(break_on_debugbreak);
|
|
|
|
+// AUDIT-DEMO smoke marker.
|
|
+DECLARE_bool(audit_demo_setup_trace);
|
|
+
|
|
+// AUDIT-061: multi-PC branch probe — emits one log line per fire with
|
|
+// (pc, lr, cr0 LGE, cr6 LGE, r3, r4, r5, r6, r31, tid). CSV of guest PCs.
|
|
+DECLARE_string(audit_61_branch_probe_pcs);
|
|
+
|
|
+// AUDIT-067: value-watch — emit a log line for each 32-bit guest store whose
|
|
+// value-to-be-stored matches any configured value. CSV of u32 values
|
|
+// ("0xDEADBEEF,..."), max 4 entries. Default empty (off); zero cost when empty.
|
|
+DECLARE_string(audit_67_value_watch);
|
|
+
|
|
#endif // XENIA_CPU_CPU_FLAGS_H_
|
|
diff --git a/src/xenia/cpu/ppc/ppc_emit_altivec.cc b/src/xenia/cpu/ppc/ppc_emit_altivec.cc
|
|
index 513b21391..c9af025ff 100644
|
|
--- a/src/xenia/cpu/ppc/ppc_emit_altivec.cc
|
|
+++ b/src/xenia/cpu/ppc/ppc_emit_altivec.cc
|
|
@@ -9,12 +9,28 @@
|
|
|
|
#include "xenia/cpu/ppc/ppc_emit-private.h"
|
|
|
|
+#include <vector>
|
|
#include "xenia/base/assert.h"
|
|
+#include "xenia/cpu/cpu_flags.h"
|
|
#include "xenia/cpu/ppc/ppc_context.h"
|
|
#include "xenia/cpu/ppc/ppc_hir_builder.h"
|
|
|
|
#include <cmath>
|
|
|
|
+// AUDIT-067: forward-decls. Defined in ppc_emit_memory.cc / ppc_hir_builder.cc.
|
|
+namespace xe {
|
|
+namespace cpu {
|
|
+namespace audit67 {
|
|
+const std::vector<uint32_t>& vals();
|
|
+}
|
|
+namespace ppc {
|
|
+void EmitAudit67ValueWatchVec(PPCHIRBuilder& f, uint32_t pc,
|
|
+ ::xe::cpu::hir::Value* vec128,
|
|
+ ::xe::cpu::hir::Value* ea);
|
|
+}
|
|
+}
|
|
+}
|
|
+
|
|
namespace xe {
|
|
namespace cpu {
|
|
namespace ppc {
|
|
@@ -175,6 +191,21 @@ int InstrEmit_stvewx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd,
|
|
f.Shr(f.And(f.Truncate(ea, INT8_TYPE), f.LoadConstantUint8(0xF)), 2);
|
|
Value* v = f.Extract(f.LoadVR(vd), el, INT32_TYPE);
|
|
f.Store(ea, f.ByteSwap(v));
|
|
+ if (!::xe::cpu::audit67::vals().empty()) {
|
|
+ // For stvewx: only one lane is actually stored; piggyback on the scalar
|
|
+ // value-watch helper by emitting the equivalent of stw of v at ea.
|
|
+ Value* pc_hi64 =
|
|
+ f.LoadConstantUint64(static_cast<uint64_t>(i.address) << 32);
|
|
+ Value* ea_lo64 = f.ZeroExtend(f.Truncate(ea, INT32_TYPE), INT64_TYPE);
|
|
+ Value* packed = f.Or(pc_hi64, ea_lo64);
|
|
+ const auto& vals = ::xe::cpu::audit67::vals();
|
|
+ for (size_t idx = 0; idx < vals.size(); ++idx) {
|
|
+ Value* cmp = f.CompareEQ(v, f.LoadConstantUint32(vals[idx]));
|
|
+ f.StoreContext(offsetof(::xe::cpu::ppc::PPCContext, scratch), packed);
|
|
+ f.ContextBarrier();
|
|
+ f.TrapTrue(cmp, static_cast<uint16_t>(250 + idx));
|
|
+ }
|
|
+ }
|
|
return 0;
|
|
}
|
|
int InstrEmit_stvewx(PPCHIRBuilder& f, const InstrData& i) {
|
|
@@ -187,7 +218,11 @@ int InstrEmit_stvewx128(PPCHIRBuilder& f, const InstrData& i) {
|
|
int InstrEmit_stvx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd,
|
|
uint32_t ra, uint32_t rb) {
|
|
Value* ea = f.And(CalculateEA_0(f, ra, rb), f.LoadConstantUint64(~0xFull));
|
|
- f.Store(ea, f.ByteSwap(f.LoadVR(vd)));
|
|
+ Value* vec = f.LoadVR(vd);
|
|
+ f.Store(ea, f.ByteSwap(vec));
|
|
+ if (!::xe::cpu::audit67::vals().empty()) {
|
|
+ EmitAudit67ValueWatchVec(f, i.address, vec, ea);
|
|
+ }
|
|
return 0;
|
|
}
|
|
int InstrEmit_stvx(PPCHIRBuilder& f, const InstrData& i) {
|
|
diff --git a/src/xenia/cpu/ppc/ppc_emit_memory.cc b/src/xenia/cpu/ppc/ppc_emit_memory.cc
|
|
index b4bdabb49..a6b44697d 100644
|
|
--- a/src/xenia/cpu/ppc/ppc_emit_memory.cc
|
|
+++ b/src/xenia/cpu/ppc/ppc_emit_memory.cc
|
|
@@ -10,11 +10,22 @@
|
|
#include "xenia/cpu/ppc/ppc_emit-private.h"
|
|
|
|
#include <stddef.h>
|
|
+#include <vector>
|
|
#include "xenia/base/assert.h"
|
|
#include "xenia/base/cvar.h"
|
|
+#include "xenia/cpu/cpu_flags.h"
|
|
#include "xenia/cpu/ppc/ppc_context.h"
|
|
#include "xenia/cpu/ppc/ppc_hir_builder.h"
|
|
|
|
+// AUDIT-067: forward-decl of value-watch table (defined in ppc_hir_builder.cc).
|
|
+namespace xe {
|
|
+namespace cpu {
|
|
+namespace audit67 {
|
|
+const std::vector<uint32_t>& vals();
|
|
+} // namespace audit67
|
|
+} // namespace cpu
|
|
+} // namespace xe
|
|
+
|
|
DEFINE_bool(
|
|
disable_prefetch_and_cachecontrol, true,
|
|
"Disables translating ppc prefetch/cache flush instructions to host "
|
|
@@ -67,6 +78,90 @@ void StoreEA(PPCHIRBuilder& f, uint32_t rt, Value* ea) {
|
|
f.StoreGPR(rt, ea);
|
|
}
|
|
|
|
+// AUDIT-067: emit a runtime equality check on the 32-bit value-to-be-stored
|
|
+// against each configured watch value. On match, store (pc, EA) packed into
|
|
+// the PPCContext scratch field so the native trap handler can read them,
|
|
+// then fire a trap with code (kTrapBase + idx). Done host-side as a
|
|
+// build-time pc constant + a runtime EA truncate, packed as
|
|
+// (pc << 32) | (ea & 0xFFFFFFFF) so the handler can decompose.
|
|
+static void EmitAudit67ValueWatch(PPCHIRBuilder& f, uint32_t pc, Value* val32,
|
|
+ Value* ea) {
|
|
+ const auto& vals = ::xe::cpu::audit67::vals();
|
|
+ if (vals.empty()) return;
|
|
+ // pc is known at JIT time → emit as constant; ea is runtime.
|
|
+ Value* pc_hi64 = f.LoadConstantUint64(static_cast<uint64_t>(pc) << 32);
|
|
+ Value* ea_lo64 = f.ZeroExtend(f.Truncate(ea, INT32_TYPE), INT64_TYPE);
|
|
+ Value* packed = f.Or(pc_hi64, ea_lo64);
|
|
+ for (size_t idx = 0; idx < vals.size(); ++idx) {
|
|
+ Value* cmp = f.CompareEQ(val32, f.LoadConstantUint32(vals[idx]));
|
|
+ f.StoreContext(offsetof(::xe::cpu::ppc::PPCContext, scratch), packed);
|
|
+ f.ContextBarrier();
|
|
+ f.TrapTrue(cmp, static_cast<uint16_t>(250 + idx));
|
|
+ }
|
|
+}
|
|
+
|
|
+// AUDIT-067 128-bit (vector) variant: checks each of the 4 32-bit lanes in a
|
|
+// vector store. Used for stvx/stvxl/stvewx (memcpy-derived installs may use
|
|
+// 128-bit vector stores). The matched lane is reflected in the dst by
|
|
+// adding (lane * 4) so the handler can see exactly where in memory the
|
|
+// value lands. Declared with external linkage so altivec.cc can call it.
|
|
+void EmitAudit67ValueWatchVec(PPCHIRBuilder& f, uint32_t pc,
|
|
+ Value* vec128, Value* ea) {
|
|
+ const auto& vals = ::xe::cpu::audit67::vals();
|
|
+ if (vals.empty()) return;
|
|
+ Value* pc_hi64 = f.LoadConstantUint64(static_cast<uint64_t>(pc) << 32);
|
|
+ for (size_t idx = 0; idx < vals.size(); ++idx) {
|
|
+ Value* watch = f.LoadConstantUint32(vals[idx]);
|
|
+ for (uint8_t lane = 0; lane < 4; ++lane) {
|
|
+ Value* lane_val = f.Extract(vec128, lane, INT32_TYPE);
|
|
+ Value* cmp = f.CompareEQ(lane_val, watch);
|
|
+ Value* lane_off = f.LoadConstantUint32(static_cast<uint32_t>(lane * 4));
|
|
+ Value* dst32 = f.Add(f.Truncate(ea, INT32_TYPE), lane_off);
|
|
+ Value* packed = f.Or(pc_hi64, f.ZeroExtend(dst32, INT64_TYPE));
|
|
+ f.StoreContext(offsetof(::xe::cpu::ppc::PPCContext, scratch), packed);
|
|
+ f.ContextBarrier();
|
|
+ f.TrapTrue(cmp, static_cast<uint16_t>(250 + idx));
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+// AUDIT-067 64-bit variant: same as above but checks BOTH halves of a 64-bit
|
|
+// stored value. EA points at the start of the 8-byte store; the matched half
|
|
+// is encoded into the trap idx via (250 + 2*idx + half), where half=0 means
|
|
+// upper 32 bits (lower address), half=1 means lower 32 bits (upper address).
|
|
+static void EmitAudit67ValueWatch64(PPCHIRBuilder& f, uint32_t pc, Value* val64,
|
|
+ Value* ea) {
|
|
+ const auto& vals = ::xe::cpu::audit67::vals();
|
|
+ if (vals.empty()) return;
|
|
+ // PowerPC is big-endian: u64 stored at EA places upper-32 bits at EA+0
|
|
+ // and lower-32 bits at EA+4. Check both halves against each watch value.
|
|
+ Value* upper32 = f.Truncate(f.Shr(val64, int8_t(32)), INT32_TYPE); // bits[63:32]
|
|
+ Value* lower32 = f.Truncate(val64, INT32_TYPE); // bits[31:0]
|
|
+ Value* pc_hi64 = f.LoadConstantUint64(static_cast<uint64_t>(pc) << 32);
|
|
+ for (size_t idx = 0; idx < vals.size(); ++idx) {
|
|
+ // Upper half lands at EA+0.
|
|
+ {
|
|
+ Value* cmp = f.CompareEQ(upper32, f.LoadConstantUint32(vals[idx]));
|
|
+ Value* ea_lo64 = f.ZeroExtend(f.Truncate(ea, INT32_TYPE), INT64_TYPE);
|
|
+ Value* packed = f.Or(pc_hi64, ea_lo64);
|
|
+ f.StoreContext(offsetof(::xe::cpu::ppc::PPCContext, scratch), packed);
|
|
+ f.ContextBarrier();
|
|
+ f.TrapTrue(cmp, static_cast<uint16_t>(250 + idx));
|
|
+ }
|
|
+ // Lower half lands at EA+4.
|
|
+ {
|
|
+ Value* cmp = f.CompareEQ(lower32, f.LoadConstantUint32(vals[idx]));
|
|
+ Value* ea_plus4 =
|
|
+ f.Add(f.Truncate(ea, INT32_TYPE), f.LoadConstantUint32(4));
|
|
+ Value* ea_lo64 = f.ZeroExtend(ea_plus4, INT64_TYPE);
|
|
+ Value* packed = f.Or(pc_hi64, ea_lo64);
|
|
+ f.StoreContext(offsetof(::xe::cpu::ppc::PPCContext, scratch), packed);
|
|
+ f.ContextBarrier();
|
|
+ f.TrapTrue(cmp, static_cast<uint16_t>(250 + idx));
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
// Integer load (A-13)
|
|
|
|
int InstrEmit_lbz(PPCHIRBuilder& f, const InstrData& i) {
|
|
@@ -518,9 +613,11 @@ int InstrEmit_stw(PPCHIRBuilder& f, const InstrData& i) {
|
|
b = f.LoadGPR(i.D.RA);
|
|
}
|
|
Value* offset = f.LoadConstantInt64(XEEXTS16(i.D.DS));
|
|
- f.StoreOffset(b, offset,
|
|
- f.ByteSwap(f.Truncate(f.LoadGPR(i.D.RT), INT32_TYPE)));
|
|
-
|
|
+ Value* val32 = f.Truncate(f.LoadGPR(i.D.RT), INT32_TYPE);
|
|
+ f.StoreOffset(b, offset, f.ByteSwap(val32));
|
|
+ if (!::xe::cpu::audit67::vals().empty()) {
|
|
+ EmitAudit67ValueWatch(f, i.address, val32, f.Add(b, offset));
|
|
+ }
|
|
return 0;
|
|
}
|
|
|
|
@@ -532,10 +629,14 @@ int InstrEmit_stmw(PPCHIRBuilder& f, const InstrData& i) {
|
|
b = f.LoadGPR(i.D.RA);
|
|
}
|
|
|
|
+ const bool watch_active = !::xe::cpu::audit67::vals().empty();
|
|
for (uint32_t j = 0; j < 32 - i.D.RT; ++j) {
|
|
Value* offset = f.LoadConstantInt64(XEEXTS16(i.D.DS) + j * 4);
|
|
- f.StoreOffset(b, offset,
|
|
- f.ByteSwap(f.Truncate(f.LoadGPR(i.D.RT + j), INT32_TYPE)));
|
|
+ Value* val32 = f.Truncate(f.LoadGPR(i.D.RT + j), INT32_TYPE);
|
|
+ f.StoreOffset(b, offset, f.ByteSwap(val32));
|
|
+ if (watch_active) {
|
|
+ EmitAudit67ValueWatch(f, i.address, val32, f.Add(b, offset));
|
|
+ }
|
|
}
|
|
return 0;
|
|
}
|
|
@@ -545,8 +646,12 @@ int InstrEmit_stwu(PPCHIRBuilder& f, const InstrData& i) {
|
|
// MEM(EA, 4) <- (RS)[32:63]
|
|
// RA <- EA
|
|
Value* ea = CalculateEA_i(f, i.D.RA, XEEXTS16(i.D.DS));
|
|
- f.Store(ea, f.ByteSwap(f.Truncate(f.LoadGPR(i.D.RT), INT32_TYPE)));
|
|
+ Value* val32 = f.Truncate(f.LoadGPR(i.D.RT), INT32_TYPE);
|
|
+ f.Store(ea, f.ByteSwap(val32));
|
|
StoreEA(f, i.D.RA, ea);
|
|
+ if (!::xe::cpu::audit67::vals().empty()) {
|
|
+ EmitAudit67ValueWatch(f, i.address, val32, ea);
|
|
+ }
|
|
return 0;
|
|
}
|
|
|
|
@@ -555,8 +660,12 @@ int InstrEmit_stwux(PPCHIRBuilder& f, const InstrData& i) {
|
|
// MEM(EA, 4) <- (RS)[32:63]
|
|
// RA <- EA
|
|
Value* ea = CalculateEA(f, i.X.RA, i.X.RB);
|
|
- f.Store(ea, f.ByteSwap(f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE)));
|
|
+ Value* val32 = f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE);
|
|
+ f.Store(ea, f.ByteSwap(val32));
|
|
StoreEA(f, i.X.RA, ea);
|
|
+ if (!::xe::cpu::audit67::vals().empty()) {
|
|
+ EmitAudit67ValueWatch(f, i.address, val32, ea);
|
|
+ }
|
|
return 0;
|
|
}
|
|
|
|
@@ -568,7 +677,11 @@ int InstrEmit_stwx(PPCHIRBuilder& f, const InstrData& i) {
|
|
// EA <- b + (RB)
|
|
// MEM(EA, 4) <- (RS)[32:63]
|
|
Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB);
|
|
- f.Store(ea, f.ByteSwap(f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE)));
|
|
+ Value* val32 = f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE);
|
|
+ f.Store(ea, f.ByteSwap(val32));
|
|
+ if (!::xe::cpu::audit67::vals().empty()) {
|
|
+ EmitAudit67ValueWatch(f, i.address, val32, ea);
|
|
+ }
|
|
return 0;
|
|
}
|
|
|
|
@@ -587,7 +700,11 @@ int InstrEmit_std(PPCHIRBuilder& f, const InstrData& i) {
|
|
}
|
|
|
|
Value* offset = f.LoadConstantInt64(XEEXTS16(i.DS.DS << 2));
|
|
- f.StoreOffset(b, offset, f.ByteSwap(f.LoadGPR(i.DS.RT)));
|
|
+ Value* val64 = f.LoadGPR(i.DS.RT);
|
|
+ f.StoreOffset(b, offset, f.ByteSwap(val64));
|
|
+ if (!::xe::cpu::audit67::vals().empty()) {
|
|
+ EmitAudit67ValueWatch64(f, i.address, val64, f.Add(b, offset));
|
|
+ }
|
|
return 0;
|
|
}
|
|
|
|
@@ -596,8 +713,12 @@ int InstrEmit_stdu(PPCHIRBuilder& f, const InstrData& i) {
|
|
// MEM(EA, 8) <- (RS)
|
|
// RA <- EA
|
|
Value* ea = CalculateEA_i(f, i.DS.RA, XEEXTS16(i.DS.DS << 2));
|
|
- f.Store(ea, f.ByteSwap(f.LoadGPR(i.DS.RT)));
|
|
+ Value* val64 = f.LoadGPR(i.DS.RT);
|
|
+ f.Store(ea, f.ByteSwap(val64));
|
|
StoreEA(f, i.DS.RA, ea);
|
|
+ if (!::xe::cpu::audit67::vals().empty()) {
|
|
+ EmitAudit67ValueWatch64(f, i.address, val64, ea);
|
|
+ }
|
|
return 0;
|
|
}
|
|
|
|
@@ -606,8 +727,12 @@ int InstrEmit_stdux(PPCHIRBuilder& f, const InstrData& i) {
|
|
// MEM(EA, 8) <- (RS)
|
|
// RA <- EA
|
|
Value* ea = CalculateEA(f, i.X.RA, i.X.RB);
|
|
- f.Store(ea, f.ByteSwap(f.LoadGPR(i.X.RT)));
|
|
+ Value* val64 = f.LoadGPR(i.X.RT);
|
|
+ f.Store(ea, f.ByteSwap(val64));
|
|
StoreEA(f, i.X.RA, ea);
|
|
+ if (!::xe::cpu::audit67::vals().empty()) {
|
|
+ EmitAudit67ValueWatch64(f, i.address, val64, ea);
|
|
+ }
|
|
return 0;
|
|
}
|
|
|
|
@@ -619,7 +744,11 @@ int InstrEmit_stdx(PPCHIRBuilder& f, const InstrData& i) {
|
|
// EA <- b + (RB)
|
|
// MEM(EA, 8) <- (RS)
|
|
Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB);
|
|
- f.Store(ea, f.ByteSwap(f.LoadGPR(i.X.RT)));
|
|
+ Value* val64 = f.LoadGPR(i.X.RT);
|
|
+ f.Store(ea, f.ByteSwap(val64));
|
|
+ if (!::xe::cpu::audit67::vals().empty()) {
|
|
+ EmitAudit67ValueWatch64(f, i.address, val64, ea);
|
|
+ }
|
|
return 0;
|
|
}
|
|
|
|
@@ -684,7 +813,11 @@ int InstrEmit_stwbrx(PPCHIRBuilder& f, const InstrData& i) {
|
|
// EA <- b + (RB)
|
|
// MEM(EA, 4) <- bswap((RS)[32:63])
|
|
Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB);
|
|
- f.Store(ea, f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE));
|
|
+ Value* val32 = f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE);
|
|
+ f.Store(ea, val32);
|
|
+ if (!::xe::cpu::audit67::vals().empty()) {
|
|
+ EmitAudit67ValueWatch(f, i.address, val32, ea);
|
|
+ }
|
|
return 0;
|
|
}
|
|
|
|
@@ -696,7 +829,11 @@ int InstrEmit_stdbrx(PPCHIRBuilder& f, const InstrData& i) {
|
|
// EA <- b + (RB)
|
|
// MEM(EA, 8) <- bswap(RS)
|
|
Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB);
|
|
- f.Store(ea, f.LoadGPR(i.X.RT));
|
|
+ Value* val64 = f.LoadGPR(i.X.RT);
|
|
+ f.Store(ea, val64);
|
|
+ if (!::xe::cpu::audit67::vals().empty()) {
|
|
+ EmitAudit67ValueWatch64(f, i.address, val64, ea);
|
|
+ }
|
|
return 0;
|
|
}
|
|
|
|
@@ -843,7 +980,8 @@ int InstrEmit_stdcx(PPCHIRBuilder& f, const InstrData& i) {
|
|
// This will always succeed if under the global lock, however.
|
|
|
|
Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB);
|
|
- Value* rt = f.ByteSwap(f.LoadGPR(i.X.RT));
|
|
+ Value* val64 = f.LoadGPR(i.X.RT);
|
|
+ Value* rt = f.ByteSwap(val64);
|
|
|
|
if (cvars::no_reserved_ops) {
|
|
f.Store(ea, rt);
|
|
@@ -862,6 +1000,9 @@ int InstrEmit_stdcx(PPCHIRBuilder& f, const InstrData& i) {
|
|
if (!cvars::no_reserved_ops) {
|
|
f.MemoryBarrier();
|
|
}
|
|
+ if (!::xe::cpu::audit67::vals().empty()) {
|
|
+ EmitAudit67ValueWatch64(f, i.address, val64, ea);
|
|
+ }
|
|
return 0;
|
|
}
|
|
|
|
@@ -885,7 +1026,8 @@ int InstrEmit_stwcx(PPCHIRBuilder& f, const InstrData& i) {
|
|
|
|
Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB);
|
|
|
|
- Value* rt = f.ByteSwap(f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE));
|
|
+ Value* val32 = f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE);
|
|
+ Value* rt = f.ByteSwap(val32);
|
|
|
|
if (cvars::no_reserved_ops) {
|
|
f.Store(ea, rt);
|
|
@@ -904,7 +1046,9 @@ int InstrEmit_stwcx(PPCHIRBuilder& f, const InstrData& i) {
|
|
if (!cvars::no_reserved_ops) {
|
|
f.MemoryBarrier();
|
|
}
|
|
-
|
|
+ if (!::xe::cpu::audit67::vals().empty()) {
|
|
+ EmitAudit67ValueWatch(f, i.address, val32, ea);
|
|
+ }
|
|
return 0;
|
|
}
|
|
// Floating-point load (A-19)
|
|
diff --git a/src/xenia/cpu/ppc/ppc_hir_builder.cc b/src/xenia/cpu/ppc/ppc_hir_builder.cc
|
|
index 42d996cba..e2f7a45db 100644
|
|
--- a/src/xenia/cpu/ppc/ppc_hir_builder.cc
|
|
+++ b/src/xenia/cpu/ppc/ppc_hir_builder.cc
|
|
@@ -34,6 +34,97 @@ DEFINE_bool(
|
|
"unimplemented PowerPC instruction is encountered.",
|
|
"CPU");
|
|
|
|
+// AUDIT-061 — multi-PC branch probe. Parses cvars::audit_61_branch_probe_pcs
|
|
+// once and exposes a (pc -> trap_id) lookup table. trap_id range [200, 65535].
|
|
+// PCs outside the table are not probed. Native side reads g_audit61_pcs[idx].
|
|
+#include <vector>
|
|
+#include <string>
|
|
+namespace xe {
|
|
+namespace cpu {
|
|
+namespace audit61 {
|
|
+constexpr uint16_t kTrapBase = 200;
|
|
+constexpr size_t kMaxPcs = 32;
|
|
+static std::vector<uint32_t> g_pcs;
|
|
+static bool g_parsed = false;
|
|
+
|
|
+const std::vector<uint32_t>& pcs() {
|
|
+ if (!g_parsed) {
|
|
+ g_parsed = true;
|
|
+ const std::string& csv = cvars::audit_61_branch_probe_pcs;
|
|
+ size_t pos = 0;
|
|
+ while (pos < csv.size() && g_pcs.size() < kMaxPcs) {
|
|
+ size_t end = csv.find(',', pos);
|
|
+ std::string tok = csv.substr(pos, end - pos);
|
|
+ // strip 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()) {
|
|
+ try {
|
|
+ uint32_t v = static_cast<uint32_t>(std::stoul(tok, nullptr, 0));
|
|
+ g_pcs.push_back(v);
|
|
+ } catch (...) {
|
|
+ }
|
|
+ }
|
|
+ if (end == std::string::npos) break;
|
|
+ pos = end + 1;
|
|
+ }
|
|
+ }
|
|
+ return g_pcs;
|
|
+}
|
|
+
|
|
+// Returns trap id for pc, or 0 if pc not in probe set.
|
|
+uint16_t trap_id_for(uint32_t pc) {
|
|
+ const auto& v = pcs();
|
|
+ for (size_t i = 0; i < v.size(); ++i) {
|
|
+ if (v[i] == pc) return static_cast<uint16_t>(kTrapBase + i);
|
|
+ }
|
|
+ return 0;
|
|
+}
|
|
+} // namespace audit61
|
|
+
|
|
+// AUDIT-067 — value-watch. Parses cvars::audit_67_value_watch once, exposes
|
|
+// values via vals(). Trap codes for matches start at kTrapBase = 250.
|
|
+namespace audit67 {
|
|
+constexpr uint16_t kTrapBase = 250;
|
|
+constexpr size_t kMaxVals = 4;
|
|
+static std::vector<uint32_t> g_vals;
|
|
+static bool g_parsed = false;
|
|
+
|
|
+const std::vector<uint32_t>& vals() {
|
|
+ if (!g_parsed) {
|
|
+ g_parsed = true;
|
|
+ const std::string& csv = cvars::audit_67_value_watch;
|
|
+ size_t pos = 0;
|
|
+ while (pos < csv.size() && g_vals.size() < kMaxVals) {
|
|
+ size_t end = csv.find(',', pos);
|
|
+ std::string tok = csv.substr(pos, end - pos);
|
|
+ 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()) {
|
|
+ try {
|
|
+ uint32_t v = static_cast<uint32_t>(std::stoul(tok, nullptr, 0));
|
|
+ g_vals.push_back(v);
|
|
+ } catch (...) {
|
|
+ }
|
|
+ }
|
|
+ if (end == std::string::npos) break;
|
|
+ pos = end + 1;
|
|
+ }
|
|
+ XELOGI("AUDIT-067-INIT csv=\"{}\" parsed_count={}", csv, g_vals.size());
|
|
+ for (size_t i = 0; i < g_vals.size(); ++i) {
|
|
+ XELOGI("AUDIT-067-INIT vals[{}] = 0x{:08X}", i, g_vals[i]);
|
|
+ }
|
|
+ }
|
|
+ return g_vals;
|
|
+}
|
|
+} // namespace audit67
|
|
+} // namespace cpu
|
|
+} // namespace xe
|
|
+
|
|
namespace xe {
|
|
namespace cpu {
|
|
namespace ppc {
|
|
@@ -174,6 +265,20 @@ bool PPCHIRBuilder::Emit(GuestFunction* function, uint32_t flags) {
|
|
|
|
MaybeBreakOnInstruction(address);
|
|
|
|
+ // AUDIT-061: emit a trap before this instruction if it's on the probe
|
|
+ // list. The trap fires BEFORE the cmp/branch HIR emit so the native
|
|
+ // handler observes cr0/cr6 set by the *previous* instruction (the cmp
|
|
+ // that controls this conditional branch). ContextBarrier flushes
|
|
+ // HIR temporaries to PPCContext so the handler reads consistent state.
|
|
+ if (!::xe::cpu::audit61::pcs().empty()) {
|
|
+ uint16_t tid = ::xe::cpu::audit61::trap_id_for(address);
|
|
+ if (tid != 0) {
|
|
+ Comment("--audit_61_branch_probe target");
|
|
+ ContextBarrier();
|
|
+ Trap(tid);
|
|
+ }
|
|
+ }
|
|
+
|
|
InstrData i;
|
|
i.address = address;
|
|
i.code = code;
|