Fix mai2 memory leak via LED 15070 rate-limiting and enhance touch handling (#101)

## Overview
This PR addresses the severe memory leak and performance issues observed in `mai2`, while also introducing improvements to the touch emulation logic.

## Root Cause Analysis
As discussed previously, the root cause of the `mai2` memory leak is not a global `segatools` buffer bug. Instead, the game aggressively spams overlapped empty reads specifically on the `LED 15070` UART. On real hardware, the serial driver naturally throttles this. Under emulation, without a throttle, it hits approximately **260kHz of empty async reads**, which causes the memory usage to explode.

## The Fix
Instead of introducing complex locking mechanisms and condition variables globally in `uart.c`, this PR applies a targeted fix:
* Added a local `Sleep(1)` directly in `common/board/led15070.c` to rate-limit empty reads on the LED path.
* Because this is isolated to LED communications, it completely resolves the memory leak without introducing any lag, livelocks, or overhead to other critical inputs.

## Additional Changes in this PR
Alongside the memory leak fix, this PR includes a few touch-related improvements (as touch emulation was reviewed during the debugging process):
* Enhanced touch input handling and improved auto-scan state management.
* Implemented IOCTL handling for touch input to properly manage communication status.

## Testing
* **mai2:** Tested successfully on multiple machines. The memory leak is completely gone, and the game runs smoothly.
* **chusan:** Tested to ensure no regressions. Sliders and inputs work flawlessly without the lag.

Reviewed-on: https://gitea.tendokyu.moe/TeamTofuShop/segatools/pulls/101
Co-authored-by: Gl0w1amp <gl0w1amp@noreply.gitea.tendokyu.moe>
Co-committed-by: Gl0w1amp <gl0w1amp@noreply.gitea.tendokyu.moe>
This commit is contained in:
Gl0w1amp
2026-04-03 15:13:27 +00:00
committed by Dniel97
parent ba6ec91cce
commit 2c2b8b11b4
5 changed files with 454 additions and 73 deletions

View File

@@ -244,6 +244,12 @@ static HRESULT led15070_handle_irp_locked(int board, struct irp *irp)
}
}
if (irp->op == IRP_OP_READ) {
if (irp->ovl != NULL && boarduart->readable.pos == 0) {
Sleep(1);
}
}
hr = uart_handle_irp(boarduart, irp);
if (FAILED(hr) || irp->op != IRP_OP_WRITE) {

View File

@@ -1,5 +1,8 @@
#include <windows.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include "hooklib/fdshark.h"
#include "hooklib/reg.h"
@@ -10,6 +13,59 @@
#include "util/dprintf.h"
#include "util/dump.h"
static HRESULT touch_handle_irp(struct irp *irp);
static HRESULT touch_handle_irp_locked(
struct irp *irp,
struct uart *uart);
static HRESULT touch_handle_read(struct irp *irp, struct uart *uart);
static HRESULT touch_handle_ioctl(struct irp *irp, struct uart *uart);
static void touch_complete_pending_read(struct uart *uart);
static void touch_release_pending_read(struct uart *uart);
static void touch_clear_auto_scan(struct uart *uart);
static bool touch_read_available(struct uart *uart);
static size_t touch_current_depth(struct uart *uart);
static void touch_shift_read(struct uart *uart, struct iobuf *read);
static HRESULT touch_enqueue(
struct uart *uart,
CONDITION_VARIABLE *cv,
const void *bytes,
size_t nbytes);
static HRESULT touch_enqueue_reply(
struct uart *uart,
CONDITION_VARIABLE *cv,
uint8_t side,
uint8_t sensor,
uint8_t command,
uint8_t value);
static void touch_auto_scan(const uint8_t player, const uint8_t state[7]);
struct touch_pending_read {
OVERLAPPED *ovl;
uint8_t *bytes;
size_t nbytes;
};
enum {
touch_pending_reads_capacity = 4096,
};
struct touch_pending_reads {
struct touch_pending_read entries[touch_pending_reads_capacity];
size_t head;
size_t count;
};
struct touch_auto_scan_state {
uint8_t frame[9];
size_t pos;
bool valid;
};
enum {
touch_status_pending = 0x00000103UL,
touch_status_success = 0x00000000UL,
};
static HRESULT read_reg_touch_1p(void *bytes, uint32_t *nbytes)
{
@@ -53,17 +109,38 @@ const char *sensor_to_str(uint8_t sensor)
}
static CRITICAL_SECTION touch_1p_lock;
static CONDITION_VARIABLE touch_1p_cv;
static struct touch_pending_reads touch_1p_pending_reads;
static struct touch_auto_scan_state touch_1p_auto_scan;
static struct uart touch_1p_uart;
static uint8_t touch_1p_written_bytes[64];
static uint8_t touch_1p_readable_bytes[64];
static uint8_t touch_1p_readable_bytes[1024];
static bool touch_1p_status = false;
static CRITICAL_SECTION touch_2p_lock;
static CONDITION_VARIABLE touch_2p_cv;
static struct touch_pending_reads touch_2p_pending_reads;
static struct touch_auto_scan_state touch_2p_auto_scan;
static struct uart touch_2p_uart;
static uint8_t touch_2p_written_bytes[64];
static uint8_t touch_2p_readable_bytes[64];
static uint8_t touch_2p_readable_bytes[1024];
static bool touch_2p_status = false;
static struct touch_pending_reads *touch_get_pending_reads(struct uart *uart)
{
return uart->port_no == 3 ? &touch_1p_pending_reads : &touch_2p_pending_reads;
}
static bool *touch_get_status_flag(struct uart *uart)
{
return uart->port_no == 3 ? &touch_1p_status : &touch_2p_status;
}
static struct touch_auto_scan_state *touch_get_auto_scan(struct uart *uart)
{
return uart->port_no == 3 ? &touch_1p_auto_scan : &touch_2p_auto_scan;
}
HRESULT touch_hook_init(const struct touch_config *cfg)
{
assert(cfg != NULL);
@@ -85,6 +162,7 @@ HRESULT touch_hook_init(const struct touch_config *cfg)
dprintf("Mai2 touch 1P: Init.\n");
InitializeCriticalSection(&touch_1p_lock);
InitializeConditionVariable(&touch_1p_cv);
uart_init(&touch_1p_uart, 3);
touch_1p_uart.written.bytes = touch_1p_written_bytes;
touch_1p_uart.written.nbytes = sizeof(touch_1p_written_bytes);
@@ -97,6 +175,7 @@ HRESULT touch_hook_init(const struct touch_config *cfg)
dprintf("Mai2 touch 2P: Init.\n");
InitializeCriticalSection(&touch_2p_lock);
InitializeConditionVariable(&touch_2p_cv);
uart_init(&touch_2p_uart, 4);
touch_2p_uart.written.bytes = touch_2p_written_bytes;
touch_2p_uart.written.nbytes = sizeof(touch_2p_written_bytes);
@@ -133,12 +212,16 @@ static HRESULT touch_handle_irp(struct irp *irp)
return hr;
}
static HRESULT touch_handle_irp_locked(struct irp *irp, struct uart *uart)
static HRESULT touch_handle_irp_locked(
struct irp *irp,
struct uart *uart)
{
HRESULT hr;
if (irp->op == IRP_OP_OPEN)
{
touch_clear_auto_scan(uart);
touch_release_pending_read(uart);
dprintf("Mai2 touch port %d: Starting backend\n", uart->port_no);
hr = mai2_dll.touch_init(touch_auto_scan);
@@ -149,10 +232,23 @@ static HRESULT touch_handle_irp_locked(struct irp *irp, struct uart *uart)
}
}
if (irp->op == IRP_OP_READ) {
return touch_handle_read(irp, uart);
}
if (irp->op == IRP_OP_IOCTL) {
return touch_handle_ioctl(irp, uart);
}
hr = uart_handle_irp(uart, irp);
if (FAILED(hr) || irp->op != IRP_OP_WRITE)
{
if (irp->op == IRP_OP_CLOSE) {
*touch_get_status_flag(uart) = false;
touch_clear_auto_scan(uart);
touch_release_pending_read(uart);
}
return hr;
}
@@ -160,9 +256,18 @@ static HRESULT touch_handle_irp_locked(struct irp *irp, struct uart *uart)
dprintf("Mai2 touch port %d WRITE:\n", uart->port_no);
dump_iobuf(&uart->written);
#endif
if (uart->written.pos < 6) {
dprintf("Mai2 touch port %d: Short write (%u bytes)\n",
uart->port_no,
(unsigned int) uart->written.pos);
uart->written.pos = 0;
return HRESULT_FROM_WIN32(ERROR_INVALID_DATA);
}
uint8_t port_no = uart->port_no;
uint8_t *src = uart->written.bytes;
uint8_t *dest = uart->readable.bytes;
switch (src[3])
{
@@ -175,18 +280,16 @@ static HRESULT touch_handle_irp_locked(struct irp *irp, struct uart *uart)
assert(mai2_dll.touch_update != NULL);
if (port_no == 3)
{
EnterCriticalSection(&touch_1p_lock);
touch_1p_status = false;
mai2_dll.touch_update(touch_1p_status, touch_2p_status);
LeaveCriticalSection(&touch_1p_lock);
}
else
{
EnterCriticalSection(&touch_2p_lock);
touch_2p_status = false;
mai2_dll.touch_update(touch_1p_status, touch_2p_status);
LeaveCriticalSection(&touch_2p_lock);
}
touch_clear_auto_scan(uart);
touch_release_pending_read(uart);
break;
case commandSTAT: // Exit Conditioning mode and resume sending touch data.
@@ -194,17 +297,13 @@ static HRESULT touch_handle_irp_locked(struct irp *irp, struct uart *uart)
assert(mai2_dll.touch_update != NULL);
if (port_no == 3)
{
EnterCriticalSection(&touch_1p_lock);
touch_1p_status = true;
mai2_dll.touch_update(touch_1p_status, touch_2p_status);
LeaveCriticalSection(&touch_1p_lock);
}
else
{
EnterCriticalSection(&touch_2p_lock);
touch_2p_status = true;
mai2_dll.touch_update(touch_1p_status, touch_2p_status);
LeaveCriticalSection(&touch_2p_lock);
}
break;
@@ -212,28 +311,40 @@ static HRESULT touch_handle_irp_locked(struct irp *irp, struct uart *uart)
#if defined(LOG_MAI2_TOUCH)
dprintf("Mai2 touch side %c: set sensor %s ratio to %d\n", src[1], sensor_to_str(src[2]), src[4]);
#endif
dest[0] = res_start;
dest[1] = src[1]; // L,R
dest[2] = src[2]; // sensor
dest[3] = commandRatio;
dest[4] = src[4]; // Ratio
dest[5] = res_end;
uart->readable.pos = 6;
// The Ratio is fixed at 0x72 and does not need to be sent to mai2io for processing.
hr = touch_enqueue_reply(
uart,
uart == &touch_1p_uart ? &touch_1p_cv : &touch_2p_cv,
src[1],
src[2],
commandRatio,
src[4]);
break;
case commandSens:
#if defined(LOG_MAI2_TOUCH)
dprintf("Mai2 touch side %c: set sensor %s sensitivity to %d\n", src[1], sensor_to_str(src[2]), src[4]);
#endif
dest[0] = res_start;
dest[1] = src[1]; // L,R
dest[2] = src[2]; // sensor
dest[3] = commandSens;
dest[4] = src[4]; // Sensitivity
dest[5] = res_end;
uart->readable.pos = 6;
mai2_dll.touch_set_sens(dest);
assert(mai2_dll.touch_set_sens != NULL);
hr = touch_enqueue_reply(
uart,
uart == &touch_1p_uart ? &touch_1p_cv : &touch_2p_cv,
src[1],
src[2],
commandSens,
src[4]);
if (SUCCEEDED(hr)) {
uint8_t sens_bytes[6];
sens_bytes[0] = res_start;
sens_bytes[1] = src[1];
sens_bytes[2] = src[2];
sens_bytes[3] = commandSens;
sens_bytes[4] = src[4];
sens_bytes[5] = res_end;
mai2_dll.touch_set_sens(sens_bytes);
}
break;
default:
@@ -249,11 +360,276 @@ static HRESULT touch_handle_irp_locked(struct irp *irp, struct uart *uart)
return hr;
}
static HRESULT touch_handle_read(struct irp *irp, struct uart *uart)
{
struct touch_pending_reads *pending_reads;
struct touch_pending_read *pending;
bool *status;
size_t tail;
pending_reads = touch_get_pending_reads(uart);
status = touch_get_status_flag(uart);
if (!touch_read_available(uart)) {
if (irp->ovl != NULL &&
*status &&
pending_reads->count < touch_pending_reads_capacity) {
tail = (pending_reads->head + pending_reads->count) %
touch_pending_reads_capacity;
pending = &pending_reads->entries[tail];
pending_reads->count++;
pending->ovl = irp->ovl;
pending->bytes = irp->read.bytes;
pending->nbytes = irp->read.nbytes;
pending->ovl->Internal = touch_status_pending;
pending->ovl->InternalHigh = 0;
if (pending->ovl->hEvent != NULL) {
ResetEvent(pending->ovl->hEvent);
}
return HRESULT_FROM_WIN32(ERROR_IO_PENDING);
}
return E_PENDING;
}
touch_shift_read(uart, &irp->read);
return S_OK;
}
static HRESULT touch_handle_ioctl(struct irp *irp, struct uart *uart)
{
if (irp->ioctl == IOCTL_SERIAL_GET_COMMSTATUS) {
uart->status.AmountInInQueue = (ULONG) touch_current_depth(uart);
uart->status.AmountInOutQueue = uart->written.pos;
return iobuf_write(&irp->read, &uart->status, sizeof(uart->status));
}
return uart_handle_irp(uart, irp);
}
static void touch_complete_pending_read(struct uart *uart)
{
struct touch_pending_reads *pending_reads;
struct touch_pending_read *pending;
struct iobuf read;
OVERLAPPED *ovl;
HANDLE event;
pending_reads = touch_get_pending_reads(uart);
if (pending_reads->count == 0 || !touch_read_available(uart)) {
return;
}
pending = &pending_reads->entries[pending_reads->head];
read.bytes = pending->bytes;
read.nbytes = pending->nbytes;
read.pos = 0;
touch_shift_read(uart, &read);
ovl = pending->ovl;
memset(pending, 0, sizeof(*pending));
pending_reads->head = (pending_reads->head + 1) %
touch_pending_reads_capacity;
pending_reads->count--;
ovl->InternalHigh = (ULONG_PTR) read.pos;
event = ovl->hEvent;
MemoryBarrier();
ovl->Internal = touch_status_success;
if (event != NULL) {
SetEvent(event);
}
}
static void touch_release_pending_read(struct uart *uart)
{
struct touch_pending_reads *pending_reads;
struct touch_pending_read *pending;
OVERLAPPED *ovl;
HANDLE event;
size_t i;
pending_reads = touch_get_pending_reads(uart);
for (i = 0; i < pending_reads->count; i++) {
pending = &pending_reads->entries[
(pending_reads->head + i) % touch_pending_reads_capacity];
if (pending->ovl == NULL) {
continue;
}
ovl = pending->ovl;
ovl->InternalHigh = 0;
event = ovl->hEvent;
MemoryBarrier();
ovl->Internal = touch_status_success;
if (event != NULL) {
SetEvent(event);
}
}
memset(pending_reads, 0, sizeof(*pending_reads));
}
static void touch_clear_auto_scan(struct uart *uart)
{
struct touch_auto_scan_state *auto_scan;
auto_scan = touch_get_auto_scan(uart);
auto_scan->pos = 0;
auto_scan->valid = false;
}
static bool touch_read_available(struct uart *uart)
{
return uart->readable.pos > 0 || touch_get_auto_scan(uart)->valid;
}
static size_t touch_current_depth(struct uart *uart)
{
size_t depth;
struct touch_auto_scan_state *auto_scan;
depth = uart->readable.pos;
auto_scan = touch_get_auto_scan(uart);
if (auto_scan->valid) {
depth += sizeof(auto_scan->frame) - auto_scan->pos;
}
return depth;
}
static void touch_shift_read(struct uart *uart, struct iobuf *read)
{
struct touch_auto_scan_state *auto_scan;
size_t read_avail;
size_t frame_avail;
size_t chunksz;
if (uart->readable.pos > 0) {
iobuf_shift(read, &uart->readable);
return;
}
auto_scan = touch_get_auto_scan(uart);
if (!auto_scan->valid) {
return;
}
read_avail = read->nbytes - read->pos;
frame_avail = sizeof(auto_scan->frame) - auto_scan->pos;
chunksz = read_avail < frame_avail ? read_avail : frame_avail;
memcpy(&read->bytes[read->pos],
&auto_scan->frame[auto_scan->pos],
chunksz);
read->pos += chunksz;
auto_scan->pos += chunksz;
if (auto_scan->pos == sizeof(auto_scan->frame)) {
auto_scan->pos = 0;
auto_scan->valid = false;
}
}
static HRESULT touch_enqueue(
struct uart *uart,
CONDITION_VARIABLE *cv,
const void *bytes,
size_t nbytes)
{
HRESULT hr;
hr = iobuf_write(&uart->readable, bytes, nbytes);
if (FAILED(hr)) {
dprintf("Mai2 touch port %d: RX queue overflow (%u/%u bytes)\n",
uart->port_no,
(unsigned int) uart->readable.pos,
(unsigned int) uart->readable.nbytes);
} else {
while (touch_read_available(uart) &&
touch_get_pending_reads(uart)->count > 0) {
touch_complete_pending_read(uart);
}
WakeConditionVariable(cv);
}
return hr;
}
static HRESULT touch_enqueue_reply(
struct uart *uart,
CONDITION_VARIABLE *cv,
uint8_t side,
uint8_t sensor,
uint8_t command,
uint8_t value)
{
uint8_t reply[6];
reply[0] = res_start;
reply[1] = side;
reply[2] = sensor;
reply[3] = command;
reply[4] = value;
reply[5] = res_end;
return touch_enqueue(uart, cv, reply, sizeof(reply));
}
static void touch_auto_scan(const uint8_t player, const uint8_t state[7])
{
struct uart *touch_uart = player == 1 ? &touch_1p_uart : &touch_2p_uart;
touch_uart->readable.bytes[0] = res_start;
memcpy(&touch_uart->readable.bytes[1], state, 7);
touch_uart->readable.bytes[8] = res_end;
touch_uart->readable.pos = 9;
struct touch_auto_scan_state *auto_scan;
struct uart *touch_uart;
CRITICAL_SECTION *touch_lock;
CONDITION_VARIABLE *touch_cv;
uint8_t frame[9];
if (player == 1) {
touch_uart = &touch_1p_uart;
touch_lock = &touch_1p_lock;
touch_cv = &touch_1p_cv;
} else {
touch_uart = &touch_2p_uart;
touch_lock = &touch_2p_lock;
touch_cv = &touch_2p_cv;
}
if (touch_uart->readable.bytes == NULL) {
return;
}
frame[0] = res_start;
memcpy(&frame[1], state, 7);
frame[8] = res_end;
EnterCriticalSection(touch_lock);
if (!*touch_get_status_flag(touch_uart)) {
LeaveCriticalSection(touch_lock);
return;
}
auto_scan = touch_get_auto_scan(touch_uart);
memcpy(auto_scan->frame, frame, sizeof(frame));
auto_scan->pos = 0;
auto_scan->valid = true;
while (touch_read_available(touch_uart) &&
touch_get_pending_reads(touch_uart)->count > 0) {
touch_complete_pending_read(touch_uart);
}
WakeConditionVariable(touch_cv);
LeaveCriticalSection(touch_lock);
}

View File

@@ -29,10 +29,3 @@ extern const char *sensor_map[34];
const char *sensor_to_str(uint8_t sensor);
HRESULT touch_hook_init(const struct touch_config *cfg);
static HRESULT touch_handle_irp(struct irp *irp);
static HRESULT touch_handle_irp_locked(struct irp *irp, struct uart *uart);
/* Called in mai2io to send touch data.
Similar to chuni slider_res_auto_scan, but the host does not require periodic updates.
Touch data is sent only when there is a change. */
static void touch_auto_scan(const uint8_t player, const uint8_t state[7]);

View File

@@ -2,6 +2,7 @@
#include <limits.h>
#include <process.h>
#include <string.h>
#include "mai2hook/touch.h"
#include "mai2io/config.h"
@@ -160,34 +161,30 @@ void mai2_io_touch_set_sens(uint8_t *bytes) {
}
void mai2_io_touch_update(bool player1, bool player2) {
if (mai2_io_cfg.debug_input_1p) {
if (player1 && mai2_io_touch_1p_thread == NULL) {
mai2_io_touch_1p_thread = (HANDLE)_beginthreadex(
NULL, 0, mai2_io_touch_1p_thread_proc, _callback, 0, NULL);
} else if (!player1 && mai2_io_touch_1p_thread != NULL) {
mai2_io_touch_1p_stop_flag = true;
if (player1 && mai2_io_touch_1p_thread == NULL) {
mai2_io_touch_1p_thread = (HANDLE)_beginthreadex(
NULL, 0, mai2_io_touch_1p_thread_proc, _callback, 0, NULL);
} else if (!player1 && mai2_io_touch_1p_thread != NULL) {
mai2_io_touch_1p_stop_flag = true;
WaitForSingleObject(mai2_io_touch_1p_thread, INFINITE);
CloseHandle(mai2_io_touch_1p_thread);
mai2_io_touch_1p_thread = NULL;
WaitForSingleObject(mai2_io_touch_1p_thread, INFINITE);
CloseHandle(mai2_io_touch_1p_thread);
mai2_io_touch_1p_thread = NULL;
mai2_io_touch_1p_stop_flag = false;
}
mai2_io_touch_1p_stop_flag = false;
}
if (mai2_io_cfg.debug_input_2p) {
if (player2 && mai2_io_touch_2p_thread == NULL) {
mai2_io_touch_2p_thread = (HANDLE)_beginthreadex(
NULL, 0, mai2_io_touch_2p_thread_proc, _callback, 0, NULL);
} else if (!player2 && mai2_io_touch_2p_thread != NULL) {
mai2_io_touch_2p_stop_flag = true;
if (player2 && mai2_io_touch_2p_thread == NULL) {
mai2_io_touch_2p_thread = (HANDLE)_beginthreadex(
NULL, 0, mai2_io_touch_2p_thread_proc, _callback, 0, NULL);
} else if (!player2 && mai2_io_touch_2p_thread != NULL) {
mai2_io_touch_2p_stop_flag = true;
WaitForSingleObject(mai2_io_touch_2p_thread, INFINITE);
CloseHandle(mai2_io_touch_2p_thread);
mai2_io_touch_2p_thread = NULL;
WaitForSingleObject(mai2_io_touch_2p_thread, INFINITE);
CloseHandle(mai2_io_touch_2p_thread);
mai2_io_touch_2p_thread = NULL;
mai2_io_touch_2p_stop_flag = false;
}
mai2_io_touch_2p_stop_flag = false;
}
}
@@ -197,14 +194,18 @@ static unsigned int __stdcall mai2_io_touch_1p_thread_proc(void *ctx) {
while (!mai2_io_touch_1p_stop_flag) {
uint8_t state[7] = {0, 0, 0, 0, 0, 0, 0};
for (int i = 0; i < 34; i++) {
if (GetAsyncKeyState(mai2_io_cfg.vk_1p_touch[i])) {
int byteIndex = i / 5;
int bitIndex = i % 5;
state[byteIndex] |= (1 << bitIndex);
if (mai2_io_cfg.debug_input_1p) {
for (int i = 0; i < 34; i++) {
if (GetAsyncKeyState(mai2_io_cfg.vk_1p_touch[i])) {
int byteIndex = i / 5;
int bitIndex = i % 5;
state[byteIndex] |= (1 << bitIndex);
}
}
}
callback(1, state);
Sleep(1);
}
return 0;
@@ -216,14 +217,18 @@ static unsigned int __stdcall mai2_io_touch_2p_thread_proc(void *ctx) {
while (!mai2_io_touch_2p_stop_flag) {
uint8_t state[7] = {0, 0, 0, 0, 0, 0, 0};
for (int i = 0; i < 34; i++) {
if (GetAsyncKeyState(mai2_io_cfg.vk_2p_touch[i])) {
int byteIndex = i / 5;
int bitIndex = i % 5;
state[byteIndex] |= (1 << bitIndex);
if (mai2_io_cfg.debug_input_2p) {
for (int i = 0; i < 34; i++) {
if (GetAsyncKeyState(mai2_io_cfg.vk_2p_touch[i])) {
int byteIndex = i / 5;
int bitIndex = i % 5;
state[byteIndex] |= (1 << bitIndex);
}
}
}
callback(2, state);
Sleep(1);
}
return 0;

View File

@@ -120,7 +120,8 @@ void mai2_io_touch_set_sens(uint8_t *bytes);
*
* This function determines whether the game is ready to accept touch input based on the states of player 1 and player 2.
* If the game is ready, it creates or stops the corresponding threads to handle touch data for each player.
* Whether or not threads are created for each player is controlled by `mai2_io_cfg.debug_input_1p` and `mai2_io_cfg.debug_input_2p` configuration.
* The `debug_input_1p` and `debug_input_2p` settings only control whether keyboard input is mapped into touch bits.
* When a player is active, the thread continues to send idle all-zero frames even with debug input disabled.
*
* @param player1 If `true`, indicates the game is ready to accept touch data from player 1, `false` means the game is not ready.
* @param player2 If `true`, indicates the game is ready to accept touch data from player 2, `false` means the game is not ready.