mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-02 14:50:41 -07:00
MDX: Decoupled Input Polling Rate From Game Refresh Rate (P4IO Timing Fix) (#446)
## Context In the real implementation of `ac_io_mdxf_update_control_status_buffer`, it doesn't actually add entries to the pad state ring buffers (it only marks the head entry as the "last consumed" entry), and `arkmdxp4.dll` expects the ring buffers to be updated at regular intervals inside `libacio2.dll` at ~250Hz per player. Instead, in the emulated version, the updates happen in this function and that regular interval is established internally using the game's refresh rate (60Hz, 120Hz, 144Hz, etc.). This effectively locks the polling rate of pad input to that refresh rate, which is quite low for pad input. ## Description of change The polling rate lock was circumvented by calling `ac_io_mdxf_update_control_status_buffer` as soon as the game receives controller input in `rawinput.cpp`. A simple wrapper function `mdxf_poll` was exposed so press and release events could be added to the ring buffers in real time as they happen. After each controller event in `rawinput.cpp`, `mdxf_poll` calls `ac_io_mdxf_update_control_status_buffer` for each player, checking the pad's current state and adding it to the ring buffer if it's changed since the last call. This results in the 0 -> 1 (press) and 1 -> 0 (release) state transitions used for gameplay to be based only on the real-time events inserted from `rawinput.cpp`, while leaving the rest of the entries generated from the main `arkmdxp4.dll` thread as "padding" the game expects (I actually tried reducing the ring buffers to *only* write entries on state transitions, but it broke gameplay, which means the game still depends regular reporting of the pad state). Here's an example of how a ring buffer would look if an arrow was pressed at some time 32ms (assume 165Hz game refresh rate, ~6ms updates): ``` Old (only frame updates): 1 | 36ms 0 | 30ms 0 | 24ms 0 | 18ms 0 | 12ms 0 | 6ms 0 | 0ms ``` The game only sees the press event at time 36ms on frame update, so time 36ms is assigned to the press time of that arrow (4ms later than the real press time). ``` New (frame updates + event updates from rawinput.cpp): 1 | 36ms 1 | 32ms <- from rawinput.cpp 0 | 30ms 0 | 24ms 0 | 18ms 0 | 12ms 0 | 6ms ``` The game sees the real press time 32ms on frame update, so the correct press time 32ms is assigned. ## Testing - Played on various refresh rates and verified the controller input remains highly responsive. - Inspected the ring buffers with a debugger and verified entries for press and release events from `rawinput.cpp` were correctly being added for their respective players.
This commit is contained in:
+136
-71
@@ -1,4 +1,5 @@
|
||||
#include "mdxf.h"
|
||||
#include "mdxf_poll.h"
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "games/ddr/io.h"
|
||||
@@ -6,35 +7,53 @@
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
#include <mutex>
|
||||
|
||||
// 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 std::mutex MUTEX_P1;
|
||||
static std::mutex MUTEX_P2;
|
||||
|
||||
static bool MDXF_IS_ACTIVE = false;
|
||||
|
||||
// buffers
|
||||
#pragma pack(push, 1)
|
||||
static struct {
|
||||
uint8_t STATUS_BUFFER_P1[16][STATUS_BUFFER_SIZE] {};
|
||||
uint8_t STATUS_BUFFER_P2[16][STATUS_BUFFER_SIZE] {};
|
||||
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;
|
||||
|
||||
typedef enum {
|
||||
ARKMDXP4_POLL = 0,
|
||||
EXTERNAL_POLL = 1
|
||||
} MDXFPollSource;
|
||||
|
||||
typedef uint64_t (__cdecl *ARK_GET_TICK_TIME64_T)();
|
||||
|
||||
static uint64_t arkGetTickTime64() {
|
||||
static ARK_GET_TICK_TIME64_T getTickTime64 =
|
||||
(ARK_GET_TICK_TIME64_T)GetProcAddress(avs::game::DLL_INSTANCE, "arkGetTickTime64");
|
||||
if (getTickTime64 == nullptr) {
|
||||
// this works on 32-bit versions of avs, but not on 64.
|
||||
// it's better than nothing though.
|
||||
return timeGetTime();
|
||||
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");
|
||||
}
|
||||
}
|
||||
return getTickTime64();
|
||||
// this works on 32-bit versions of avs, but not on 64.
|
||||
// it's better than nothing though.
|
||||
return getTickTime64 ? getTickTime64() : timeGetTime();
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -43,45 +62,47 @@ static uint64_t arkGetTickTime64() {
|
||||
|
||||
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)
|
||||
// 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
|
||||
uint8_t head;
|
||||
uint8_t (*buffer)[STATUS_BUFFER_SIZE];
|
||||
size_t size;
|
||||
|
||||
if (node == 17 || node == 25) {
|
||||
head = HEAD_P1;
|
||||
buffer = BUFFERS.STATUS_BUFFER_P1;
|
||||
size = std::size(BUFFERS.STATUS_BUFFER_P1);
|
||||
} else if (node == 18 || node == 26) {
|
||||
head = HEAD_P2;
|
||||
buffer = BUFFERS.STATUS_BUFFER_P2;
|
||||
size = std::size(BUFFERS.STATUS_BUFFER_P2);
|
||||
} else {
|
||||
memset(out, 0, STATUS_BUFFER_SIZE);
|
||||
return error_ret;
|
||||
}
|
||||
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);
|
||||
|
||||
if (head_in != 0xFF) {
|
||||
head = head_in;
|
||||
}
|
||||
if (head_in != 0xFF) {
|
||||
*head = head_in;
|
||||
}
|
||||
|
||||
// Compute ring index: walk backwards from head 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;
|
||||
// Compute ring index: walk backwards from head 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;
|
||||
|
||||
// Copy the chosen entry
|
||||
memcpy(out, buffer[i], STATUS_BUFFER_SIZE);
|
||||
// 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 head value actually used
|
||||
return static_cast<uint64_t>(*head);
|
||||
}
|
||||
|
||||
return error_ret;
|
||||
@@ -126,38 +147,45 @@ static bool __cdecl ac_io_mdxf_set_output_level(unsigned int a1, unsigned int a2
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool __cdecl ac_io_mdxf_update_control_status_buffer(int node) {
|
||||
static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFPollSource source) {
|
||||
|
||||
// check freeze
|
||||
if (STATUS_BUFFER_FREEZE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// clear buffer
|
||||
uint8_t *buffer = nullptr;
|
||||
switch (node) {
|
||||
case 17:
|
||||
case 25:
|
||||
// increase counter
|
||||
HEAD_P1 = (HEAD_P1 + 1) % std::size(BUFFERS.STATUS_BUFFER_P1);
|
||||
buffer = BUFFERS.STATUS_BUFFER_P1[HEAD_P1];
|
||||
break;
|
||||
case 18:
|
||||
case 26:
|
||||
// increase counter
|
||||
HEAD_P2 = (HEAD_P2 + 1) % std::size(BUFFERS.STATUS_BUFFER_P2);
|
||||
buffer = BUFFERS.STATUS_BUFFER_P2[HEAD_P2];
|
||||
break;
|
||||
default:
|
||||
|
||||
// return failure on unknown node
|
||||
return false;
|
||||
}
|
||||
memset(buffer, 0, STATUS_BUFFER_SIZE);
|
||||
|
||||
// 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 (!MDXF_IS_ACTIVE && source == ARKMDXP4_POLL) {
|
||||
MDXF_IS_ACTIVE = true;
|
||||
}
|
||||
|
||||
uint8_t (*buffer)[STATUS_BUFFER_SIZE];
|
||||
uint8_t *head = nullptr;
|
||||
uint16_t *prev_state = nullptr;
|
||||
std::mutex* mutex = nullptr;
|
||||
|
||||
switch (node) {
|
||||
case 17:
|
||||
case 25:
|
||||
mutex = &MUTEX_P1;
|
||||
head = &HEAD_P1;
|
||||
prev_state = &PREV_STATE_P1;
|
||||
buffer = BUFFERS.STATUS_BUFFER_P1;
|
||||
break;
|
||||
case 18:
|
||||
case 26:
|
||||
mutex = &MUTEX_P2;
|
||||
head = &HEAD_P2;
|
||||
prev_state = &PREV_STATE_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
|
||||
@@ -189,29 +217,66 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer(int node) {
|
||||
break;
|
||||
}
|
||||
|
||||
*(uint64_t*)&buffer[0x18] = arkGetTickTime64();
|
||||
|
||||
// get buttons
|
||||
auto &buttons = games::ddr::get_buttons();
|
||||
uint8_t up_down = 0;
|
||||
uint8_t left_right = 0;
|
||||
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[0]))) {
|
||||
buffer[4] |= 0xF0;
|
||||
up_down |= 0xF0;
|
||||
}
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[1]))) {
|
||||
buffer[4] |= 0x0F;
|
||||
up_down |= 0x0F;
|
||||
}
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[2]))) {
|
||||
buffer[5] |= 0xF0;
|
||||
left_right |= 0xF0;
|
||||
}
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[3]))) {
|
||||
buffer[5] |= 0x0F;
|
||||
left_right |= 0x0F;
|
||||
}
|
||||
|
||||
uint16_t current_state = (uint16_t(up_down) << 8) | left_right;
|
||||
|
||||
std::lock_guard<std::mutex> lock(*mutex);
|
||||
|
||||
// If state hasn't changed and the update was triggered outside arkmdxp4.dll, then don't advance head pointer or write a new entry
|
||||
if (current_state == *prev_state && source != ARKMDXP4_POLL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
*prev_state = current_state;
|
||||
|
||||
// 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);
|
||||
|
||||
// Write button state
|
||||
buffer_entry[4] = up_down;
|
||||
buffer_entry[5] = left_right;
|
||||
|
||||
//Write current game time
|
||||
*(uint64_t*)&buffer_entry[0x18] = arkGetTickTime64();
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if (MDXF_IS_ACTIVE) {
|
||||
ac_io_mdxf_update_control_status_buffer_impl(17, EXTERNAL_POLL);
|
||||
ac_io_mdxf_update_control_status_buffer_impl(18, EXTERNAL_POLL);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Module stuff
|
||||
*/
|
||||
@@ -229,4 +294,4 @@ void acio::MDXFModule::attach() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// mdxf_poll.h
|
||||
#pragma once
|
||||
|
||||
// Called from rawinput thread whenever inputs have just been updated
|
||||
void mdxf_poll();
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "piuio.h"
|
||||
#include "touch.h"
|
||||
#include "acio/mdxf/mdxf_poll.h"
|
||||
|
||||
extern "C" {
|
||||
#include "external/usbhidusage/usb-hid-usage.h"
|
||||
@@ -1825,6 +1826,9 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
|
||||
|
||||
// free device
|
||||
device.mutex->unlock();
|
||||
|
||||
// update controller state ring buffers (DDR/MDXF)
|
||||
mdxf_poll();
|
||||
}
|
||||
|
||||
// call the default window handler for cleanup
|
||||
|
||||
Reference in New Issue
Block a user