Compare commits
8 Commits
capture-sh
...
auto/re-fr
| Author | SHA1 | Date | |
|---|---|---|---|
| 4040c701c5 | |||
| 15fe11d5d9 | |||
| e3e17e4951 | |||
| d15c8cfab6 | |||
| 31366e5cac | |||
|
|
a08526dff0 | ||
|
|
93fe5d69df | ||
|
|
067a373734 |
@@ -1067,6 +1067,11 @@ void EmulatorWindow::OnKeyDown(ui::KeyEvent& e) {
|
|||||||
case ui::VirtualKey::kF12: {
|
case ui::VirtualKey::kF12: {
|
||||||
TakeScreenshot();
|
TakeScreenshot();
|
||||||
} break;
|
} break;
|
||||||
|
case ui::VirtualKey::kF10: {
|
||||||
|
// RE: snapshot the next frame's draws (world-space vertex positions) to
|
||||||
|
// xenia_ship_capture.log for capital-ship placement correlation.
|
||||||
|
xe::gpu::RequestShipCaptureFrame();
|
||||||
|
} break;
|
||||||
|
|
||||||
case ui::VirtualKey::kEscape: {
|
case ui::VirtualKey::kEscape: {
|
||||||
// Allow users to escape fullscreen (but not enter it).
|
// Allow users to escape fullscreen (but not enter it).
|
||||||
|
|||||||
@@ -64,6 +64,12 @@
|
|||||||
#include "xenia/hid/winkey/winkey_hid.h"
|
#include "xenia/hid/winkey/winkey_hid.h"
|
||||||
#include "xenia/hid/xinput/xinput_hid.h"
|
#include "xenia/hid/xinput/xinput_hid.h"
|
||||||
#endif // XE_PLATFORM_WIN32
|
#endif // XE_PLATFORM_WIN32
|
||||||
|
// RE aid: a controller driven by a text file instead of a kernel input device.
|
||||||
|
// A uinput pad created inside a container registers with the HOST's input stack
|
||||||
|
// (input devices are not namespaced), so scripted input leaks to the desktop.
|
||||||
|
// This one is visible only to whoever can read the file. Header-only on purpose:
|
||||||
|
// it adds no build target.
|
||||||
|
#include "xenia/hid/file/file_input_driver.h"
|
||||||
|
|
||||||
#if XE_PLATFORM_WIN32
|
#if XE_PLATFORM_WIN32
|
||||||
#define APU_OPTIONS "[any, nop, sdl, xaudio2]"
|
#define APU_OPTIONS "[any, nop, sdl, xaudio2]"
|
||||||
@@ -72,7 +78,7 @@
|
|||||||
#elif XE_PLATFORM_LINUX
|
#elif XE_PLATFORM_LINUX
|
||||||
#define APU_OPTIONS "[any, alsa, nop, sdl]"
|
#define APU_OPTIONS "[any, alsa, nop, sdl]"
|
||||||
#define GPU_OPTIONS "[any, vulkan, null]"
|
#define GPU_OPTIONS "[any, vulkan, null]"
|
||||||
#define HID_OPTIONS "[any, nop, sdl]"
|
#define HID_OPTIONS "[any, file, nop, sdl]"
|
||||||
#else
|
#else
|
||||||
#define APU_OPTIONS "[any, nop, sdl]"
|
#define APU_OPTIONS "[any, nop, sdl]"
|
||||||
#define GPU_OPTIONS "[any, vulkan, null]"
|
#define GPU_OPTIONS "[any, vulkan, null]"
|
||||||
@@ -82,6 +88,11 @@
|
|||||||
DEFINE_string(apu, "any", "Audio system. Use: " APU_OPTIONS, "APU");
|
DEFINE_string(apu, "any", "Audio system. Use: " APU_OPTIONS, "APU");
|
||||||
DEFINE_string(gpu, "any", "Graphics system. Use: " GPU_OPTIONS, "GPU");
|
DEFINE_string(gpu, "any", "Graphics system. Use: " GPU_OPTIONS, "GPU");
|
||||||
DEFINE_string(hid, "any", "Input system. Use: " HID_OPTIONS, "HID");
|
DEFINE_string(hid, "any", "Input system. Use: " HID_OPTIONS, "HID");
|
||||||
|
DEFINE_string(pad_file, "/tmp/xenia_pad.txt",
|
||||||
|
"Controller state file read by `--hid=file`: key=value pairs such "
|
||||||
|
"as `press=A,START lt=0 rt=255 lx=0 ly=0`. Absent keys are "
|
||||||
|
"neutral, a missing file means no input.",
|
||||||
|
"HID");
|
||||||
|
|
||||||
DEFINE_path(
|
DEFINE_path(
|
||||||
storage_root, "",
|
storage_root, "",
|
||||||
@@ -446,6 +457,10 @@ std::vector<std::unique_ptr<hid::InputDriver>> EmulatorApp::CreateInputDrivers(
|
|||||||
if (cvars::hid.compare("nop") == 0) {
|
if (cvars::hid.compare("nop") == 0) {
|
||||||
drivers.emplace_back(
|
drivers.emplace_back(
|
||||||
xe::hid::nop::Create(window, EmulatorWindow::kZOrderHidInput));
|
xe::hid::nop::Create(window, EmulatorWindow::kZOrderHidInput));
|
||||||
|
} else if (cvars::hid.compare("file") == 0) {
|
||||||
|
// Explicit, never part of "any": this pad must be asked for.
|
||||||
|
drivers.emplace_back(
|
||||||
|
xe::hid::filepad::Create(window, EmulatorWindow::kZOrderHidInput));
|
||||||
} else {
|
} else {
|
||||||
Factory<hid::InputDriver, ui::Window*, size_t> factory;
|
Factory<hid::InputDriver, ui::Window*, size_t> factory;
|
||||||
#if XE_PLATFORM_WIN32
|
#if XE_PLATFORM_WIN32
|
||||||
|
|||||||
@@ -125,8 +125,210 @@ const char* ReVertexFormatName(xenos::VertexFormat f) {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
// ── Ship-placement capture (RE) ─────────────────────────────────────────────
|
||||||
|
// A hotkey (F10 in the emulator window) requests a one-shot snapshot: the next
|
||||||
|
// batch of draws is dumped to xenia_ship_capture_NN.log (NN = press number —
|
||||||
|
// several angles can be captured in one run without overwriting) with each
|
||||||
|
// draw's guest vertex-buffer address, vertex/index counts, up to 64 LOCAL
|
||||||
|
// vertex positions, and the first 48 VS float constants (c0..c2 = the WVP rows
|
||||||
|
// that place the part). De-duped by (buffer address, c0..c2 transform) — NOT by
|
||||||
|
// address alone — so the SAME part buffer drawn several times (two engine
|
||||||
|
// nacelles, every ship of a fleet) yields one record per distinct placement.
|
||||||
|
namespace {
|
||||||
|
constexpr int kShipCaptureBudget = 8000; // draws to scan per request
|
||||||
|
std::atomic<uint32_t> g_ship_capture_gen{0};
|
||||||
|
std::atomic<int> g_ship_capture_remaining{0};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void RequestShipCaptureFrame() {
|
||||||
|
uint32_t gen = g_ship_capture_gen.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||||
|
g_ship_capture_remaining.store(kShipCaptureBudget, std::memory_order_relaxed);
|
||||||
|
XELOGI("[SHIP-CAP] capture armed → xenia_ship_capture_{:02d}.log", gen);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommandProcessor::CaptureShipDrawForRE(
|
||||||
|
uint32_t vgt_draw_initiator_value,
|
||||||
|
const IndexBufferInfo* index_buffer_info) {
|
||||||
|
if (g_ship_capture_remaining.load(std::memory_order_relaxed) <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
static std::mutex cap_mutex;
|
||||||
|
static std::ofstream cap_out;
|
||||||
|
static std::unordered_set<uint64_t> cap_seen; // by (vbase, WVP transform)
|
||||||
|
static uint32_t cap_gen = 0;
|
||||||
|
std::lock_guard<std::mutex> lock(cap_mutex);
|
||||||
|
if (g_ship_capture_remaining.load(std::memory_order_relaxed) <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A new request (fresh generation) opens its own numbered snapshot file and
|
||||||
|
// clears the de-dup set, so each F10 press yields an independent capture.
|
||||||
|
uint32_t gen = g_ship_capture_gen.load(std::memory_order_relaxed);
|
||||||
|
if (gen != cap_gen || !cap_out.is_open()) {
|
||||||
|
if (cap_out.is_open()) {
|
||||||
|
cap_out.close();
|
||||||
|
}
|
||||||
|
auto name = fmt::format("xenia_ship_capture_{:02d}.log", gen);
|
||||||
|
cap_out.open(name, std::ios::out | std::ios::trunc);
|
||||||
|
cap_seen.clear();
|
||||||
|
cap_gen = gen;
|
||||||
|
XELOGI("[SHIP-CAP] writing {}", name);
|
||||||
|
}
|
||||||
|
g_ship_capture_remaining.fetch_sub(1, std::memory_order_relaxed);
|
||||||
|
if (!cap_out.is_open()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reg::VGT_DRAW_INITIATOR init;
|
||||||
|
init.value = vgt_draw_initiator_value;
|
||||||
|
Shader* vs = active_vertex_shader_;
|
||||||
|
if (!vs || !vs->is_ucode_analyzed() || vs->vertex_bindings().empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& binding = vs->vertex_bindings()[0];
|
||||||
|
xenos::xe_gpu_vertex_fetch_t fetch =
|
||||||
|
register_file_->GetVertexFetch(binding.fetch_constant);
|
||||||
|
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 || vbase == 0) {
|
||||||
|
return; // no float-position stream (UI/effects) — skip
|
||||||
|
}
|
||||||
|
// One record per distinct (buffer, placement): hash the c0..c2 WVP rows so
|
||||||
|
// repeated draws of the SAME buffer at DIFFERENT transforms (multi-instance
|
||||||
|
// parts, fleet ships) each get their own record.
|
||||||
|
auto vsc0 = register_file_->Get<reg::SQ_VS_CONST>();
|
||||||
|
uint64_t th = 1469598103934665603ull; // FNV-1a over the 12 c0..c2 floats
|
||||||
|
for (uint32_t i = 0; i < 12; ++i) {
|
||||||
|
uint32_t idx = vsc0.base + (i / 4);
|
||||||
|
if (idx >= 256) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint32_t r = XE_GPU_REG_SHADER_CONSTANT_000_X + 4 * idx + (i % 4);
|
||||||
|
uint32_t bits = register_file_->values[r];
|
||||||
|
th = (th ^ bits) * 1099511628211ull;
|
||||||
|
}
|
||||||
|
// Mix the index range into the key as well (added 2026-08-13). The engine
|
||||||
|
// issues SEVERAL draws over one vertex buffer, each with its own index
|
||||||
|
// sub-range (the first Stage_02 capture showed a 119-vertex hull LOD drawn
|
||||||
|
// with 21 indices) — keying on (vbase, transform) alone kept only the first
|
||||||
|
// batch, which reads like a mysteriously short draw. With the range in the key
|
||||||
|
// every batch is recorded, so the block's full index extent is observable.
|
||||||
|
uint64_t ib_key = 0;
|
||||||
|
if (index_buffer_info) {
|
||||||
|
ib_key = (uint64_t(index_buffer_info->guest_base) << 20) ^
|
||||||
|
uint64_t(index_buffer_info->count);
|
||||||
|
}
|
||||||
|
uint64_t key = (uint64_t(vbase) << 32) ^ (th & 0xFFFFFFFFull) ^ (th >> 32) ^
|
||||||
|
(ib_key * 1099511628211ull);
|
||||||
|
if (!cap_seen.insert(key).second) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cap_seen.size() > 8192) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint32_t vcount = buf_bytes / stride;
|
||||||
|
cap_out << fmt::format(
|
||||||
|
"DRAW vbase=0x{:08X} stride={} vcount={} indices={} prim={} vs=0x{:016X}\n",
|
||||||
|
vbase, stride, vcount, uint32_t(init.num_indices),
|
||||||
|
uint32_t(init.prim_type), vs->ucode_data_hash());
|
||||||
|
// The guest's INDEX buffer for this draw. Our offline mesh decoder only
|
||||||
|
// *assumes* the index buffer sits immediately before the vertex buffer; this
|
||||||
|
// line is the ground truth for that assumption (ib base vs vbase), and the
|
||||||
|
// decoded min/max index says how much of the vertex pool the draw really
|
||||||
|
// covers — which is what the `indices=` field alone cannot answer.
|
||||||
|
if (index_buffer_info) {
|
||||||
|
const auto& ib = *index_buffer_info;
|
||||||
|
bool i32 = ib.format == xenos::IndexFormat::kInt32;
|
||||||
|
uint32_t icount = ib.count;
|
||||||
|
cap_out << fmt::format(
|
||||||
|
" ib base=0x{:08X} count={} fmt={} endian={} len={} delta_vb={}",
|
||||||
|
ib.guest_base, icount, i32 ? "u32" : "u16", uint32_t(ib.endianness),
|
||||||
|
ib.length, int64_t(vbase) - int64_t(ib.guest_base));
|
||||||
|
const uint8_t* ip = memory_->TranslatePhysical<const uint8_t*>(ib.guest_base);
|
||||||
|
if (ip && icount) {
|
||||||
|
uint32_t scan = icount < 65536 ? icount : 65536;
|
||||||
|
uint32_t imin = 0xFFFFFFFFu, imax = 0;
|
||||||
|
auto rd = [&](uint32_t k) -> uint32_t {
|
||||||
|
// Guest index data is big-endian in memory (the endianness field says
|
||||||
|
// how the GPU swaps it); read it that way and record the field so the
|
||||||
|
// offline side can compensate if a draw ever differs.
|
||||||
|
const uint8_t* q = ip + (i32 ? k * 4 : k * 2);
|
||||||
|
return i32 ? (uint32_t(q[0]) << 24) | (uint32_t(q[1]) << 16) |
|
||||||
|
(uint32_t(q[2]) << 8) | uint32_t(q[3])
|
||||||
|
: (uint32_t(q[0]) << 8) | uint32_t(q[1]);
|
||||||
|
};
|
||||||
|
for (uint32_t k = 0; k < scan; ++k) {
|
||||||
|
uint32_t v = rd(k);
|
||||||
|
if (v < imin) imin = v;
|
||||||
|
if (v > imax) imax = v;
|
||||||
|
}
|
||||||
|
cap_out << fmt::format(" min={} max={} idx:", imin, imax);
|
||||||
|
uint32_t nd = icount < 24 ? icount : 24;
|
||||||
|
for (uint32_t k = 0; k < nd; ++k) {
|
||||||
|
cap_out << fmt::format(" {}", rd(k));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cap_out << "\n";
|
||||||
|
} else {
|
||||||
|
cap_out << " ib auto\n";
|
||||||
|
}
|
||||||
|
uint32_t n = vcount < 64 ? vcount : 64;
|
||||||
|
cap_out << " pos:";
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
cap_out << fmt::format(" ({:.4f},{:.4f},{:.4f})", be_f32(p), be_f32(p + 4),
|
||||||
|
be_f32(p + 8));
|
||||||
|
}
|
||||||
|
cap_out << "\n";
|
||||||
|
|
||||||
|
// Vertex-shader float constants: the buffer holds LOCAL positions, so the
|
||||||
|
// per-part world (or world-view-projection) matrix that places the part lives
|
||||||
|
// here as a run of float4 constants. Diffing two parts' constants isolates the
|
||||||
|
// matrix (the camera VP block is shared). Dump the first 48 vec4 from the VS
|
||||||
|
// constant base as host-float (the register file stores them host-endian).
|
||||||
|
auto vsc = register_file_->Get<reg::SQ_VS_CONST>();
|
||||||
|
uint32_t cbase = vsc.base; // starting float4 index
|
||||||
|
cap_out << fmt::format(" vsconst base={}:", cbase);
|
||||||
|
for (uint32_t i = 0; i < 48; ++i) {
|
||||||
|
uint32_t idx = cbase + i;
|
||||||
|
if (idx >= 256) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint32_t r = XE_GPU_REG_SHADER_CONSTANT_000_X + 4 * idx;
|
||||||
|
float cx = register_file_->Get<float>(r);
|
||||||
|
float cy = register_file_->Get<float>(r + 1);
|
||||||
|
float cz = register_file_->Get<float>(r + 2);
|
||||||
|
float cw = register_file_->Get<float>(r + 3);
|
||||||
|
cap_out << fmt::format(" c{}=({:.4f},{:.4f},{:.4f},{:.4f})", i, cx, cy, cz,
|
||||||
|
cw);
|
||||||
|
}
|
||||||
|
cap_out << "\n";
|
||||||
|
cap_out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
void CommandProcessor::LogDrawForRE(uint32_t vgt_draw_initiator_value,
|
void CommandProcessor::LogDrawForRE(uint32_t vgt_draw_initiator_value,
|
||||||
const IndexBufferInfo* index_buffer_info) {
|
const IndexBufferInfo* index_buffer_info) {
|
||||||
|
// One-shot ship-placement capture runs independently of the log_draws cvar.
|
||||||
|
CaptureShipDrawForRE(vgt_draw_initiator_value, index_buffer_info);
|
||||||
if (!cvars::log_draws) {
|
if (!cvars::log_draws) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ class ByteStream;
|
|||||||
|
|
||||||
namespace gpu {
|
namespace gpu {
|
||||||
|
|
||||||
|
// Arm a one-shot ship-placement capture (see CommandProcessor::CaptureShipDrawForRE).
|
||||||
|
// Called from the UI thread (F10 hotkey); thread-safe. Defined in command_processor.cc.
|
||||||
|
void RequestShipCaptureFrame();
|
||||||
|
|
||||||
enum class GPUSetting { ClearMemoryPageState, ReadbackMemexport };
|
enum class GPUSetting { ClearMemoryPageState, ReadbackMemexport };
|
||||||
|
|
||||||
enum class ReadbackResolveMode {
|
enum class ReadbackResolveMode {
|
||||||
@@ -453,6 +457,14 @@ class CommandProcessor {
|
|||||||
void LogDrawForRE(uint32_t vgt_draw_initiator_value,
|
void LogDrawForRE(uint32_t vgt_draw_initiator_value,
|
||||||
const IndexBufferInfo* index_buffer_info);
|
const IndexBufferInfo* index_buffer_info);
|
||||||
|
|
||||||
|
// Ship-placement capture (RE): a one-shot snapshot armed by RequestShipCaptureFrame()
|
||||||
|
// (F10 hotkey). Dumps each draw's guest vertex-buffer address, vertex/index
|
||||||
|
// counts, and up to 64 WORLD-space positions to xenia_ship_capture.log, so a
|
||||||
|
// correlator can recover each capital-ship part's exact placement. Defined in
|
||||||
|
// command_processor.cc.
|
||||||
|
void CaptureShipDrawForRE(uint32_t vgt_draw_initiator_value,
|
||||||
|
const IndexBufferInfo* index_buffer_info);
|
||||||
|
|
||||||
// "Actual" is for the command processor thread, to be read by the
|
// "Actual" is for the command processor thread, to be read by the
|
||||||
// implementations.
|
// implementations.
|
||||||
SwapPostEffect GetActualSwapPostEffect() const {
|
SwapPostEffect GetActualSwapPostEffect() const {
|
||||||
|
|||||||
341
src/xenia/hid/file/file_input_driver.h
Normal file
341
src/xenia/hid/file/file_input_driver.h
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
/**
|
||||||
|
******************************************************************************
|
||||||
|
* Xenia : Xbox 360 Emulator Research Project *
|
||||||
|
******************************************************************************
|
||||||
|
* RE aid: a controller whose state comes from a FILE, not from a device.
|
||||||
|
******************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef XENIA_HID_FILE_FILE_INPUT_DRIVER_H_
|
||||||
|
#define XENIA_HID_FILE_FILE_INPUT_DRIVER_H_
|
||||||
|
|
||||||
|
// Why this exists
|
||||||
|
// ---------------
|
||||||
|
// The scripted-input tool used for reverse engineering created its pad through
|
||||||
|
// `/dev/uinput`. Input devices are NOT namespaced by the kernel, so a uinput
|
||||||
|
// device created inside a container is registered with the HOST's input stack:
|
||||||
|
// every trigger hold and button press is delivered to whatever on the host reads
|
||||||
|
// gamepads, not only to the emulator. That is a real leak, and it was noticed the
|
||||||
|
// hard way.
|
||||||
|
//
|
||||||
|
// This driver takes the kernel out of the loop entirely. The pad state lives in
|
||||||
|
// an ordinary text file that only this container can see, and `GetState` reads it
|
||||||
|
// (re-parsing only when the file changes). Nothing is registered with the host,
|
||||||
|
// no X server is involved either, and — a bonus for RE — the analogue values are
|
||||||
|
// exact rather than whatever a virtual stick quantises to.
|
||||||
|
//
|
||||||
|
// File format: one or more whitespace/newline separated `key=value` pairs.
|
||||||
|
//
|
||||||
|
// press=A,START buttons by name (see kButtonNames), comma separated
|
||||||
|
// buttons=0x1010 or the raw XINPUT mask, if you prefer
|
||||||
|
// lt=0 rt=255 triggers, 0..255
|
||||||
|
// lx=0 ly=0 left thumb, -32768..32767
|
||||||
|
// rx=0 ry=0 right thumb, -32768..32767
|
||||||
|
//
|
||||||
|
// Anything absent is neutral, so `press=A` alone is a valid file. An empty or
|
||||||
|
// missing file means "no input" — which is also the safe default if the file is
|
||||||
|
// deleted mid-run.
|
||||||
|
//
|
||||||
|
// Path: `--pad_file=<path>`, default `/tmp/xenia_pad.txt`. Only user 0 is
|
||||||
|
// connected; other slots report no device, as a single-pad console would.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#include "xenia/base/cvar.h"
|
||||||
|
#include "xenia/base/logging.h"
|
||||||
|
#include "xenia/hid/input_driver.h"
|
||||||
|
#include "xenia/ui/virtual_key.h"
|
||||||
|
|
||||||
|
DECLARE_string(pad_file);
|
||||||
|
|
||||||
|
namespace xe {
|
||||||
|
namespace hid {
|
||||||
|
namespace filepad {
|
||||||
|
|
||||||
|
struct NamedButton {
|
||||||
|
const char* name;
|
||||||
|
uint16_t mask;
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr NamedButton kButtonNames[] = {
|
||||||
|
{"UP", X_INPUT_GAMEPAD_DPAD_UP},
|
||||||
|
{"DOWN", X_INPUT_GAMEPAD_DPAD_DOWN},
|
||||||
|
{"LEFT", X_INPUT_GAMEPAD_DPAD_LEFT},
|
||||||
|
{"RIGHT", X_INPUT_GAMEPAD_DPAD_RIGHT},
|
||||||
|
{"START", X_INPUT_GAMEPAD_START},
|
||||||
|
{"BACK", X_INPUT_GAMEPAD_BACK},
|
||||||
|
{"LS", X_INPUT_GAMEPAD_LEFT_THUMB},
|
||||||
|
{"RS", X_INPUT_GAMEPAD_RIGHT_THUMB},
|
||||||
|
{"LB", X_INPUT_GAMEPAD_LEFT_SHOULDER},
|
||||||
|
{"RB", X_INPUT_GAMEPAD_RIGHT_SHOULDER},
|
||||||
|
{"A", X_INPUT_GAMEPAD_A},
|
||||||
|
{"B", X_INPUT_GAMEPAD_B},
|
||||||
|
{"X", X_INPUT_GAMEPAD_X},
|
||||||
|
{"Y", X_INPUT_GAMEPAD_Y},
|
||||||
|
};
|
||||||
|
|
||||||
|
class FileInputDriver final : public InputDriver {
|
||||||
|
public:
|
||||||
|
FileInputDriver(xe::ui::Window* window, size_t window_z_order)
|
||||||
|
: InputDriver(window, window_z_order) {}
|
||||||
|
~FileInputDriver() override = default;
|
||||||
|
|
||||||
|
X_STATUS Setup() override {
|
||||||
|
XELOGI("[file-pad] reading controller state from {}", cvars::pad_file);
|
||||||
|
return X_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
X_RESULT GetCapabilities(uint32_t user_index, uint32_t flags,
|
||||||
|
X_INPUT_CAPABILITIES* out_caps) override {
|
||||||
|
if (user_index != 0) {
|
||||||
|
return X_ERROR_DEVICE_NOT_CONNECTED;
|
||||||
|
}
|
||||||
|
std::memset(out_caps, 0, sizeof(*out_caps));
|
||||||
|
out_caps->type = 0x01; // XINPUT_DEVTYPE_GAMEPAD
|
||||||
|
out_caps->sub_type = 0x01; // XINPUT_DEVSUBTYPE_GAMEPAD
|
||||||
|
out_caps->flags = 0;
|
||||||
|
out_caps->gamepad.buttons = 0xFFFF;
|
||||||
|
out_caps->gamepad.left_trigger = 0xFF;
|
||||||
|
out_caps->gamepad.right_trigger = 0xFF;
|
||||||
|
out_caps->gamepad.thumb_lx = static_cast<int16_t>(0xFFFFu);
|
||||||
|
out_caps->gamepad.thumb_ly = static_cast<int16_t>(0xFFFFu);
|
||||||
|
out_caps->gamepad.thumb_rx = static_cast<int16_t>(0xFFFFu);
|
||||||
|
out_caps->gamepad.thumb_ry = static_cast<int16_t>(0xFFFFu);
|
||||||
|
return X_ERROR_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
X_RESULT GetState(uint32_t user_index, X_INPUT_STATE* out_state) override {
|
||||||
|
if (user_index != 0) {
|
||||||
|
return X_ERROR_DEVICE_NOT_CONNECTED;
|
||||||
|
}
|
||||||
|
Refresh();
|
||||||
|
std::memset(out_state, 0, sizeof(*out_state));
|
||||||
|
out_state->packet_number = packet_;
|
||||||
|
out_state->gamepad.buttons = buttons_;
|
||||||
|
out_state->gamepad.left_trigger = lt_;
|
||||||
|
out_state->gamepad.right_trigger = rt_;
|
||||||
|
out_state->gamepad.thumb_lx = lx_;
|
||||||
|
out_state->gamepad.thumb_ly = ly_;
|
||||||
|
out_state->gamepad.thumb_rx = rx_;
|
||||||
|
out_state->gamepad.thumb_ry = ry_;
|
||||||
|
return X_ERROR_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
X_RESULT SetState(uint32_t user_index, X_INPUT_VIBRATION* vibration) override {
|
||||||
|
return user_index == 0 ? X_ERROR_SUCCESS : X_ERROR_DEVICE_NOT_CONNECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menus do NOT read the pad through GetState. "PRESS (A) BUTTON" and most
|
||||||
|
// 360 front-ends poll XamInputGetKeystrokeEx, so a driver that only answers
|
||||||
|
// GetState looks completely dead on a title screen while its own log happily
|
||||||
|
// shows the button arriving. Returning X_ERROR_EMPTY here is what made the
|
||||||
|
// first scripted run press A into the void.
|
||||||
|
//
|
||||||
|
// One event per call, edge triggered: KEYUPs for everything released, then
|
||||||
|
// KEYDOWNs for everything pressed, exactly as the SDL driver orders them.
|
||||||
|
// Deliberately NO auto-repeat — scripted input wants precisely one event per
|
||||||
|
// press, and repeat is what makes menu steps overshoot.
|
||||||
|
X_RESULT GetKeystroke(uint32_t user_index, uint32_t flags,
|
||||||
|
X_INPUT_KEYSTROKE* out_keystroke) override {
|
||||||
|
const bool user_any = user_index == 0xFF || user_index == 0xFFFFFFFFu;
|
||||||
|
if (!user_any && user_index != 0) {
|
||||||
|
return X_ERROR_DEVICE_NOT_CONNECTED;
|
||||||
|
}
|
||||||
|
if (!out_keystroke) {
|
||||||
|
return X_ERROR_BAD_ARGUMENTS;
|
||||||
|
}
|
||||||
|
Refresh();
|
||||||
|
|
||||||
|
// Bit index in X_INPUT_GAMEPAD::buttons -> virtual key. Order matters: it is
|
||||||
|
// the order multiple simultaneous changes are reported in.
|
||||||
|
static constexpr uint16_t kVk[16] = {
|
||||||
|
uint16_t(ui::VirtualKey::kXInputPadDpadUp),
|
||||||
|
uint16_t(ui::VirtualKey::kXInputPadDpadDown),
|
||||||
|
uint16_t(ui::VirtualKey::kXInputPadDpadLeft),
|
||||||
|
uint16_t(ui::VirtualKey::kXInputPadDpadRight),
|
||||||
|
uint16_t(ui::VirtualKey::kXInputPadStart),
|
||||||
|
uint16_t(ui::VirtualKey::kXInputPadBack),
|
||||||
|
uint16_t(ui::VirtualKey::kXInputPadLThumbPress),
|
||||||
|
uint16_t(ui::VirtualKey::kXInputPadRThumbPress),
|
||||||
|
uint16_t(ui::VirtualKey::kXInputPadLShoulder),
|
||||||
|
uint16_t(ui::VirtualKey::kXInputPadRShoulder),
|
||||||
|
0, /* guide */
|
||||||
|
0, /* unused */
|
||||||
|
uint16_t(ui::VirtualKey::kXInputPadA),
|
||||||
|
uint16_t(ui::VirtualKey::kXInputPadB),
|
||||||
|
uint16_t(ui::VirtualKey::kXInputPadX),
|
||||||
|
uint16_t(ui::VirtualKey::kXInputPadY),
|
||||||
|
};
|
||||||
|
|
||||||
|
const uint16_t changed = static_cast<uint16_t>(buttons_ ^ reported_);
|
||||||
|
if (!changed) {
|
||||||
|
return X_ERROR_EMPTY;
|
||||||
|
}
|
||||||
|
for (int pass = 0; pass < 2; ++pass) {
|
||||||
|
const bool clear_pass = pass == 0;
|
||||||
|
for (uint8_t i = 0; i < 16; ++i) {
|
||||||
|
const uint16_t bit = static_cast<uint16_t>(1u << i);
|
||||||
|
if (!(changed & bit) || kVk[i] == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const bool pressed = (buttons_ & bit) != 0;
|
||||||
|
if (clear_pass == pressed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
reported_ = static_cast<uint16_t>(pressed ? (reported_ | bit)
|
||||||
|
: (reported_ & ~bit));
|
||||||
|
out_keystroke->virtual_key = kVk[i];
|
||||||
|
out_keystroke->unicode = 0;
|
||||||
|
out_keystroke->flags =
|
||||||
|
pressed ? X_INPUT_KEYSTROKE_KEYDOWN : X_INPUT_KEYSTROKE_KEYUP;
|
||||||
|
out_keystroke->user_index = 0;
|
||||||
|
out_keystroke->hid_code = 0;
|
||||||
|
XELOGI("[file-pad] keystroke vk={:04X} {}", kVk[i],
|
||||||
|
pressed ? "down" : "up");
|
||||||
|
return X_ERROR_SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only bits without a virtual key changed (guide/unused): swallow them so
|
||||||
|
// the caller is not asked again forever.
|
||||||
|
reported_ = buttons_;
|
||||||
|
return X_ERROR_EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputType GetInputType() const override { return InputType::Controller; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Re-parse only when the file actually changed: `GetState` is polled every
|
||||||
|
// frame and a stat is far cheaper than a read+parse.
|
||||||
|
//
|
||||||
|
// The change test uses **nanosecond** mtime, not `st_mtime`. Whole-second
|
||||||
|
// granularity plus size looked sufficient and is not: a script that steps a
|
||||||
|
// menu writes several same-length states per second (`press=A` then `press=B`,
|
||||||
|
// both 8 bytes), and every one of those after the first would be silently
|
||||||
|
// dropped. That failure is invisible — the emulator just does not react — so
|
||||||
|
// it is worth the extra field.
|
||||||
|
void Refresh() {
|
||||||
|
struct stat st;
|
||||||
|
if (::stat(cvars::pad_file.c_str(), &st) != 0) {
|
||||||
|
if (present_) {
|
||||||
|
present_ = false;
|
||||||
|
Neutral();
|
||||||
|
++packet_;
|
||||||
|
XELOGI("[file-pad] {} gone -> neutral", cvars::pad_file);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (present_ && st.st_mtim.tv_sec == mtime_sec_ &&
|
||||||
|
st.st_mtim.tv_nsec == mtime_nsec_ && st.st_size == size_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
present_ = true;
|
||||||
|
mtime_sec_ = st.st_mtim.tv_sec;
|
||||||
|
mtime_nsec_ = st.st_mtim.tv_nsec;
|
||||||
|
size_ = st.st_size;
|
||||||
|
std::FILE* f = std::fopen(cvars::pad_file.c_str(), "rb");
|
||||||
|
if (!f) {
|
||||||
|
Neutral();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char buf[512] = {0};
|
||||||
|
size_t n = std::fread(buf, 1, sizeof(buf) - 1, f);
|
||||||
|
std::fclose(f);
|
||||||
|
buf[n] = '\0';
|
||||||
|
Parse(buf);
|
||||||
|
++packet_;
|
||||||
|
// One line per change (not per frame): with no display to watch, this log is
|
||||||
|
// the only proof that a scripted press was actually picked up.
|
||||||
|
XELOGI("[file-pad] #{} buttons={:04X} lt={} rt={} lx={} ly={} rx={} ry={}",
|
||||||
|
packet_, buttons_, lt_, rt_, lx_, ly_, rx_, ry_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Neutral() {
|
||||||
|
buttons_ = 0;
|
||||||
|
lt_ = rt_ = 0;
|
||||||
|
lx_ = ly_ = rx_ = ry_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Parse(const char* text) {
|
||||||
|
Neutral();
|
||||||
|
std::string s(text);
|
||||||
|
size_t pos = 0;
|
||||||
|
while (pos < s.size()) {
|
||||||
|
size_t end = s.find_first_of(" \t\r\n", pos);
|
||||||
|
if (end == std::string::npos) {
|
||||||
|
end = s.size();
|
||||||
|
}
|
||||||
|
std::string tok = s.substr(pos, end - pos);
|
||||||
|
pos = end + 1;
|
||||||
|
size_t eq = tok.find('=');
|
||||||
|
if (eq == std::string::npos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::string key = tok.substr(0, eq), val = tok.substr(eq + 1);
|
||||||
|
if (key == "press") {
|
||||||
|
size_t p = 0;
|
||||||
|
while (p < val.size()) {
|
||||||
|
size_t c = val.find(',', p);
|
||||||
|
if (c == std::string::npos) {
|
||||||
|
c = val.size();
|
||||||
|
}
|
||||||
|
std::string name = val.substr(p, c - p);
|
||||||
|
p = c + 1;
|
||||||
|
for (const auto& b : kButtonNames) {
|
||||||
|
if (name == b.name) {
|
||||||
|
buttons_ |= b.mask;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (key == "buttons") {
|
||||||
|
buttons_ |= static_cast<uint16_t>(std::strtoul(val.c_str(), nullptr, 0));
|
||||||
|
} else if (key == "lt") {
|
||||||
|
lt_ = Clamp8(std::strtol(val.c_str(), nullptr, 0));
|
||||||
|
} else if (key == "rt") {
|
||||||
|
rt_ = Clamp8(std::strtol(val.c_str(), nullptr, 0));
|
||||||
|
} else if (key == "lx") {
|
||||||
|
lx_ = Clamp16(std::strtol(val.c_str(), nullptr, 0));
|
||||||
|
} else if (key == "ly") {
|
||||||
|
ly_ = Clamp16(std::strtol(val.c_str(), nullptr, 0));
|
||||||
|
} else if (key == "rx") {
|
||||||
|
rx_ = Clamp16(std::strtol(val.c_str(), nullptr, 0));
|
||||||
|
} else if (key == "ry") {
|
||||||
|
ry_ = Clamp16(std::strtol(val.c_str(), nullptr, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t Clamp8(long v) {
|
||||||
|
return static_cast<uint8_t>(v < 0 ? 0 : (v > 255 ? 255 : v));
|
||||||
|
}
|
||||||
|
static int16_t Clamp16(long v) {
|
||||||
|
return static_cast<int16_t>(v < -32768 ? -32768 : (v > 32767 ? 32767 : v));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t buttons_ = 0;
|
||||||
|
uint8_t lt_ = 0, rt_ = 0;
|
||||||
|
int16_t lx_ = 0, ly_ = 0, rx_ = 0, ry_ = 0;
|
||||||
|
uint32_t packet_ = 1;
|
||||||
|
// Buttons already reported through GetKeystroke; the edge detector's memory.
|
||||||
|
uint16_t reported_ = 0;
|
||||||
|
bool present_ = false;
|
||||||
|
time_t mtime_sec_ = 0;
|
||||||
|
long mtime_nsec_ = -1;
|
||||||
|
off_t size_ = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline std::unique_ptr<InputDriver> Create(xe::ui::Window* window,
|
||||||
|
size_t window_z_order) {
|
||||||
|
return std::make_unique<FileInputDriver>(window, window_z_order);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace filepad
|
||||||
|
} // namespace hid
|
||||||
|
} // namespace xe
|
||||||
|
|
||||||
|
#endif // XENIA_HID_FILE_FILE_INPUT_DRIVER_H_
|
||||||
197
src/xenia/kernel/util/frame_state_probe.h
Normal file
197
src/xenia/kernel/util/frame_state_probe.h
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
/**
|
||||||
|
******************************************************************************
|
||||||
|
* Xenia : Xbox 360 Emulator Research Project *
|
||||||
|
******************************************************************************
|
||||||
|
* Copyright 2026 Ben Vanik. All rights reserved. *
|
||||||
|
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||||
|
******************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef XENIA_KERNEL_UTIL_FRAME_STATE_PROBE_H_
|
||||||
|
#define XENIA_KERNEL_UTIL_FRAME_STATE_PROBE_H_
|
||||||
|
|
||||||
|
// A per-guest-frame sampler of guest memory, for reverse engineering.
|
||||||
|
//
|
||||||
|
// Reading guest RAM from outside the emulator (Canary backs it with a
|
||||||
|
// /dev/shm file, so a host process can pread it) is easy but UNSYNCHRONISED:
|
||||||
|
// the reader has no idea where the guest is in its update, so successive reads
|
||||||
|
// are separated by an unknown, jittering number of guest updates. Measuring a
|
||||||
|
// rate that way aliases badly -- a project measuring the craft's angular
|
||||||
|
// velocity got 3x swings between adjacent 0.25 s windows purely from sampling.
|
||||||
|
//
|
||||||
|
// This samples from INSIDE, once per VdSwap, i.e. exactly once per guest frame
|
||||||
|
// at a fixed point in it. Consecutive lines are then one frame apart by
|
||||||
|
// construction, and the frame counter is exact.
|
||||||
|
//
|
||||||
|
// Which bytes to sample is not known at launch (object addresses are found by
|
||||||
|
// scanning at runtime), so the regions are read from a small control file that
|
||||||
|
// is re-read whenever its mtime changes -- the same trick the file input pad
|
||||||
|
// uses. Format, one region per line, '#' comments ignored:
|
||||||
|
//
|
||||||
|
// 0x40D10590 128 a guest VA and a byte count
|
||||||
|
//
|
||||||
|
// Output, one line per frame, appended to --frame_probe_log:
|
||||||
|
//
|
||||||
|
// F <frame> H <host_ns> G <guest_ticks> R0 <hex> R1 <hex> ...
|
||||||
|
//
|
||||||
|
// Disabled unless --frame_probe_log is set, and then costs one stat() plus the
|
||||||
|
// listed reads per frame.
|
||||||
|
|
||||||
|
#include <cinttypes>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#include "xenia/base/cvar.h"
|
||||||
|
#include "xenia/base/clock.h"
|
||||||
|
#include "xenia/base/logging.h"
|
||||||
|
#include "xenia/memory.h"
|
||||||
|
|
||||||
|
DEFINE_string(frame_probe_log, "",
|
||||||
|
"RE: append one line of guest state per frame to this file. "
|
||||||
|
"Empty disables the probe entirely.",
|
||||||
|
"RE");
|
||||||
|
DEFINE_string(frame_probe, "/tmp/xenia_frame_probe.txt",
|
||||||
|
"RE: control file listing the guest regions --frame_probe_log "
|
||||||
|
"samples, one '<hex_va> <len>' per line. Re-read when it changes.",
|
||||||
|
"RE");
|
||||||
|
|
||||||
|
namespace xe {
|
||||||
|
namespace kernel {
|
||||||
|
namespace util {
|
||||||
|
|
||||||
|
class FrameStateProbe {
|
||||||
|
public:
|
||||||
|
static FrameStateProbe& instance() {
|
||||||
|
static FrameStateProbe probe;
|
||||||
|
return probe;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called once per guest frame, from VdSwap.
|
||||||
|
void Sample(Memory* memory) {
|
||||||
|
if (cvars::frame_probe_log.empty() || !memory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
++frame_;
|
||||||
|
ReloadIfChanged();
|
||||||
|
if (regions_.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!out_) {
|
||||||
|
out_ = std::fopen(cvars::frame_probe_log.c_str(), "a");
|
||||||
|
if (!out_) {
|
||||||
|
XELOGE("[frame-probe] cannot open {}", cvars::frame_probe_log);
|
||||||
|
// Do not retry every frame on a bad path.
|
||||||
|
cvars::frame_probe_log.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::fprintf(out_, "F %" PRIu64 " H %" PRIu64 " G %" PRIu64, frame_,
|
||||||
|
Clock::QueryHostSystemTime(), Clock::QueryGuestTickCount());
|
||||||
|
for (size_t i = 0; i < regions_.size(); ++i) {
|
||||||
|
const auto& r = regions_[i];
|
||||||
|
std::fprintf(out_, " R%zu ", i);
|
||||||
|
if (!ReadRegion(memory, r.address, r.length)) {
|
||||||
|
std::fprintf(out_, "-");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (uint32_t b = 0; b < r.length; ++b) {
|
||||||
|
std::fprintf(out_, "%02x", scratch_[b]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::fputc('\n', out_);
|
||||||
|
// Flushed per frame on purpose: the host analysis reads this file while the
|
||||||
|
// game is still flying, and a run can end in a crash or a pkill -9.
|
||||||
|
std::fflush(out_);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr uint32_t kMaxRegionBytes = 1024;
|
||||||
|
static constexpr size_t kMaxRegions = 8;
|
||||||
|
|
||||||
|
struct Region {
|
||||||
|
uint32_t address;
|
||||||
|
uint32_t length;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copies out first so the line cannot be torn across a guest write mid-print.
|
||||||
|
bool ReadRegion(Memory* memory, uint32_t address, uint32_t length) {
|
||||||
|
auto heap = memory->LookupHeap(address);
|
||||||
|
if (!heap) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint32_t protect = 0;
|
||||||
|
if (!heap->QueryProtect(address, &protect) || !protect) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!heap->QueryProtect(address + length - 1, &protect) || !protect) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::memcpy(scratch_, memory->TranslateVirtual(address), length);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReloadIfChanged() {
|
||||||
|
struct stat st;
|
||||||
|
if (::stat(cvars::frame_probe.c_str(), &st) != 0) {
|
||||||
|
if (!regions_.empty()) {
|
||||||
|
regions_.clear();
|
||||||
|
XELOGI("[frame-probe] control file gone -- sampling stopped");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int64_t stamp = static_cast<int64_t>(st.st_mtim.tv_sec) * 1000000000 +
|
||||||
|
st.st_mtim.tv_nsec;
|
||||||
|
if (stamp == stamp_ && st.st_size == size_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stamp_ = stamp;
|
||||||
|
size_ = st.st_size;
|
||||||
|
Parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Parse() {
|
||||||
|
regions_.clear();
|
||||||
|
FILE* f = std::fopen(cvars::frame_probe.c_str(), "r");
|
||||||
|
if (!f) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char line[256];
|
||||||
|
while (std::fgets(line, sizeof(line), f)) {
|
||||||
|
char* p = line;
|
||||||
|
while (*p == ' ' || *p == '\t') ++p;
|
||||||
|
if (*p == '#' || *p == '\n' || *p == '\0') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
uint32_t addr = 0, len = 0;
|
||||||
|
if (std::sscanf(p, "%" SCNx32 " %" SCNu32, &addr, &len) != 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!len || len > kMaxRegionBytes || regions_.size() >= kMaxRegions) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
regions_.push_back({addr, len});
|
||||||
|
}
|
||||||
|
std::fclose(f);
|
||||||
|
for (size_t i = 0; i < regions_.size(); ++i) {
|
||||||
|
XELOGI("[frame-probe] R{} = {:08X} +{}", i, regions_[i].address,
|
||||||
|
regions_[i].length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Region> regions_;
|
||||||
|
uint8_t scratch_[kMaxRegionBytes] = {};
|
||||||
|
FILE* out_ = nullptr;
|
||||||
|
uint64_t frame_ = 0;
|
||||||
|
int64_t stamp_ = -1;
|
||||||
|
off_t size_ = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace util
|
||||||
|
} // namespace kernel
|
||||||
|
} // namespace xe
|
||||||
|
|
||||||
|
#endif // XENIA_KERNEL_UTIL_FRAME_STATE_PROBE_H_
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "xenia/emulator.h"
|
#include "xenia/emulator.h"
|
||||||
#include "xenia/gpu/graphics_system.h"
|
#include "xenia/gpu/graphics_system.h"
|
||||||
#include "xenia/kernel/kernel_state.h"
|
#include "xenia/kernel/kernel_state.h"
|
||||||
|
#include "xenia/kernel/util/frame_state_probe.h"
|
||||||
#include "xenia/kernel/util/shim_utils.h"
|
#include "xenia/kernel/util/shim_utils.h"
|
||||||
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.h"
|
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.h"
|
||||||
#include "xenia/kernel/xboxkrnl/xboxkrnl_rtl.h"
|
#include "xenia/kernel/xboxkrnl/xboxkrnl_rtl.h"
|
||||||
@@ -472,6 +473,11 @@ void VdSwap_entry(
|
|||||||
lpdword_t frontbuffer_ptr, // ptr to frontbuffer address
|
lpdword_t frontbuffer_ptr, // ptr to frontbuffer address
|
||||||
lpdword_t texture_format_ptr, lpdword_t color_space_ptr, lpdword_t width,
|
lpdword_t texture_format_ptr, lpdword_t color_space_ptr, lpdword_t width,
|
||||||
lpdword_t height) {
|
lpdword_t height) {
|
||||||
|
// RE probe: one guest frame has just finished, and we are on the guest thread
|
||||||
|
// that finished it -- the only place a sample of guest state is guaranteed to
|
||||||
|
// be exactly one frame after the previous one. No-op unless --frame_probe_log.
|
||||||
|
util::FrameStateProbe::instance().Sample(kernel_memory());
|
||||||
|
|
||||||
// All of these parameters are REQUIRED.
|
// All of these parameters are REQUIRED.
|
||||||
assert(buffer_ptr);
|
assert(buffer_ptr);
|
||||||
assert(fetch_ptr);
|
assert(fetch_ptr);
|
||||||
|
|||||||
Reference in New Issue
Block a user