Compare commits
7 Commits
capture-sh
...
auto/re-fi
| Author | SHA1 | Date | |
|---|---|---|---|
| 15fe11d5d9 | |||
| e3e17e4951 | |||
| d15c8cfab6 | |||
| 31366e5cac | |||
|
|
a08526dff0 | ||
|
|
93fe5d69df | ||
|
|
067a373734 |
@@ -1067,6 +1067,11 @@ void EmulatorWindow::OnKeyDown(ui::KeyEvent& e) {
|
||||
case ui::VirtualKey::kF12: {
|
||||
TakeScreenshot();
|
||||
} 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: {
|
||||
// Allow users to escape fullscreen (but not enter it).
|
||||
|
||||
@@ -64,6 +64,12 @@
|
||||
#include "xenia/hid/winkey/winkey_hid.h"
|
||||
#include "xenia/hid/xinput/xinput_hid.h"
|
||||
#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
|
||||
#define APU_OPTIONS "[any, nop, sdl, xaudio2]"
|
||||
@@ -72,7 +78,7 @@
|
||||
#elif XE_PLATFORM_LINUX
|
||||
#define APU_OPTIONS "[any, alsa, nop, sdl]"
|
||||
#define GPU_OPTIONS "[any, vulkan, null]"
|
||||
#define HID_OPTIONS "[any, nop, sdl]"
|
||||
#define HID_OPTIONS "[any, file, nop, sdl]"
|
||||
#else
|
||||
#define APU_OPTIONS "[any, nop, sdl]"
|
||||
#define GPU_OPTIONS "[any, vulkan, null]"
|
||||
@@ -82,6 +88,11 @@
|
||||
DEFINE_string(apu, "any", "Audio system. Use: " APU_OPTIONS, "APU");
|
||||
DEFINE_string(gpu, "any", "Graphics system. Use: " GPU_OPTIONS, "GPU");
|
||||
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(
|
||||
storage_root, "",
|
||||
@@ -446,6 +457,10 @@ std::vector<std::unique_ptr<hid::InputDriver>> EmulatorApp::CreateInputDrivers(
|
||||
if (cvars::hid.compare("nop") == 0) {
|
||||
drivers.emplace_back(
|
||||
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 {
|
||||
Factory<hid::InputDriver, ui::Window*, size_t> factory;
|
||||
#if XE_PLATFORM_WIN32
|
||||
|
||||
@@ -125,8 +125,210 @@ const char* ReVertexFormatName(xenos::VertexFormat f) {
|
||||
}
|
||||
} // 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,
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ class ByteStream;
|
||||
|
||||
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 ReadbackResolveMode {
|
||||
@@ -453,6 +457,14 @@ class CommandProcessor {
|
||||
void LogDrawForRE(uint32_t vgt_draw_initiator_value,
|
||||
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
|
||||
// implementations.
|
||||
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_
|
||||
Reference in New Issue
Block a user