ddr: tweaks to mdxf i/o (#464)

## Link to GitHub Issue, if one exists
#452

## Description of change
* Split the MDXF I/O option into three: auto, thread, and backfill.
`auto` is the same as what `thread` used to be before this change (based
on I/O poll rate), but `thread` now always forces thread poll mode.
`auto` is the default (behavior wise, same as before)
* For the poll rate measurement, start polling after 20s mark to avoid
frame drops during game boot
* Minor code clean up

## Testing
manual test in windowed + full screen
This commit is contained in:
bicarus-dev
2025-12-20 01:37:56 -08:00
committed by GitHub
parent c38f42cb19
commit fbc8bc2502
4 changed files with 78 additions and 32 deletions
+61 -22
View File
@@ -9,6 +9,15 @@
#include "util/utils.h" #include "util/utils.h"
#include <mutex> #include <mutex>
#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 // constants
const size_t STATUS_BUFFER_SIZE = 32; const size_t STATUS_BUFFER_SIZE = 32;
const size_t STATUS_BUFFER_NUM_ENTRIES = 16; const size_t STATUS_BUFFER_NUM_ENTRIES = 16;
@@ -34,10 +43,12 @@ static const uint8_t BACKFILL_PADDING_MS = 2;
static uint64_t START_TIME = 0; static uint64_t START_TIME = 0;
static int CALL_COUNT = 0; static int CALL_COUNT = 0;
static const int THRESHOLD_REFRESH_RATE = 120; static const int THRESHOLD_REFRESH_RATE = 120;
static bool IS_REFRESH_RATE_DETERMINED = false;
static bool IS_THREAD_NEEDED = false;
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::atomic<bool> MDXF_THREAD_RUNNING{false};
static std::thread MDXF_THREAD; static std::thread MDXF_THREAD;
static constexpr int THREAD_REFRESH_RATE_HZ = 125; static constexpr int THREAD_REFRESH_RATE_HZ = 125;
@@ -53,12 +64,15 @@ static struct {
static bool STATUS_BUFFER_FREEZE = false; 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. // Decides which method to use for populating ring buffer entries for "padding".
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 // Overwritten in spicecfg using P4IO Buffer Algorithm option.
if the game's refresh rate is fast enough to forego starting the thread. // THREAD_MODE: Spins a thread running at THREAD_REFRESH_RATE_HZ which periodically fills the ring
BACKFILL_MODE: On every update cycle, fill the ring buffer with entries for the last known state BACKFILL_INTERVAL_MS apart from each other // buffer with auxiliary entries. Falls back on BACKFILL_MODE
from the time of the last entry to the current time before adding the entry for the current state. */ // BACKFILL_MODE: On every update cycle, fill the ring buffer with entries for the last known state
acio::MDXFBufferFillMode acio::MDXF_BUFFER_FILL_MODE = acio::THREAD_MODE; // 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 { typedef enum {
ARKMDXP4_POLL = 0, ARKMDXP4_POLL = 0,
@@ -89,6 +103,8 @@ static void mdxf_thread_start() {
return; return;
} }
log_info("mdxf", "starting poll thread");
MDXF_THREAD = std::thread([] { MDXF_THREAD = std::thread([] {
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL); SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
@@ -129,32 +145,50 @@ static int snap_refresh_rate(int measured_hz) {
return best; 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 // 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() { static void count_calls_from_game() {
if (IS_REFRESH_RATE_DETERMINED) { if (IS_REFRESH_RATE_DETERMINED) {
return; return;
} }
const uint64_t current_time = arkGetTickTime64(); const uint64_t current_time = arkGetTickTime64();
if (START_TIME == 0) { if (!IS_REFRESH_RATE_MEASUREMENT_STARTED) {
START_TIME = current_time; 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;
}
} }
CALL_COUNT++;
const uint64_t elapsed_time = current_time - START_TIME; const uint64_t elapsed_time = current_time - START_TIME;
if (elapsed_time >= 3000) { CALL_COUNT++;
if (elapsed_time >= 5000) {
double measured_hz = static_cast<double>(CALL_COUNT) * 1000.0 / static_cast<double>(elapsed_time); 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 // Account for the main loop calling this twice per iteration
measured_hz *= 0.5; measured_hz *= 0.5;
const int snapped_hz = snap_refresh_rate(static_cast<int>(measured_hz)); 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; IS_REFRESH_RATE_DETERMINED = true;
IS_THREAD_NEEDED = (snapped_hz < THRESHOLD_REFRESH_RATE);
log_info("ARKMDXP4", "Detected: {}Hz, Best Fit: {}Hz (Needs 125Hz Helper Thread: {})", static_cast<int>(measured_hz), snapped_hz, IS_THREAD_NEEDED ? "Yes" : "No"); log_info(
"mdxf",
"detected: {} Hz, best fit: {} Hz",
static_cast<int>(measured_hz),
snapped_hz);
if (IS_THREAD_NEEDED) { if (IS_THREAD_NEEDED) {
mdxf_thread_start(); mdxf_thread_start();
@@ -263,9 +297,14 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP
// Marks this module as actively being used, allowing this function to be called from other sources // Marks this module as actively being used, allowing this function to be called from other sources
if (source == ARKMDXP4_POLL) { if (source == ARKMDXP4_POLL) {
if (!IS_MDXF_ACTIVE) { if (!IS_MDXF_ACTIVE) {
log_debug("mdxf", "initializing mdxf I/O support");
IS_MDXF_ACTIVE = true; 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::THREAD_MODE) { if (acio::MDXF_BUFFER_FILL_MODE == acio::MDXFBufferFillMode::AUTO_MODE) {
count_calls_from_game(); count_calls_from_game();
} }
} }
@@ -458,7 +497,7 @@ void acio::MDXFModule::attach() {
} }
acio::MDXFModule::~MDXFModule() { acio::MDXFModule::~MDXFModule() {
if (acio::MDXF_BUFFER_FILL_MODE == acio::THREAD_MODE) { if (IS_THREAD_NEEDED) {
mdxf_thread_stop(); mdxf_thread_stop();
} }
} }
+11 -5
View File
@@ -3,11 +3,17 @@
#include "../module.h" #include "../module.h"
namespace acio { namespace acio {
typedef enum { enum class MDXFBufferFillMode {
THREAD_MODE = 0, // backfill mode, but if <120Hz, enable poll thread
BACKFILL_MODE = 1 AUTO_MODE,
} MDXFBufferFillMode;
// forces poll thread
THREAD_MODE,
// forces backfill mode (no poll thread)
BACKFILL_MODE
};
extern MDXFBufferFillMode MDXF_BUFFER_FILL_MODE; extern MDXFBufferFillMode MDXF_BUFFER_FILL_MODE;
+2 -3
View File
@@ -1152,11 +1152,10 @@ int main_implementation(int argc, char *argv[]) {
} }
if (options[launcher::Options::DDRP4IOBufferMode].is_active()) { if (options[launcher::Options::DDRP4IOBufferMode].is_active()) {
if (options[launcher::Options::DDRP4IOBufferMode].value_text() == "thread") { if (options[launcher::Options::DDRP4IOBufferMode].value_text() == "thread") {
acio::MDXF_BUFFER_FILL_MODE = acio::THREAD_MODE; acio::MDXF_BUFFER_FILL_MODE = acio::MDXFBufferFillMode::THREAD_MODE;
} else if (options[launcher::Options::DDRP4IOBufferMode].value_text() == "backfill") { } else if (options[launcher::Options::DDRP4IOBufferMode].value_text() == "backfill") {
acio::MDXF_BUFFER_FILL_MODE = acio::BACKFILL_MODE; acio::MDXF_BUFFER_FILL_MODE = acio::MDXFBufferFillMode::BACKFILL_MODE;
} }
// else - default (no value)
} }
if (options[launcher::Options::MidiAlgoVer].is_active()) { if (options[launcher::Options::MidiAlgoVer].is_active()) {
+4 -2
View File
@@ -2298,13 +2298,15 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.desc = .desc =
"Remember to restart after changing this value.\n\n" "Remember to restart after changing this value.\n\n"
"Sets the algorithm used to populate entries to the buffer of controller polls read by the game.\n\n" "Sets the algorithm used to populate entries to the buffer of controller polls read by the game.\n\n"
"Thread (default): Starts a thread to periodically insert polls into the buffer (falls back to Backfill if game's refresh rate is at least 120Hz).\n\n" "auto (default): thread mode if <120Hz, backfill if >=120Hz\n\n"
"Backfill: Fills the buffer on each frame with last known state info at short regular intervals up to the current time, then writes the current state.\n\n" "thread: starts a thread to periodically insert polls into the buffer\n\n"
"backfill: fills the buffer on each frame with last known state info at short regular intervals up to the current time, then writes the current state.\n\n"
"Only has an effect when emulating P4IO (arkmdxp4.dll)", "Only has an effect when emulating P4IO (arkmdxp4.dll)",
.type = OptionType::Enum, .type = OptionType::Enum,
.game_name = "Dance Dance Revolution", .game_name = "Dance Dance Revolution",
.category = "Game Options (Advanced)", .category = "Game Options (Advanced)",
.elements = { .elements = {
{"auto", ""},
{"thread", ""}, {"thread", ""},
{"backfill", ""}, {"backfill", ""},
}, },