Compare commits
10 Commits
81d3eb056b
...
f970a5173f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f970a5173f | ||
|
|
25eb17b91d | ||
|
|
f10484834c | ||
|
|
919e526fd8 | ||
|
|
2233fae235 | ||
|
|
f81e90f03a | ||
|
|
07f09f8fe0 | ||
|
|
4670afbeb5 | ||
|
|
87e4a67b61 | ||
|
|
1517a63974 |
@@ -314,9 +314,14 @@ else()
|
||||
)
|
||||
add_compile_options($<$<CONFIG:Release>:-O3>)
|
||||
# Link-time optimization (use lld and llvm-ar/ranlib to avoid system
|
||||
# linker/archiver LTO plugin version mismatch with clang's bitcode)
|
||||
add_compile_options($<$<CONFIG:Release>:-flto=thin>)
|
||||
add_link_options($<$<CONFIG:Release>:-flto=thin>)
|
||||
# linker/archiver LTO plugin version mismatch with clang's bitcode).
|
||||
# Guarded so memory-constrained hosts can build Release without the
|
||||
# ThinLTO link-time memory spike: pass -DXENIA_ENABLE_LTO=OFF.
|
||||
option(XENIA_ENABLE_LTO "Enable ThinLTO for Release builds" ON)
|
||||
if(XENIA_ENABLE_LTO)
|
||||
add_compile_options($<$<CONFIG:Release>:-flto=thin>)
|
||||
add_link_options($<$<CONFIG:Release>:-flto=thin>)
|
||||
endif()
|
||||
add_link_options($<$<CONFIG:Release>:-fuse-ld=lld>)
|
||||
find_program(LLVM_AR NAMES llvm-ar)
|
||||
find_program(LLVM_RANLIB NAMES llvm-ranlib)
|
||||
|
||||
@@ -1134,10 +1134,20 @@ void EmulatorWindow::OnMouseUp(const ui::MouseEvent& e) {
|
||||
void EmulatorWindow::TakeScreenshot() {
|
||||
xe::ui::RawImage image;
|
||||
|
||||
// Null-check the presenter BEFORE dereferencing it. The original condition
|
||||
// called CaptureGuestOutput() on the pointer and only tested it for null
|
||||
// afterwards, so pressing the screenshot key before/without a live presenter
|
||||
// dereferenced a null pointer and crashed.
|
||||
auto* presenter = GetGraphicsSystemPresenter();
|
||||
if (presenter == nullptr) {
|
||||
XELOGE("No graphics presenter available for screenshot");
|
||||
return;
|
||||
}
|
||||
|
||||
imgui_drawer_->EnableNotifications(false);
|
||||
|
||||
if (!GetGraphicsSystemPresenter()->CaptureGuestOutput(image) ||
|
||||
GetGraphicsSystemPresenter() == nullptr) {
|
||||
if (!presenter->CaptureGuestOutput(image)) {
|
||||
imgui_drawer_->EnableNotifications(true);
|
||||
XELOGE("Failed to capture guest output for screenshot");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -336,8 +336,30 @@ void ALSAAudioDriver::WorkerThread() {
|
||||
size_t current_write = write_index_.load(std::memory_order_acquire);
|
||||
|
||||
if (current_read == current_write) {
|
||||
// No data available, sleep and try again
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
// No guest audio available. During long guest stalls (e.g. a mission
|
||||
// "Preparing for Sortie" load) the game stops submitting frames; if we
|
||||
// merely sleep, ALSA drains its small buffer, XRUNs, and playback never
|
||||
// recovers (the game's audio is silent for the rest of the session).
|
||||
// Keep the PCM alive by topping it up with silence whenever the queued
|
||||
// audio runs low, so real audio resumes seamlessly once the stall ends.
|
||||
snd_pcm_sframes_t avail = snd_pcm_avail_update(pcm_handle_);
|
||||
if (avail < 0) {
|
||||
if (!RecoverFromUnderrun(avail)) {
|
||||
running_ = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
snd_pcm_uframes_t queued =
|
||||
(buffer_size_ > (snd_pcm_uframes_t)avail) ? buffer_size_ - avail : 0;
|
||||
if (queued < period_size_ * 2) {
|
||||
snd_pcm_sframes_t w =
|
||||
snd_pcm_writei(pcm_handle_, silence.get(), period_size_);
|
||||
if (w < 0) {
|
||||
RecoverFromUnderrun(w);
|
||||
}
|
||||
} else {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,22 @@ DEFINE_uint32(audit_jit_prolog_pc, 0,
|
||||
"bytes at host(r3). Zero (default) disables the probe. Audit-059 "
|
||||
"round 7+ JIT-prolog probe — generic PC-configurable variant.",
|
||||
"Auditing");
|
||||
DEFINE_uint32(audit_jit_prolog_mem_dump, 0,
|
||||
"Audit-059 round 14 — paired memory-dump VA. When non-zero "
|
||||
"AND audit_jit_prolog_pc fires, dereference the guest VA 3 "
|
||||
"levels deep and emit one XELOGKERNEL line: addr -> val "
|
||||
"(singleton instance), val -> vtable, vtable -> vtable[0] / "
|
||||
"vtable[24]. Reads the singleton at [0x828E1F08], its vtable, "
|
||||
"and its vtable[0] (first virtual method = bctrl target at "
|
||||
"sub_822F1AA8+0x90). Zero (default) disables.",
|
||||
"Auditing");
|
||||
DEFINE_uint32(audit_jit_prolog_r3_bytes, 0x40,
|
||||
"Audit-052 — number of bytes to dump from host(r3) on every "
|
||||
"audit_jit_prolog_pc fire (capped at 256, 16-byte aligned). "
|
||||
"Default 64 (existing behaviour). Set to 80 to capture the "
|
||||
"audit-051 stack-local struct at sub_82452DC0's r31+96 "
|
||||
"(probe sub_8245B000 entry where r3 IS the struct ptr).",
|
||||
"Auditing");
|
||||
DEFINE_bool(ignore_undefined_externs, true,
|
||||
"Don't exit when an undefined extern is called.", "CPU");
|
||||
DEFINE_bool(emit_source_annotations, false,
|
||||
@@ -482,11 +498,16 @@ uint64_t AuditLogJitPrologArgs(void* raw_context, uint64_t /*unused*/) {
|
||||
"r6={:08X} r7={:08X} r8={:08X} r9={:08X} r10={:08X} lr={:08X}",
|
||||
pc, tid, r3, r4, r5, r6, r7, r8, r9, r10, lr);
|
||||
|
||||
// Dump 64 bytes at host(r3) if r3 looks like a plausible guest VA.
|
||||
// Dump N bytes at host(r3) if r3 looks like a plausible guest VA.
|
||||
// N comes from cvar `audit_jit_prolog_r3_bytes` (default 64 = existing
|
||||
// behaviour). Round up to a 16-byte multiple; cap at 256.
|
||||
uint32_t r3_dump_bytes = static_cast<uint32_t>(cvars::audit_jit_prolog_r3_bytes);
|
||||
if (r3_dump_bytes > 256) r3_dump_bytes = 256;
|
||||
r3_dump_bytes = (r3_dump_bytes + 15) & ~15u;
|
||||
if (r3 >= 0x10000 && r3 < 0xE0000000) {
|
||||
uint8_t* host = ctx->TranslateVirtual(r3);
|
||||
if (host) {
|
||||
for (uint32_t off = 0; off < 0x40; off += 16) {
|
||||
for (uint32_t off = 0; off < r3_dump_bytes; off += 16) {
|
||||
uint32_t d0 = xe::load_and_swap<uint32_t>(host + off + 0);
|
||||
uint32_t d1 = xe::load_and_swap<uint32_t>(host + off + 4);
|
||||
uint32_t d2 = xe::load_and_swap<uint32_t>(host + off + 8);
|
||||
@@ -503,6 +524,28 @@ uint64_t AuditLogJitPrologArgs(void* raw_context, uint64_t /*unused*/) {
|
||||
XELOGKERNEL("AUDIT-HLC JitProlog pc={:08X} r3 out of VA range, skipping dump",
|
||||
pc);
|
||||
}
|
||||
|
||||
// Audit-059 round 14 — paired 3-level dereference. When
|
||||
// `audit_jit_prolog_mem_dump` is set, read the singleton at that VA,
|
||||
// its vtable, vtable[0] (first virtual method = bctrl target), and
|
||||
// vtable[24] (slot 6 = silph chain method per round 9).
|
||||
uint32_t mem_addr = static_cast<uint32_t>(cvars::audit_jit_prolog_mem_dump);
|
||||
if (mem_addr != 0u && mem_addr >= 0x10000 && mem_addr < 0xE0000000) {
|
||||
auto load_be32 = [&](uint32_t va) -> uint32_t {
|
||||
if (va < 0x10000 || va >= 0xE0000000) return 0u;
|
||||
uint8_t* h = ctx->TranslateVirtual(va);
|
||||
if (!h) return 0u;
|
||||
return xe::load_and_swap<uint32_t>(h);
|
||||
};
|
||||
uint32_t val = load_be32(mem_addr);
|
||||
uint32_t vtable = val != 0u ? load_be32(val) : 0u;
|
||||
uint32_t m0 = vtable != 0u ? load_be32(vtable) : 0u;
|
||||
uint32_t m6 = vtable != 0u ? load_be32(vtable + 24u) : 0u;
|
||||
XELOGKERNEL(
|
||||
"AUDIT-MEM-READ addr={:08X} val={:08X} vtable={:08X} vtable[0]={:08X} "
|
||||
"vtable[24]={:08X} pc={:08X} tid={:08X}",
|
||||
mem_addr, val, vtable, m0, m6, pc, tid);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
#include "xenia/gpu/command_processor.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "third_party/fmt/include/fmt/format.h"
|
||||
#include "xenia/base/byte_stream.h"
|
||||
#include "xenia/base/clock.h"
|
||||
@@ -18,6 +21,8 @@
|
||||
#include "xenia/gpu/gpu_flags.h"
|
||||
#include "xenia/gpu/graphics_system.h"
|
||||
#include "xenia/gpu/packet_disassembler.h"
|
||||
#include "xenia/gpu/registers.h"
|
||||
#include "xenia/gpu/shader.h"
|
||||
#include "xenia/gpu/sampler_info.h"
|
||||
#include "xenia/gpu/texture_info.h"
|
||||
#include "xenia/gpu/xenos_zpd_report.h"
|
||||
@@ -77,6 +82,14 @@ DEFINE_string(
|
||||
|
||||
UPDATE_from_string(readback_resolve, 2025, 12, 4, 21, "fast");
|
||||
|
||||
DEFINE_bool(
|
||||
log_draws, false,
|
||||
"Reverse-engineering aid: write each distinct draw's primitive type, index "
|
||||
"buffer, and per-stream vertex declaration (stream base/stride + per-element "
|
||||
"format/offset) to xenia_re_draws.log in the working directory. For decoding "
|
||||
"game mesh formats against GPU ground truth.",
|
||||
"GPU");
|
||||
|
||||
DEFINE_bool(
|
||||
readback_memexport, false,
|
||||
"Read data written by memory export in shaders on the CPU. "
|
||||
@@ -88,6 +101,171 @@ DEFINE_bool(
|
||||
namespace xe {
|
||||
namespace gpu {
|
||||
|
||||
namespace {
|
||||
// Short readable name for a guest vertex element format (RE logging only).
|
||||
const char* ReVertexFormatName(xenos::VertexFormat f) {
|
||||
switch (f) {
|
||||
case xenos::VertexFormat::k_32_FLOAT: return "f32";
|
||||
case xenos::VertexFormat::k_32_32_FLOAT: return "f32x2";
|
||||
case xenos::VertexFormat::k_32_32_32_FLOAT: return "f32x3";
|
||||
case xenos::VertexFormat::k_32_32_32_32_FLOAT: return "f32x4";
|
||||
case xenos::VertexFormat::k_16_16_FLOAT: return "f16x2";
|
||||
case xenos::VertexFormat::k_16_16_16_16_FLOAT: return "f16x4";
|
||||
case xenos::VertexFormat::k_16_16: return "s16x2";
|
||||
case xenos::VertexFormat::k_16_16_16_16: return "s16x4";
|
||||
case xenos::VertexFormat::k_8_8_8_8: return "8888";
|
||||
case xenos::VertexFormat::k_2_10_10_10: return "2_10_10_10";
|
||||
case xenos::VertexFormat::k_10_11_11: return "10_11_11";
|
||||
case xenos::VertexFormat::k_11_11_10: return "11_11_10";
|
||||
case xenos::VertexFormat::k_32: return "u32";
|
||||
case xenos::VertexFormat::k_32_32: return "u32x2";
|
||||
case xenos::VertexFormat::k_32_32_32_32: return "u32x4";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void CommandProcessor::LogDrawForRE(uint32_t vgt_draw_initiator_value,
|
||||
const IndexBufferInfo* index_buffer_info) {
|
||||
if (!cvars::log_draws) {
|
||||
return;
|
||||
}
|
||||
static std::mutex re_mutex;
|
||||
static std::ofstream re_out;
|
||||
static std::unordered_set<uint64_t> re_seen;
|
||||
std::lock_guard<std::mutex> lock(re_mutex);
|
||||
if (!re_out.is_open()) {
|
||||
re_out.open("xenia_re_draws.log", std::ios::out | std::ios::trunc);
|
||||
XELOGI("[RE-DRAW] logging distinct draws to xenia_re_draws.log");
|
||||
}
|
||||
if (!re_out.is_open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
reg::VGT_DRAW_INITIATOR init;
|
||||
init.value = vgt_draw_initiator_value;
|
||||
Shader* vs = active_vertex_shader_;
|
||||
|
||||
// De-dup by the VERTEX-DECLARATION FINGERPRINT (shader + primitive type +
|
||||
// per-stream element formats/offsets), NOT by buffer address. Animated UI
|
||||
// that redraws the same mesh format into fresh buffers every frame therefore
|
||||
// collapses to a single record, keeping the logging near-free — while every
|
||||
// distinct mesh format (the player plane, each weapon) is still captured once.
|
||||
uint64_t sig = 1469598103934665603ull; // FNV-ish seed
|
||||
auto mix = [&sig](uint64_t v) { sig = (sig ^ v) * 1099511628211ull; };
|
||||
mix(uint64_t(init.prim_type));
|
||||
if (vs) {
|
||||
mix(vs->ucode_data_hash());
|
||||
}
|
||||
bool analyzed = vs && vs->is_ucode_analyzed();
|
||||
if (analyzed) {
|
||||
for (const auto& binding : vs->vertex_bindings()) {
|
||||
mix(binding.fetch_constant);
|
||||
mix(binding.stride_words);
|
||||
for (const auto& attr : binding.attributes) {
|
||||
mix(uint64_t(attr.fetch_instr.attributes.data_format));
|
||||
mix(uint64_t(uint32_t(attr.fetch_instr.attributes.offset)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mix(uint64_t(init.num_indices));
|
||||
}
|
||||
if (!re_seen.insert(sig).second) {
|
||||
return;
|
||||
}
|
||||
// Safety cap on distinct formats, so a pathological title can't grow the log
|
||||
// (and the working set) without bound.
|
||||
if (re_seen.size() > 4096) {
|
||||
return;
|
||||
}
|
||||
|
||||
re_out << fmt::format("DRAW prim={} indices={} src={} ",
|
||||
uint32_t(init.prim_type), uint32_t(init.num_indices),
|
||||
uint32_t(init.source_select));
|
||||
if (index_buffer_info) {
|
||||
re_out << fmt::format(
|
||||
"index[base=0x{:08X} count={} fmt={} endian={} len={}] ",
|
||||
index_buffer_info->guest_base, index_buffer_info->count,
|
||||
index_buffer_info->format == xenos::IndexFormat::kInt16 ? "u16" : "u32",
|
||||
uint32_t(index_buffer_info->endianness), index_buffer_info->length);
|
||||
} else {
|
||||
re_out << "index[auto] ";
|
||||
}
|
||||
if (vs) {
|
||||
re_out << fmt::format("vs=0x{:016X}", vs->ucode_data_hash());
|
||||
}
|
||||
re_out << "\n";
|
||||
|
||||
if (analyzed) {
|
||||
for (const auto& binding : vs->vertex_bindings()) {
|
||||
xenos::xe_gpu_vertex_fetch_t fetch =
|
||||
register_file_->GetVertexFetch(binding.fetch_constant);
|
||||
re_out << fmt::format(
|
||||
" stream fc={} base=0x{:08X} stride_words={} size_words={} "
|
||||
"endian={} type={}\n",
|
||||
binding.fetch_constant, uint32_t(fetch.address) << 2,
|
||||
binding.stride_words, uint32_t(fetch.size), uint32_t(fetch.endian),
|
||||
uint32_t(fetch.type));
|
||||
for (const auto& attr : binding.attributes) {
|
||||
const auto& a = attr.fetch_instr.attributes;
|
||||
re_out << fmt::format(
|
||||
" attr fmt={}({}) offset_words={} stride_words={} signed={} "
|
||||
"int={} exp_adjust={}\n",
|
||||
uint32_t(a.data_format), ReVertexFormatName(a.data_format), a.offset,
|
||||
a.stride, a.is_signed ? 1 : 0, a.is_integer ? 1 : 0, a.exp_adjust);
|
||||
}
|
||||
}
|
||||
|
||||
// Dump the first few vertex POSITIONS from guest memory. The f32 position
|
||||
// bytes are identical between the guest buffer and the on-disc .xpr (only
|
||||
// f16 pairs are rearranged on load), so these values can be searched for in
|
||||
// the file to locate a mesh whose in-file offset is otherwise unknown
|
||||
// (e.g. multi-XBG7 body meshes). See docs/re/structures/xbg7-mesh.md.
|
||||
if (!vs->vertex_bindings().empty()) {
|
||||
const auto& binding = vs->vertex_bindings()[0];
|
||||
xenos::xe_gpu_vertex_fetch_t fetch =
|
||||
register_file_->GetVertexFetch(binding.fetch_constant);
|
||||
// Position = the first f32×3 attribute (offset is in dwords).
|
||||
int32_t pos_off_bytes = -1;
|
||||
for (const auto& attr : binding.attributes) {
|
||||
if (attr.fetch_instr.attributes.data_format ==
|
||||
xenos::VertexFormat::k_32_32_32_FLOAT) {
|
||||
pos_off_bytes = attr.fetch_instr.attributes.offset * 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint32_t stride = binding.stride_words * 4;
|
||||
uint32_t vbase = uint32_t(fetch.address) << 2;
|
||||
uint32_t buf_bytes = uint32_t(fetch.size) * 4;
|
||||
if (pos_off_bytes >= 0 && stride > 0) {
|
||||
uint32_t max_v = buf_bytes / stride;
|
||||
uint32_t n = max_v < 8 ? max_v : 8;
|
||||
re_out << " positions:";
|
||||
for (uint32_t v = 0; v < n; ++v) {
|
||||
uint32_t a = vbase + v * stride + uint32_t(pos_off_bytes);
|
||||
const uint8_t* p = memory_->TranslatePhysical<const uint8_t*>(a);
|
||||
if (!p) {
|
||||
break;
|
||||
}
|
||||
auto be_f32 = [](const uint8_t* q) {
|
||||
uint32_t w = (uint32_t(q[0]) << 24) | (uint32_t(q[1]) << 16) |
|
||||
(uint32_t(q[2]) << 8) | uint32_t(q[3]);
|
||||
float f;
|
||||
std::memcpy(&f, &w, 4);
|
||||
return f;
|
||||
};
|
||||
re_out << fmt::format(" ({:.4f},{:.4f},{:.4f})", be_f32(p),
|
||||
be_f32(p + 4), be_f32(p + 8));
|
||||
}
|
||||
re_out << "\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
re_out << " (vertex shader not analyzed yet)\n";
|
||||
}
|
||||
re_out.flush();
|
||||
}
|
||||
|
||||
// This should be written completely differently with support for different
|
||||
// types.
|
||||
void SaveGPUSetting(GPUSetting setting, uint64_t value) {
|
||||
|
||||
@@ -446,6 +446,13 @@ class CommandProcessor {
|
||||
}
|
||||
virtual bool IssueCopy() { return false; }
|
||||
|
||||
// Reverse-engineering aid (cvar `log_draws`): dump the guest's exact
|
||||
// primitive type + index buffer + per-stream vertex declaration for each
|
||||
// distinct draw to a dedicated file, for decoding game mesh formats.
|
||||
// No-op unless the cvar is enabled. Defined in command_processor.cc.
|
||||
void LogDrawForRE(uint32_t vgt_draw_initiator_value,
|
||||
const IndexBufferInfo* index_buffer_info);
|
||||
|
||||
// "Actual" is for the command processor thread, to be read by the
|
||||
// implementations.
|
||||
SwapPostEffect GetActualSwapPostEffect() const {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1152,6 +1152,11 @@ bool COMMAND_PROCESSOR::ExecutePacketType3Draw(
|
||||
uint32_t(vgt_draw_initiator.prim_type),
|
||||
uint32_t(vgt_draw_initiator.source_select));
|
||||
}
|
||||
// Reverse-engineering aid (no-op unless the `log_draws` cvar is set):
|
||||
// record the guest's primitive type + index buffer + vertex declaration.
|
||||
// Placed after IssueDraw so the vertex shader has been analyzed.
|
||||
COMMAND_PROCESSOR::LogDrawForRE(
|
||||
vgt_draw_initiator.value, is_indexed ? &index_buffer_info : nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,3 +18,8 @@ DEFINE_bool(audit_handle_lifecycle, false,
|
||||
"Emit XELOGKERNEL on Event/Semaphore/Wait lifecycle "
|
||||
"(create/set/wait/complete). Audit oracle probe — off by default.",
|
||||
"Auditing");
|
||||
DEFINE_uint32(audit_track_event_handle, 0,
|
||||
"If non-zero and AUDIT-HLC enabled, log underlying X_KEVENT "
|
||||
"guest VA whenever NtCreateEvent returns this handle. "
|
||||
"Audit round-24 oracle probe.",
|
||||
"Auditing");
|
||||
|
||||
@@ -14,5 +14,6 @@
|
||||
DECLARE_bool(headless);
|
||||
DECLARE_bool(log_high_frequency_kernel_calls);
|
||||
DECLARE_bool(audit_handle_lifecycle);
|
||||
DECLARE_uint32(audit_track_event_handle);
|
||||
|
||||
#endif // XENIA_KERNEL_KERNEL_FLAGS_H_
|
||||
|
||||
@@ -1071,6 +1071,14 @@ void KernelState::CompleteOverlappedEx(uint32_t overlapped_ptr, X_RESULT result,
|
||||
auto ev = object_table()->LookupObject<XEvent>(event_handle);
|
||||
assert_not_null(ev);
|
||||
if (ev) {
|
||||
if (cvars::audit_handle_lifecycle) {
|
||||
uint32_t lr =
|
||||
static_cast<uint32_t>(cpu::ThreadState::Get()->context()->lr);
|
||||
XELOGKERNEL(
|
||||
"AUDIT-HLC CompleteOverlappedEx_signal_event handle={:08X} "
|
||||
"kevent_va={:08X} lr={:08X}",
|
||||
uint32_t(event_handle), ev->guest_object(), lr);
|
||||
}
|
||||
ev->Set(0, false);
|
||||
}
|
||||
}
|
||||
@@ -1378,6 +1386,40 @@ void KernelState::EmulateCPInterruptDPC(uint32_t interrupt_callback,
|
||||
"AUDIT-HLC EmulateCPInterruptDPC callback={:08X} data={:08X} source={} "
|
||||
"cpu={}",
|
||||
interrupt_callback, interrupt_callback_data, source, cpu);
|
||||
|
||||
// AUDIT-2BF round 14 — one-shot singleton + vtable dump. Resolves
|
||||
// the silph init chain bctrl target at PC 0x822F1B4C (vtable[0] of
|
||||
// ANON_Class_2D56F86D at [0x828E1F08]). Dereferences 3 deep:
|
||||
// [0x828E1F08] (singleton) → vtable → vtable[0] (=first virtual
|
||||
// method, the bctrl target) and vtable[24] (=slot 6, canary's silph
|
||||
// chain target sub_821B55D8). Done once per process via atomic
|
||||
// flag. Defensive null-checks at each level.
|
||||
static std::atomic<bool> g_audit_singleton_dumped{false};
|
||||
bool expected = false;
|
||||
if (g_audit_singleton_dumped.compare_exchange_strong(expected, true)) {
|
||||
const uint32_t addr = 0x828E1F08;
|
||||
uint32_t val = 0, vtable = 0, m0 = 0, m6 = 0;
|
||||
auto* host_addr = memory()->TranslateVirtual<uint32_t*>(addr);
|
||||
if (host_addr) {
|
||||
val = xe::load_and_swap<uint32_t>(host_addr);
|
||||
}
|
||||
if (val) {
|
||||
auto* host_val = memory()->TranslateVirtual<uint32_t*>(val);
|
||||
if (host_val) {
|
||||
vtable = xe::load_and_swap<uint32_t>(host_val);
|
||||
}
|
||||
}
|
||||
if (vtable) {
|
||||
auto* host_vt0 = memory()->TranslateVirtual<uint32_t*>(vtable);
|
||||
auto* host_vt6 = memory()->TranslateVirtual<uint32_t*>(vtable + 24);
|
||||
if (host_vt0) m0 = xe::load_and_swap<uint32_t>(host_vt0);
|
||||
if (host_vt6) m6 = xe::load_and_swap<uint32_t>(host_vt6);
|
||||
}
|
||||
XELOGKERNEL(
|
||||
"AUDIT-HLC singleton[0x828E1F08]={:08X} vtable={:08X} "
|
||||
"vtable[0]={:08X} vtable[24]={:08X}",
|
||||
val, vtable, m0, m6);
|
||||
}
|
||||
}
|
||||
|
||||
auto thread = kernel::XThread::GetCurrentThread();
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
*/
|
||||
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/cpu/thread_state.h"
|
||||
#include "xenia/kernel/info/file.h"
|
||||
#include "xenia/kernel/kernel_flags.h"
|
||||
#include "xenia/kernel/kernel_state.h"
|
||||
#include "xenia/kernel/util/shim_utils.h"
|
||||
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.h"
|
||||
@@ -208,6 +210,13 @@ dword_result_t NtReadFile_entry(dword_t file_handle, dword_t event_handle,
|
||||
}
|
||||
|
||||
if (ev && signal_event) {
|
||||
if (cvars::audit_handle_lifecycle) {
|
||||
uint32_t lr = static_cast<uint32_t>(cpu::ThreadState::Get()->context()->lr);
|
||||
XELOGKERNEL(
|
||||
"AUDIT-HLC NtReadFile_signal_event handle={:08X} kevent_va={:08X} "
|
||||
"lr={:08X}",
|
||||
uint32_t(event_handle), ev->guest_object(), lr);
|
||||
}
|
||||
ev->Set(0, false);
|
||||
}
|
||||
|
||||
@@ -294,6 +303,13 @@ dword_result_t NtReadFileScatter_entry(
|
||||
}
|
||||
|
||||
if (ev && signal_event) {
|
||||
if (cvars::audit_handle_lifecycle) {
|
||||
uint32_t lr = static_cast<uint32_t>(cpu::ThreadState::Get()->context()->lr);
|
||||
XELOGKERNEL(
|
||||
"AUDIT-HLC NtReadFileScatter_signal_event handle={:08X} "
|
||||
"kevent_va={:08X} lr={:08X}",
|
||||
uint32_t(event_handle), ev->guest_object(), lr);
|
||||
}
|
||||
ev->Set(0, false);
|
||||
}
|
||||
|
||||
@@ -381,6 +397,13 @@ dword_result_t NtWriteFile_entry(dword_t file_handle, dword_t event_handle,
|
||||
}
|
||||
|
||||
if (ev && signal_event) {
|
||||
if (cvars::audit_handle_lifecycle) {
|
||||
uint32_t lr = static_cast<uint32_t>(cpu::ThreadState::Get()->context()->lr);
|
||||
XELOGKERNEL(
|
||||
"AUDIT-HLC NtWriteFile_signal_event handle={:08X} kevent_va={:08X} "
|
||||
"lr={:08X}",
|
||||
uint32_t(event_handle), ev->guest_object(), lr);
|
||||
}
|
||||
ev->Set(0, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include "xenia/kernel/xboxkrnl/xboxkrnl_ob.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/cpu/processor.h"
|
||||
#include "xenia/cpu/thread_state.h"
|
||||
#include "xenia/kernel/kernel_flags.h"
|
||||
#include "xenia/kernel/kernel_state.h"
|
||||
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.h"
|
||||
#include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h"
|
||||
@@ -398,6 +400,14 @@ dword_result_t NtDuplicateObject_entry(dword_t handle, lpdword_t new_handle_ptr,
|
||||
X_STATUS result =
|
||||
kernel_state()->object_table()->DuplicateHandle(handle, &new_handle);
|
||||
|
||||
if (cvars::audit_handle_lifecycle) {
|
||||
uint32_t lr = static_cast<uint32_t>(cpu::ThreadState::Get()->context()->lr);
|
||||
XELOGKERNEL(
|
||||
"AUDIT-HLC NtDuplicateObject src={:08X} dst={:08X} options={:08X} "
|
||||
"lr={:08X}",
|
||||
uint32_t(handle), uint32_t(new_handle), uint32_t(options), lr);
|
||||
}
|
||||
|
||||
if (new_handle_ptr) {
|
||||
*new_handle_ptr = new_handle;
|
||||
}
|
||||
|
||||
@@ -197,6 +197,30 @@ dword_result_t ExCreateThread_entry(lpdword_t handle_ptr, dword_t stack_size,
|
||||
lpvoid_t start_address,
|
||||
lpvoid_t start_context,
|
||||
dword_t creation_flags) {
|
||||
if (cvars::audit_handle_lifecycle) {
|
||||
auto* ctx = cpu::ThreadState::Get()->context();
|
||||
uint32_t lr = static_cast<uint32_t>(ctx->lr);
|
||||
// Walk one PPC stack frame up from the shim's frame to recover the GUEST
|
||||
// caller's LR (lr_enter is just the kernel-internal shim's return
|
||||
// address; we want the function that called *it*). Same convention as
|
||||
// the NtWaitForSingleObjectEx probe above.
|
||||
uint32_t guest_lr = 0;
|
||||
uint32_t sp = static_cast<uint32_t>(ctx->r[1]);
|
||||
uint32_t back1 = xe::load_and_swap<uint32_t>(ctx->TranslateVirtual(sp));
|
||||
if (back1) {
|
||||
uint32_t back2 =
|
||||
xe::load_and_swap<uint32_t>(ctx->TranslateVirtual(back1));
|
||||
if (back2) {
|
||||
guest_lr =
|
||||
xe::load_and_swap<uint32_t>(ctx->TranslateVirtual(back2 - 8));
|
||||
}
|
||||
}
|
||||
XELOGKERNEL(
|
||||
"AUDIT-HLC ExCreateThread entry={:08X} start_ctx={:08X} xapi={:08X} "
|
||||
"flags={:08X} lr={:08X} guest_lr={:08X}",
|
||||
uint32_t(start_address), uint32_t(start_context),
|
||||
uint32_t(xapi_thread_startup), uint32_t(creation_flags), lr, guest_lr);
|
||||
}
|
||||
return ExCreateThread(handle_ptr, stack_size, thread_id_ptr,
|
||||
xapi_thread_startup, start_address, start_context,
|
||||
creation_flags);
|
||||
@@ -688,11 +712,26 @@ dword_result_t NtCreateEvent_entry(
|
||||
}
|
||||
if (cvars::audit_handle_lifecycle) {
|
||||
uint32_t lr = static_cast<uint32_t>(cpu::ThreadState::Get()->context()->lr);
|
||||
uint32_t out_handle = handle_ptr ? uint32_t(*handle_ptr) : 0u;
|
||||
XELOGKERNEL(
|
||||
"AUDIT-HLC NtCreateEvent handle={:08X} type={} initial_state={} "
|
||||
"lr={:08X}",
|
||||
handle_ptr ? uint32_t(*handle_ptr) : 0u, uint32_t(event_type),
|
||||
uint32_t(initial_state), lr);
|
||||
out_handle, uint32_t(event_type), uint32_t(initial_state), lr);
|
||||
// Round-24: also log the underlying X_KEVENT guest VA for every
|
||||
// NtCreateEvent so KeSetEvent probes can be cross-referenced by ptr.
|
||||
XELOGKERNEL(
|
||||
"AUDIT-HLC NtCreateEvent_inner handle={:08X} kevent_va={:08X} "
|
||||
"lr={:08X}",
|
||||
out_handle, ev->guest_object(), lr);
|
||||
// When caller pinned a target handle, additionally emit the legacy
|
||||
// _target line so older round-24 grep recipes still match.
|
||||
if (cvars::audit_track_event_handle != 0 &&
|
||||
out_handle == cvars::audit_track_event_handle) {
|
||||
XELOGKERNEL(
|
||||
"AUDIT-HLC NtCreateEvent_target handle={:08X} object_va={:08X} "
|
||||
"lr={:08X}",
|
||||
out_handle, ev->guest_object(), lr);
|
||||
}
|
||||
}
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
@@ -720,9 +759,24 @@ uint32_t xeNtSetEvent(uint32_t handle, xe::be<uint32_t>* previous_state_ptr) {
|
||||
|
||||
dword_result_t NtSetEvent_entry(dword_t handle, lpdword_t previous_state_ptr) {
|
||||
if (cvars::audit_handle_lifecycle) {
|
||||
uint32_t lr = static_cast<uint32_t>(cpu::ThreadState::Get()->context()->lr);
|
||||
XELOGKERNEL("AUDIT-HLC NtSetEvent handle={:08X} lr={:08X}",
|
||||
uint32_t(handle), lr);
|
||||
auto* ctx = cpu::ThreadState::Get()->context();
|
||||
uint32_t lr = static_cast<uint32_t>(ctx->lr);
|
||||
// Round-24: walk PPC back-chain for caller's guest LR (same idiom as
|
||||
// NtWaitForSingleObjectEx probe).
|
||||
uint32_t guest_lr = 0;
|
||||
uint32_t sp = static_cast<uint32_t>(ctx->r[1]);
|
||||
uint32_t back1 = xe::load_and_swap<uint32_t>(ctx->TranslateVirtual(sp));
|
||||
if (back1) {
|
||||
uint32_t back2 =
|
||||
xe::load_and_swap<uint32_t>(ctx->TranslateVirtual(back1));
|
||||
if (back2) {
|
||||
guest_lr = xe::load_and_swap<uint32_t>(
|
||||
ctx->TranslateVirtual(back2 - 8));
|
||||
}
|
||||
}
|
||||
XELOGKERNEL(
|
||||
"AUDIT-HLC NtSetEvent handle={:08X} lr={:08X} guest_lr={:08X}",
|
||||
uint32_t(handle), lr, guest_lr);
|
||||
}
|
||||
return xeNtSetEvent(handle, previous_state_ptr);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "xenia/kernel/xconfig.h"
|
||||
|
||||
#include "xenia/base/cvar.h"
|
||||
#include "xenia/base/logging.h"
|
||||
|
||||
#include "xenia/base/filesystem.h"
|
||||
@@ -19,6 +20,17 @@
|
||||
|
||||
#include <ranges>
|
||||
|
||||
// Speaker configuration reported to the guest (XCONFIG_USER_AUDIO_FLAGS).
|
||||
// Default = 0 (Digital Stereo): the Linux/ALSA APU driver's 5.1 (6-channel)
|
||||
// path yields silence in some titles (e.g. Project Sylpheed missions), while
|
||||
// stereo output works, so stereo is the safe default. Set to 0x00010001 for
|
||||
// Dolby Digital + Pro Logic surround.
|
||||
DEFINE_uint32(guest_audio_flags, 0,
|
||||
"Speaker config reported to the guest (XCONFIG_USER_AUDIO_FLAGS): "
|
||||
"0 = Digital Stereo (safe on Linux/ALSA), 0x00010001 = Dolby "
|
||||
"Digital + Pro Logic surround.",
|
||||
"APU");
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
|
||||
@@ -94,7 +106,7 @@ void XConfig::SetDefaults() {
|
||||
xconfig_data_.user.language = static_cast<uint32_t>(XLanguage::kEnglish);
|
||||
xconfig_data_.user.country =
|
||||
static_cast<uint8_t>(XOnlineCountry::kUnitedStates);
|
||||
xconfig_data_.user.audio_flags = DolbyDigital | DolbyProLogic;
|
||||
xconfig_data_.user.audio_flags = cvars::guest_audio_flags;
|
||||
xconfig_data_.user.av_pack_hdmi_sz = XHDTVResolution.at(1).to_host();
|
||||
xconfig_data_.user.av_pack_component_sz = XHDTVResolution.at(1).to_host();
|
||||
xconfig_data_.user.av_pack_vga_sz = XVGAResolution.at(3).to_host();
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
#include "xenia/base/byte_stream.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/cpu/thread_state.h"
|
||||
#include "xenia/kernel/kernel_flags.h"
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
@@ -58,6 +60,13 @@ void XEvent::InitializeNative(void* native_ptr, X_DISPATCH_HEADER* header) {
|
||||
}
|
||||
|
||||
int32_t XEvent::Set(uint32_t priority_increment, bool wait) {
|
||||
if (cvars::audit_handle_lifecycle) {
|
||||
auto* ts = cpu::ThreadState::Get();
|
||||
uint32_t lr = ts ? static_cast<uint32_t>(ts->context()->lr) : 0u;
|
||||
XELOGKERNEL(
|
||||
"AUDIT-HLC XEvent::Set handle={:08X} kevent_va={:08X} prio={} lr={:08X}",
|
||||
handle(), guest_object(), priority_increment, lr);
|
||||
}
|
||||
set_priority_increment(priority_increment);
|
||||
event_->Set();
|
||||
return 1;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "xenia/base/threading.h"
|
||||
#include "xenia/cpu/processor.h"
|
||||
#include "xenia/emulator.h"
|
||||
#include "xenia/kernel/kernel_flags.h"
|
||||
#include "xenia/kernel/kernel_state.h"
|
||||
#include "xenia/kernel/user_module.h"
|
||||
#include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h"
|
||||
@@ -544,6 +545,13 @@ X_STATUS XThread::Terminate(int exit_code) {
|
||||
void XThread::Execute() {
|
||||
XELOGKERNEL("XThread::Execute thid {} (handle={:08X}, '{}', native={:08X})",
|
||||
thread_id_, handle(), thread_name_, thread_->system_id());
|
||||
if (cvars::audit_handle_lifecycle) {
|
||||
XELOGKERNEL(
|
||||
"AUDIT-HLC XThread::Execute tid={} start_address={:08X} "
|
||||
"start_context={:08X} xapi={:08X}",
|
||||
thread_id_, creation_params_.start_address,
|
||||
creation_params_.start_context, creation_params_.xapi_thread_startup);
|
||||
}
|
||||
// Let the kernel know we are starting.
|
||||
kernel_state()->OnThreadExecute(this);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user