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:
cchike
2025-12-12 19:03:04 -08:00
committed by GitHub
parent c3428236f8
commit 6f58d9ab68
3 changed files with 145 additions and 71 deletions
+136 -71
View File
@@ -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);
}
}
+5
View File
@@ -0,0 +1,5 @@
// mdxf_poll.h
#pragma once
// Called from rawinput thread whenever inputs have just been updated
void mdxf_poll();
+4
View File
@@ -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