mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
71ba9b6b47
## Link to GitHub Issue or related Pull Request, if one exists Fixes #681 ## Description of change `Sleep` and `sleep_for()` can be very inaccurate and varies depending on what the OS gives us... ### `timeBeginPeriod(1)` On boot, we are now calling `timeBeginPeriod(1)`, which affects the whole process but makes `Sleep` more accurate. There is some risk here if any game was relying on doing things like `Sleep(1)` and expecting it to run for 15.6ms. Most games already call `timeBeginPeriod(1)` in the game engine, though not the whole time, so I'm hoping that this is not too impactful. ### Opt out of Win11 power throttling Ensure that timer resolution change above is respected even when the window is occluded / minimized by opting out of throttling via `PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION`. ### Use Win10 high resolution timer instead of Sleep On Win10 1803 and above, there is a new OS-level API for high resolution timers; if this is available, use it (`CREATE_WAITABLE_TIMER_HIGH_RESOLUTION`). Worth noting that WINE doesn't support this currently. If not, fall back to `Sleep`, which is significantly better than `sleep_for()` in my experiments. Callers of Sleep / sleep_for were replaced with this new timer. Most of them anyway; calls to Sleep() with more than 100ms+ was left alone. ### Add an option as a chicken bit To opt out I'm adding a new option called `Use Legacy Timers` which will revert to behavior before this PR. The code paths that switched from `sleep_for` to `Sleep` will remain in place though, not affected by the option. ## Expected changes In some I/O emulation modules, poll threads may run more frequently, resulting in lower latency. It also means that spice overall may use more CPU resources and power. If you don't like this, you can always enable the option to opt out; e.g., if you're on old arcade cab PC. ## Testing DDR p4io - ok drs touch hook - ok IIDX camera hook - ok CCJ trackball - ok
519 lines
18 KiB
C++
519 lines
18 KiB
C++
#include "mdxf.h"
|
|
#include "mdxf_poll.h"
|
|
|
|
#include <mutex>
|
|
|
|
#include "avs/game.h"
|
|
#include "games/ddr/ddr.h"
|
|
#include "games/ddr/io.h"
|
|
#include "launcher/launcher.h"
|
|
#include "rawinput/rawinput.h"
|
|
#include "util/logging.h"
|
|
#include "util/precise_timer.h"
|
|
#include "util/utils.h"
|
|
|
|
#define MDFX_DEBUG_VERBOSE 0
|
|
|
|
#if MDFX_DEBUG_VERBOSE
|
|
#define log_debug(module, format_str, ...) logger::push( \
|
|
LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
|
|
#else
|
|
#define log_debug(module, format_str, ...)
|
|
#endif
|
|
|
|
// constants
|
|
const size_t STATUS_BUFFER_SIZE = 32;
|
|
const size_t STATUS_BUFFER_NUM_ENTRIES = 16;
|
|
|
|
// static stuff
|
|
static uint8_t HEAD_P1 = 0;
|
|
static uint8_t HEAD_P2 = 0;
|
|
|
|
static uint16_t PREV_STATE_P1 = 0;
|
|
static uint16_t PREV_STATE_P2 = 0;
|
|
static uint64_t PREV_TIME_P1 = 0;
|
|
static uint64_t PREV_TIME_P2 = 0;
|
|
|
|
static std::mutex MUTEX_P1;
|
|
static std::mutex MUTEX_P2;
|
|
|
|
static bool IS_MDXF_ACTIVE = false;
|
|
|
|
static const uint8_t BACKFILL_INTERVAL_MS = 4;
|
|
static const uint8_t BACKFILL_PADDING_MS = 2;
|
|
|
|
// These are used to determine if a thread needs to be spun up to keep pad state ring buffers populated with enough recent polls
|
|
static uint64_t START_TIME = 0;
|
|
static int CALL_COUNT = 0;
|
|
static const int THRESHOLD_REFRESH_RATE = 120;
|
|
|
|
static std::atomic<bool> IS_REFRESH_RATE_MEASUREMENT_STARTED{false};
|
|
static std::atomic<bool> IS_REFRESH_RATE_DETERMINED{false};
|
|
static std::atomic<bool> IS_THREAD_NEEDED{false};
|
|
static std::atomic<bool> MDXF_THREAD_RUNNING{false};
|
|
|
|
static std::thread MDXF_THREAD;
|
|
|
|
static constexpr int THREAD_REFRESH_RATE_HZ = 125;
|
|
static constexpr auto THREAD_PERIOD = std::chrono::milliseconds(1000 / THREAD_REFRESH_RATE_HZ);
|
|
|
|
// buffers
|
|
#pragma pack(push, 1)
|
|
static struct {
|
|
uint8_t STATUS_BUFFER_P1[STATUS_BUFFER_NUM_ENTRIES][STATUS_BUFFER_SIZE] {};
|
|
uint8_t STATUS_BUFFER_P2[STATUS_BUFFER_NUM_ENTRIES][STATUS_BUFFER_SIZE] {};
|
|
} BUFFERS {};
|
|
#pragma pack(pop)
|
|
|
|
static bool STATUS_BUFFER_FREEZE = false;
|
|
|
|
// Decides which method to use for populating ring buffer entries for "padding".
|
|
// Overwritten in spicecfg using P4IO Buffer Algorithm option.
|
|
// THREAD_MODE: Spins a thread running at THREAD_REFRESH_RATE_HZ which periodically fills the ring
|
|
// buffer with auxiliary entries. Falls back on BACKFILL_MODE
|
|
// BACKFILL_MODE: On every update cycle, fill the ring buffer with entries for the last known state
|
|
// BACKFILL_INTERVAL_MS apart from each other from the time of the last entry to the
|
|
// current time before adding the entry for the current state.
|
|
// AUTO_MODE: thread mode if <120Hz, backfill if >=120Hz
|
|
acio::MDXFBufferFillMode acio::MDXF_BUFFER_FILL_MODE = acio::MDXFBufferFillMode::AUTO_MODE;
|
|
|
|
typedef enum {
|
|
ARKMDXP4_POLL = 0,
|
|
INTERNAL_POLL = 1,
|
|
EXTERNAL_POLL = 2
|
|
} MDXFPollSource;
|
|
|
|
typedef uint64_t (__cdecl *ARK_GET_TICK_TIME64_T)();
|
|
|
|
static uint64_t arkGetTickTime64() {
|
|
static ARK_GET_TICK_TIME64_T getTickTime64 = nullptr;
|
|
|
|
if (!getTickTime64) {
|
|
HMODULE h = avs::game::DLL_INSTANCE;
|
|
if (h) {
|
|
getTickTime64 = (ARK_GET_TICK_TIME64_T)GetProcAddress(h, "arkGetTickTime64");
|
|
}
|
|
}
|
|
// this works on 32-bit versions of avs, but not on 64.
|
|
// it's better than nothing though.
|
|
return getTickTime64 ? getTickTime64() : timeGetTime();
|
|
}
|
|
|
|
// Used to keep the ring buffer populated with steady updates. 60Hz interval is too slow
|
|
static void mdxf_thread_start() {
|
|
bool expected = false;
|
|
if (!MDXF_THREAD_RUNNING.compare_exchange_strong(expected, true)) {
|
|
return;
|
|
}
|
|
|
|
log_info("mdxf", "starting poll thread");
|
|
|
|
MDXF_THREAD = std::thread([] {
|
|
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
|
|
|
|
timeutils::PreciseSleepTimer timer;
|
|
while (MDXF_THREAD_RUNNING.load(std::memory_order_acquire)) {
|
|
mdxf_poll(false);
|
|
timer.sleep(THREAD_PERIOD);
|
|
}
|
|
});
|
|
}
|
|
|
|
static void mdxf_thread_stop() {
|
|
if (!MDXF_THREAD_RUNNING.exchange(false)) {
|
|
return;
|
|
}
|
|
|
|
if (MDXF_THREAD.joinable()) {
|
|
MDXF_THREAD.join();
|
|
}
|
|
|
|
}
|
|
|
|
// Snaps measured refresh rate to best fit
|
|
static int snap_refresh_rate(int measured_hz) {
|
|
static constexpr std::array<int, 6> rates = {
|
|
60, 120, 144, 165, 180, 240
|
|
};
|
|
|
|
int best = rates[0];
|
|
int best_err = std::fabs(measured_hz - best);
|
|
|
|
for (int r : rates) {
|
|
int err = std::fabs(measured_hz - r);
|
|
if (err < best_err) {
|
|
best = r;
|
|
best_err = err;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
// Increments the number of times the update function was called,
|
|
// then calculates the current refresh rate of the game
|
|
// (20 seconds after the game starts, until 25 seconds)
|
|
static void count_calls_from_game() {
|
|
if (IS_REFRESH_RATE_DETERMINED) {
|
|
return;
|
|
}
|
|
|
|
const uint64_t current_time = arkGetTickTime64();
|
|
|
|
if (!IS_REFRESH_RATE_MEASUREMENT_STARTED) {
|
|
if (START_TIME == 0) {
|
|
START_TIME = current_time;
|
|
}
|
|
|
|
// boot screen takes about 10 seconds, so let's wait for double that
|
|
if ((current_time - START_TIME) < 20000) {
|
|
// too early, do nothing
|
|
return;
|
|
} else {
|
|
// 20s has passed for the first time, start measuring on next call
|
|
IS_REFRESH_RATE_MEASUREMENT_STARTED = true;
|
|
START_TIME = current_time;
|
|
log_debug("mdxf", "measurement begin");
|
|
return;
|
|
}
|
|
}
|
|
|
|
const uint64_t elapsed_time = current_time - START_TIME;
|
|
CALL_COUNT++;
|
|
if (elapsed_time >= 5000) {
|
|
double measured_hz = static_cast<double>(CALL_COUNT) * 1000.0 / static_cast<double>(elapsed_time);
|
|
|
|
// Account for the main loop calling this twice per iteration
|
|
measured_hz *= 0.5;
|
|
const int snapped_hz = snap_refresh_rate(static_cast<int>(measured_hz));
|
|
|
|
IS_REFRESH_RATE_DETERMINED = true;
|
|
IS_THREAD_NEEDED = (snapped_hz < THRESHOLD_REFRESH_RATE);
|
|
log_info(
|
|
"mdxf",
|
|
"detected: {} Hz, best fit: {} Hz",
|
|
static_cast<int>(measured_hz),
|
|
snapped_hz);
|
|
|
|
if (IS_THREAD_NEEDED) {
|
|
mdxf_thread_start();
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Implementations
|
|
*/
|
|
|
|
static uint64_t __cdecl ac_io_mdxf_get_control_status_buffer(int node, void *out, uint8_t index, uint8_t head_in) {
|
|
|
|
// Default error value (matches original mask behavior)
|
|
auto error_ret = static_cast<uint64_t>(node - 0x11) & 0xFFFFFFFFFFFFFF00;
|
|
|
|
// Dance Dance Revolution
|
|
if (avs::game::is_model("MDX")) {
|
|
// Select player-specific state
|
|
std::mutex* mutex = nullptr;
|
|
uint8_t* head = nullptr;
|
|
uint8_t (*buffer)[STATUS_BUFFER_SIZE];
|
|
size_t size = STATUS_BUFFER_NUM_ENTRIES;
|
|
|
|
if (node == 17 || node == 25) {
|
|
mutex = &MUTEX_P1;
|
|
head = &HEAD_P1;
|
|
buffer = BUFFERS.STATUS_BUFFER_P1;
|
|
} else if (node == 18 || node == 26) {
|
|
mutex = &MUTEX_P2;
|
|
head = &HEAD_P2;
|
|
buffer = BUFFERS.STATUS_BUFFER_P2;
|
|
} else {
|
|
memset(out, 0, STATUS_BUFFER_SIZE);
|
|
return error_ret;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(*mutex);
|
|
|
|
const uint8_t start_index = (head_in == 0xFF) ? *head : head_in;
|
|
|
|
// Compute ring index: walk backwards from start_index as index increases
|
|
// Assumes ring buffer size is a power of two
|
|
const size_t mask = size - 1;
|
|
const size_t offset = static_cast<size_t>(index) & mask;
|
|
const size_t i = (static_cast<size_t>(start_index) - offset + size) & mask;
|
|
|
|
// Copy the chosen entry
|
|
memcpy(out, buffer[i], STATUS_BUFFER_SIZE);
|
|
|
|
// Return the start value actually used
|
|
return static_cast<uint64_t>(start_index);
|
|
}
|
|
|
|
return error_ret;
|
|
}
|
|
|
|
static bool __cdecl ac_io_mdxf_set_output_level(unsigned int a1, unsigned int a2, uint8_t value) {
|
|
if (avs::game::is_model("MDX")) {
|
|
static const struct {
|
|
int a2[4];
|
|
} mapping[] = {
|
|
{
|
|
// a1 == 17
|
|
{
|
|
games::ddr::Lights::GOLD_P1_STAGE_UP_RIGHT,
|
|
games::ddr::Lights::GOLD_P1_STAGE_DOWN_LEFT,
|
|
games::ddr::Lights::GOLD_P1_STAGE_UP_LEFT,
|
|
games::ddr::Lights::GOLD_P1_STAGE_DOWN_RIGHT
|
|
}
|
|
},
|
|
{
|
|
// a1 == 18
|
|
{
|
|
games::ddr::Lights::GOLD_P2_STAGE_UP_RIGHT,
|
|
games::ddr::Lights::GOLD_P2_STAGE_DOWN_LEFT,
|
|
games::ddr::Lights::GOLD_P2_STAGE_UP_LEFT,
|
|
games::ddr::Lights::GOLD_P2_STAGE_DOWN_RIGHT
|
|
}
|
|
}
|
|
};
|
|
if ((a1 == 17 || a1 == 18) && (a2 < 4)) {
|
|
// get light from mapping
|
|
const auto light = mapping[a1 - 17].a2[a2];
|
|
|
|
// get lights
|
|
auto &lights = games::ddr::get_lights();
|
|
|
|
// write lights
|
|
GameAPI::Lights::writeLight(RI_MGR, lights[light], value / 128.f);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFPollSource source, uint64_t current_time) {
|
|
|
|
// check freeze
|
|
if (STATUS_BUFFER_FREEZE) {
|
|
return true;
|
|
}
|
|
|
|
// Dance Dance Revolution
|
|
if (avs::game::is_model("MDX")) {
|
|
// Marks this module as actively being used, allowing this function to be called from other sources
|
|
if (source == ARKMDXP4_POLL) {
|
|
if (!IS_MDXF_ACTIVE) {
|
|
log_debug("mdxf", "initializing mdxf I/O support");
|
|
IS_MDXF_ACTIVE = true;
|
|
if (acio::MDXF_BUFFER_FILL_MODE == acio::MDXFBufferFillMode::THREAD_MODE) {
|
|
IS_THREAD_NEEDED = true;
|
|
mdxf_thread_start();
|
|
}
|
|
}
|
|
if (acio::MDXF_BUFFER_FILL_MODE == acio::MDXFBufferFillMode::AUTO_MODE) {
|
|
count_calls_from_game();
|
|
}
|
|
}
|
|
|
|
uint8_t (*buffer)[STATUS_BUFFER_SIZE];
|
|
uint8_t *head = nullptr;
|
|
uint16_t *prev_state = nullptr;
|
|
uint64_t *prev_time = nullptr;
|
|
std::mutex* mutex = nullptr;
|
|
|
|
switch (node) {
|
|
case 17:
|
|
case 25:
|
|
mutex = &MUTEX_P1;
|
|
head = &HEAD_P1;
|
|
prev_state = &PREV_STATE_P1;
|
|
prev_time = &PREV_TIME_P1;
|
|
buffer = BUFFERS.STATUS_BUFFER_P1;
|
|
break;
|
|
case 18:
|
|
case 26:
|
|
mutex = &MUTEX_P2;
|
|
head = &HEAD_P2;
|
|
prev_state = &PREV_STATE_P2;
|
|
prev_time = &PREV_TIME_P2;
|
|
buffer = BUFFERS.STATUS_BUFFER_P2;
|
|
break;
|
|
default:
|
|
// return failure on unknown node
|
|
return false;
|
|
}
|
|
|
|
// Sensor Map (LDUR):
|
|
// FOOT DOWN = bit 32-35 = byte 4, bit 0-3
|
|
// FOOT UP = bit 36-39 = byte 4, bit 4-7
|
|
// FOOT RIGHT = bit 40-43 = byte 5, bit 0-3
|
|
// FOOT LEFT = bit 44-47 = byte 5, bit 4-7
|
|
static const size_t buttons_p1[] = {
|
|
games::ddr::Buttons::P1_PANEL_UP,
|
|
games::ddr::Buttons::P1_PANEL_DOWN,
|
|
games::ddr::Buttons::P1_PANEL_LEFT,
|
|
games::ddr::Buttons::P1_PANEL_RIGHT,
|
|
};
|
|
static const size_t buttons_p2[] = {
|
|
games::ddr::Buttons::P2_PANEL_UP,
|
|
games::ddr::Buttons::P2_PANEL_DOWN,
|
|
games::ddr::Buttons::P2_PANEL_LEFT,
|
|
games::ddr::Buttons::P2_PANEL_RIGHT,
|
|
};
|
|
|
|
// decide on button map
|
|
const size_t *button_map = nullptr;
|
|
int player = 0;
|
|
switch (node) {
|
|
case 17:
|
|
case 25:
|
|
button_map = &buttons_p1[0];
|
|
player = 1;
|
|
break;
|
|
case 18:
|
|
case 26:
|
|
button_map = &buttons_p2[0];
|
|
player = 2;
|
|
break;
|
|
}
|
|
|
|
uint16_t current_state;
|
|
|
|
// Only call getState() if called externally when actual input events happen, otherwise use previous known state
|
|
if (source == EXTERNAL_POLL) {
|
|
// get buttons
|
|
auto &buttons = games::ddr::get_buttons();
|
|
|
|
// get analogs
|
|
bool analog_left = false;
|
|
bool analog_right = false;
|
|
games::ddr::get_analog_x_axis(player, analog_left, analog_right);
|
|
bool analog_up = false;
|
|
bool analog_down = false;
|
|
games::ddr::get_analog_y_axis(player, analog_up, analog_down);
|
|
|
|
uint8_t up_down = 0;
|
|
uint8_t left_right = 0;
|
|
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[0])) || analog_up) {
|
|
up_down |= 0xF0;
|
|
}
|
|
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[1])) || analog_down) {
|
|
up_down |= 0x0F;
|
|
}
|
|
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[2])) || analog_left) {
|
|
left_right |= 0xF0;
|
|
}
|
|
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[3])) || analog_right) {
|
|
left_right |= 0x0F;
|
|
}
|
|
current_state = (uint16_t(up_down) << 8) | left_right;
|
|
} else {
|
|
current_state = *prev_state;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(*mutex);
|
|
|
|
const bool has_state_changed = *prev_state != current_state;
|
|
const bool has_time_changed = *prev_time < current_time;
|
|
|
|
// If state hasn't changed and either the update was triggered externally or the time hasn't changed, then don't advance head pointer or write a new entry
|
|
if (!has_state_changed && (source == EXTERNAL_POLL || !has_time_changed)) {
|
|
return true;
|
|
}
|
|
|
|
// The start and stop time cutoffs for backfilling entries. Min(..) ensures times aren't negative.
|
|
// The stop time is just before the current_time, set by BACKFILL_PADDING_MS, which avoids the last backfilled entry being too close to current_time.
|
|
uint64_t start_time = *prev_time;
|
|
const uint64_t stop_time = current_time - std::min<uint64_t>(current_time, BACKFILL_PADDING_MS);
|
|
|
|
// Ensures the first iteration will write the first entry at current_time and not backfill to time 0ms.
|
|
if (start_time == 0) {
|
|
start_time = current_time - std::min<uint64_t>(current_time, BACKFILL_INTERVAL_MS);
|
|
}
|
|
|
|
// Ensures only STATUS_BUFFER_NUM_ENTRIES entries at most are backfilled
|
|
const uint64_t max_backfill = BACKFILL_INTERVAL_MS * STATUS_BUFFER_NUM_ENTRIES;
|
|
const uint64_t min_time = current_time - std::min<uint64_t>(current_time, max_backfill);
|
|
if (start_time < min_time) {
|
|
start_time = min_time;
|
|
}
|
|
|
|
// Don't backfill entries if called externally or if a separate thread is being used to fill auxiliary entries
|
|
if (source == EXTERNAL_POLL || IS_THREAD_NEEDED) {
|
|
start_time = stop_time - 1;
|
|
}
|
|
|
|
uint64_t time = start_time;
|
|
uint16_t state = *prev_state;
|
|
|
|
// Backfill entries a fixed interval apart from each other between prev_time and current_time
|
|
while (time < stop_time) {
|
|
// Advance head pointer
|
|
*head = (*head + 1) % STATUS_BUFFER_NUM_ENTRIES;
|
|
uint8_t* buffer_entry = buffer[*head];
|
|
|
|
// Clear buffer
|
|
memset(buffer_entry, 0, STATUS_BUFFER_SIZE);
|
|
|
|
time += BACKFILL_INTERVAL_MS;
|
|
|
|
// If the stop time is reached, then write current_time and current_state instead for this final iteration
|
|
const bool isEdge = (time >= stop_time);
|
|
if (isEdge) {
|
|
state = current_state;
|
|
time = current_time;
|
|
}
|
|
|
|
// Write button state
|
|
buffer_entry[4] = (state >> 8) & 0xFF;
|
|
buffer_entry[5] = state & 0xFF;
|
|
|
|
// Write game time
|
|
*(uint64_t*)&buffer_entry[0x18] = time;
|
|
}
|
|
|
|
*prev_state = current_state;
|
|
*prev_time = current_time;
|
|
}
|
|
|
|
// return success
|
|
return true;
|
|
}
|
|
|
|
static bool __cdecl ac_io_mdxf_update_control_status_buffer(int node) {
|
|
return ac_io_mdxf_update_control_status_buffer_impl(node, ARKMDXP4_POLL, arkGetTickTime64());
|
|
}
|
|
|
|
// Used for triggering updates of the controller states from outside arkmdxp4.dll main refresh loop (i.e. within rawinput.cpp on controller events)
|
|
void mdxf_poll(bool isExternal) {
|
|
if (IS_MDXF_ACTIVE) {
|
|
const MDXFPollSource source = isExternal ? EXTERNAL_POLL : INTERNAL_POLL;
|
|
const uint64_t call_time_ms = arkGetTickTime64();
|
|
ac_io_mdxf_update_control_status_buffer_impl(17, source, call_time_ms);
|
|
ac_io_mdxf_update_control_status_buffer_impl(18, source, call_time_ms);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Module stuff
|
|
*/
|
|
|
|
acio::MDXFModule::MDXFModule(HMODULE module, acio::HookMode hookMode) : ACIOModule("MDXF", module, hookMode) {
|
|
this->status_buffer = (uint8_t*) &BUFFERS;
|
|
this->status_buffer_size = sizeof(BUFFERS);
|
|
this->status_buffer_freeze = &STATUS_BUFFER_FREEZE;
|
|
}
|
|
|
|
void acio::MDXFModule::attach() {
|
|
ACIOModule::attach();
|
|
|
|
// hooks
|
|
ACIO_MODULE_HOOK(ac_io_mdxf_get_control_status_buffer);
|
|
ACIO_MODULE_HOOK(ac_io_mdxf_set_output_level);
|
|
ACIO_MODULE_HOOK(ac_io_mdxf_update_control_status_buffer);
|
|
}
|
|
|
|
acio::MDXFModule::~MDXFModule() {
|
|
if (IS_THREAD_NEEDED) {
|
|
mdxf_thread_stop();
|
|
}
|
|
}
|