mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-02 06:40:42 -07:00
MDX: Merged Thread and Backfill Implementations (P4IO Timing Fix) (#460)
## Link to GitHub Issue, if one exists #452 ## Description of change (Another extension of #450) Merged the old threading implementation into the current backfill implementation because it was showing signs of having slightly better timing performance. The following description is taken from the strikethrough text from the referenced PR above: >When the module is booted, it counts how many times ac_io_mdxf_update_control_status_buffer was called in the first three seconds, then calculates the approximate refresh rate of the current game instance. If the refresh rate is under THRESHOLD_REFRESH_RATE (120Hz), a low-priority thread dedicated to inserting polls into the ring buffers is started and run at THREAD_REFRESH_RATE_HZ (125Hz). Combined with the polls inserted by the main arkmdxp4 thread and rawinput real-time events, the 125Hz update cadence is enough to bring the span of poll history represented by the ring buffers closer to parity with what arkmdxp4 expects. The default mode is `THREAD_MODE` but can be switched to `BACKFILL_MODE` (which could possibly be made as a `spicecfg` option). There was also a fixed bug in the `ac_io_mdxf_get_control_status_buffer` function, which should not have been writing to the `head` pointer. This was allowing for an edge case situation where the `head` pointer was advanced in another thread then rewound back to the previous `head` in this thread. The real implementation only reads the pointer and returns it. ## Testing - Logged the ring buffer states after many iterations and verified regular structure - Same playtesting as the prior PRs
This commit is contained in:
+133
-15
@@ -30,6 +30,19 @@ 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 bool IS_REFRESH_RATE_DETERMINED = false;
|
||||
static 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 {
|
||||
@@ -40,9 +53,22 @@ static struct {
|
||||
|
||||
static bool STATUS_BUFFER_FREEZE = false;
|
||||
|
||||
typedef enum {
|
||||
THREAD_MODE = 0,
|
||||
BACKFILL_MODE = 1
|
||||
} MDXFBufferFillMode;
|
||||
|
||||
/* Decides which method to use for populating ring buffer entries for "padding". Could possibly be made configurable through spicecfg.
|
||||
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
|
||||
if the game's refresh rate is fast enough to forego starting the thread.
|
||||
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. */
|
||||
static const MDXFBufferFillMode BUFFER_FILL_MODE = THREAD_MODE;
|
||||
|
||||
typedef enum {
|
||||
ARKMDXP4_POLL = 0,
|
||||
EXTERNAL_POLL = 1
|
||||
INTERNAL_POLL = 1,
|
||||
EXTERNAL_POLL = 2
|
||||
} MDXFPollSource;
|
||||
|
||||
typedef uint64_t (__cdecl *ARK_GET_TICK_TIME64_T)();
|
||||
@@ -61,6 +87,86 @@ static uint64_t arkGetTickTime64() {
|
||||
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;
|
||||
}
|
||||
|
||||
MDXF_THREAD = std::thread([] {
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
|
||||
|
||||
while (MDXF_THREAD_RUNNING.load(std::memory_order_acquire)) {
|
||||
mdxf_poll(false);
|
||||
std::this_thread::sleep_for(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 after 3 seconds has passed
|
||||
static void count_calls_from_game() {
|
||||
if (IS_REFRESH_RATE_DETERMINED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint64_t current_time = arkGetTickTime64();
|
||||
|
||||
if (START_TIME == 0) {
|
||||
START_TIME = current_time;
|
||||
}
|
||||
|
||||
CALL_COUNT++;
|
||||
|
||||
const uint64_t elapsed_time = current_time - START_TIME;
|
||||
if (elapsed_time >= 3000) {
|
||||
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_THREAD_NEEDED = (snapped_hz < THRESHOLD_REFRESH_RATE);
|
||||
IS_REFRESH_RATE_DETERMINED = true;
|
||||
|
||||
log_info("ARKMDXP4", "Detected: {}Hz, Best Fit: {}Hz (Needs 125Hz Helper Thread: {})", static_cast<int>(measured_hz), snapped_hz, IS_THREAD_NEEDED ? "Yes" : "No");
|
||||
|
||||
if (IS_THREAD_NEEDED) {
|
||||
mdxf_thread_start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Implementations
|
||||
*/
|
||||
@@ -93,21 +199,19 @@ static uint64_t __cdecl ac_io_mdxf_get_control_status_buffer(int node, void *out
|
||||
|
||||
std::lock_guard<std::mutex> lock(*mutex);
|
||||
|
||||
if (head_in != 0xFF) {
|
||||
*head = head_in;
|
||||
}
|
||||
const uint8_t start_index = (head_in == 0xFF) ? *head : head_in;
|
||||
|
||||
// Compute ring index: walk backwards from head as index increases
|
||||
// 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>(*head) - offset + size) & 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 head value actually used
|
||||
return static_cast<uint64_t>(*head);
|
||||
// Return the start value actually used
|
||||
return static_cast<uint64_t>(start_index);
|
||||
}
|
||||
|
||||
return error_ret;
|
||||
@@ -162,8 +266,13 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP
|
||||
// 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 (!IS_MDXF_ACTIVE && source == ARKMDXP4_POLL) {
|
||||
IS_MDXF_ACTIVE = true;
|
||||
if (source == ARKMDXP4_POLL) {
|
||||
if (!IS_MDXF_ACTIVE) {
|
||||
IS_MDXF_ACTIVE = true;
|
||||
}
|
||||
if (BUFFER_FILL_MODE == THREAD_MODE) {
|
||||
count_calls_from_game();
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t (*buffer)[STATUS_BUFFER_SIZE];
|
||||
@@ -226,6 +335,8 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -276,8 +387,8 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP
|
||||
start_time = min_time;
|
||||
}
|
||||
|
||||
// Only write one entry if called externally
|
||||
if (source == EXTERNAL_POLL) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -323,11 +434,12 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer(int node) {
|
||||
}
|
||||
|
||||
// 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() {
|
||||
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, EXTERNAL_POLL, call_time_ms);
|
||||
ac_io_mdxf_update_control_status_buffer_impl(18, EXTERNAL_POLL, call_time_ms);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,3 +461,9 @@ void acio::MDXFModule::attach() {
|
||||
ACIO_MODULE_HOOK(ac_io_mdxf_set_output_level);
|
||||
ACIO_MODULE_HOOK(ac_io_mdxf_update_control_status_buffer);
|
||||
}
|
||||
|
||||
acio::MDXFModule::~MDXFModule() {
|
||||
if (BUFFER_FILL_MODE == THREAD_MODE) {
|
||||
mdxf_thread_stop();
|
||||
}
|
||||
}
|
||||
@@ -9,5 +9,6 @@ namespace acio {
|
||||
MDXFModule(HMODULE module, HookMode hookMode);
|
||||
|
||||
virtual void attach() override;
|
||||
~MDXFModule() override;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
#pragma once
|
||||
|
||||
// Called from rawinput thread whenever inputs have just been updated
|
||||
void mdxf_poll();
|
||||
void mdxf_poll(bool isExternal);
|
||||
@@ -1850,7 +1850,7 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
|
||||
}
|
||||
|
||||
// update controller state ring buffers (DDR/MDXF)
|
||||
mdxf_poll();
|
||||
mdxf_poll(true);
|
||||
|
||||
// call the default window handler for cleanup
|
||||
DefWindowProc(hWnd, msg, wparam, lParam);
|
||||
|
||||
Reference in New Issue
Block a user