From 0c72697baf55ed084946d3733296fcc5eda79d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Mon, 23 Feb 2026 21:24:59 +0300 Subject: [PATCH 01/11] libretro: splitscreen multiplayer --- libretro-build/Makefile.common | 3 + src/platform/libretro/libretro.c | 132 +- src/platform/libretro/libretro_core_options.h | 15 + src/platform/libretro/libretro_input.c | 81 ++ src/platform/libretro/libretro_input.h | 20 + src/platform/libretro/libretro_lockstep.c | 1065 +++++++++++++++++ src/platform/libretro/libretro_multiplayer.c | 434 +++++++ src/platform/libretro/libretro_multiplayer.h | 65 + 8 files changed, 1739 insertions(+), 76 deletions(-) create mode 100644 src/platform/libretro/libretro_input.c create mode 100644 src/platform/libretro/libretro_input.h create mode 100644 src/platform/libretro/libretro_lockstep.c create mode 100644 src/platform/libretro/libretro_multiplayer.c create mode 100644 src/platform/libretro/libretro_multiplayer.h diff --git a/libretro-build/Makefile.common b/libretro-build/Makefile.common index 18a4065168a..870752ae8a1 100644 --- a/libretro-build/Makefile.common +++ b/libretro-build/Makefile.common @@ -112,6 +112,9 @@ SOURCES_C := $(CORE_DIR)/src/arm/arm.c \ $(CORE_DIR)/src/gba/cart/vfame.c \ $(CORE_DIR)/src/gba/video.c \ $(CORE_DIR)/src/platform/libretro/memory.c \ + $(CORE_DIR)/src/platform/libretro/libretro_input.c \ + $(CORE_DIR)/src/platform/libretro/libretro_lockstep.c \ + $(CORE_DIR)/src/platform/libretro/libretro_multiplayer.c \ $(CORE_DIR)/src/platform/libretro/libretro.c \ $(CORE_DIR)/src/sm83/isa-sm83.c \ $(CORE_DIR)/src/sm83/sm83.c \ diff --git a/src/platform/libretro/libretro.c b/src/platform/libretro/libretro.c index 4b05a422f6b..3c58c543eb4 100644 --- a/src/platform/libretro/libretro.c +++ b/src/platform/libretro/libretro.c @@ -42,6 +42,8 @@ FS_Archive sdmcArchive; #endif #include "libretro_core_options.h" +#include "libretro_input.h" +#include "libretro_multiplayer.h" #define GBA_RESAMPLED_RATE 65536 static unsigned targetSampleRate = GBA_RESAMPLED_RATE; @@ -135,19 +137,9 @@ static bool audioLowPassEnabled = false; static int32_t audioLowPassRange = 0; static int32_t audioLowPassLeftPrev = 0; static int32_t audioLowPassRightPrev = 0; - -static const int keymap[] = { - RETRO_DEVICE_ID_JOYPAD_A, - RETRO_DEVICE_ID_JOYPAD_B, - RETRO_DEVICE_ID_JOYPAD_SELECT, - RETRO_DEVICE_ID_JOYPAD_START, - RETRO_DEVICE_ID_JOYPAD_RIGHT, - RETRO_DEVICE_ID_JOYPAD_LEFT, - RETRO_DEVICE_ID_JOYPAD_UP, - RETRO_DEVICE_ID_JOYPAD_DOWN, - RETRO_DEVICE_ID_JOYPAD_R, - RETRO_DEVICE_ID_JOYPAD_L, -}; +static struct mLibretroTurboState turboState; +static struct mLibretroMultiplayer multiplayer; +static char loadedRomPath[PATH_MAX]; #ifndef GIT_VERSION #define GIT_VERSION "" @@ -1364,6 +1356,12 @@ void retro_get_system_av_info(struct retro_system_av_info* info) { info->geometry.max_height = height; info->geometry.aspect_ratio = width / (double) height; + mLibretroMultiplayerAdjustGeometry(&multiplayer, + &info->geometry.base_width, + &info->geometry.base_height, + &info->geometry.max_width, + &info->geometry.max_height, + &info->geometry.aspect_ratio); info->timing.fps = core->frequency(core) / (float) core->frameCycles(core); #ifdef M_CORE_GBA @@ -1473,9 +1471,14 @@ void retro_init(void) { retroAudioLatency = 0; updateAudioLatency = false; updateAudioRate = false; + mLibretroTurboStateInit(&turboState); + mLibretroMultiplayerInit(&multiplayer, VIDEO_WIDTH_MAX, VIDEO_HEIGHT_MAX); + loadedRomPath[0] = '\0'; } void retro_deinit(void) { + mLibretroMultiplayerDeinit(&multiplayer, core); + if (outputBuffer) { #ifdef _3DS linearFree(outputBuffer); @@ -1515,43 +1518,15 @@ void retro_deinit(void) { audioLowPassRange = 0; audioLowPassLeftPrev = 0; audioLowPassRightPrev = 0; -} - -static int turboclock = 0; -static bool indownstate = true; - -int16_t cycleturbo(bool a, bool b, bool l, bool r) { - int16_t buttons = 0; - turboclock++; - if (turboclock >= 2) { - turboclock = 0; - indownstate = !indownstate; - } - - if (a) { - buttons |= indownstate << 0; - } - - if (b) { - buttons |= indownstate << 1; - } - - if (l) { - buttons |= indownstate << 9; - } - - if (r) { - buttons |= indownstate << 8; - } - - return buttons; + core = NULL; } void retro_run(void) { if (deferredSetup) { _doDeferredSetup(); } - uint16_t keys; + uint16_t player1Keys; + uint16_t player2Keys; bool skipFrame = false; inputPollCallback(); @@ -1575,36 +1550,16 @@ void retro_run(void) { #if defined(COLOR_16_BIT) && defined(COLOR_5_6_5) _loadPostProcessingSettings(); #endif + mLibretroMultiplayerUpdateMode(&multiplayer, environCallback); + mLibretroMultiplayerApplyMode(&multiplayer, core, data, dataSize, loadedRomPath, logCallback); #ifdef M_CORE_GB _updateGbPal(); #endif } - keys = 0; - int i; - if (useBitmasks) { - int16_t joypadMask = inputCallback(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_MASK); - for (i = 0; i < sizeof(keymap) / sizeof(*keymap); ++i) { - keys |= ((joypadMask >> keymap[i]) & 1) << i; - } - // XXX: turbo keys, should be moved to frontend -#define JOYPAD_BIT(BUTTON) (1 << RETRO_DEVICE_ID_JOYPAD_ ## BUTTON) - keys |= cycleturbo(joypadMask & JOYPAD_BIT(X), joypadMask & JOYPAD_BIT(Y), joypadMask & JOYPAD_BIT(L2), joypadMask & JOYPAD_BIT(R2)); -#undef JOYPAD_BIT - } else { - for (i = 0; i < sizeof(keymap) / sizeof(*keymap); ++i) { - keys |= (!!inputCallback(0, RETRO_DEVICE_JOYPAD, 0, keymap[i])) << i; - } - // XXX: turbo keys, should be moved to frontend - keys |= cycleturbo( - inputCallback(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X), - inputCallback(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y), - inputCallback(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L2), - inputCallback(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R2) - ); - } - - core->setKeys(core, keys); + player1Keys = mLibretroInputReadKeys(0, inputCallback, useBitmasks, &turboState); + player2Keys = mLibretroInputReadKeys(1, inputCallback, useBitmasks, &turboState); + mLibretroMultiplayerSetKeys(&multiplayer, core, player1Keys, player2Keys); if (!luxSensorUsed) { static bool wasAdjustingLux = false; @@ -1683,7 +1638,7 @@ void retro_run(void) { updateAudioLatency = false; } - core->runFrame(core); + mLibretroMultiplayerRunFrame(&multiplayer, core); unsigned width, height; core->currentVideoSize(core, &width, &height); @@ -1707,15 +1662,29 @@ void retro_run(void) { } if (!skipFrame) { + if (multiplayer.active) { + size_t outPitch; + unsigned outWidth; + unsigned outHeight; + const mColor* frame = mLibretroMultiplayerComposeFrame(&multiplayer, outputBuffer, width, height, &outPitch, &outWidth, &outHeight); + videoCallback(frame, outWidth, outHeight, outPitch); + } else { #if defined(COLOR_16_BIT) && defined(COLOR_5_6_5) - if (videoPostProcess) { - videoPostProcess(width, height); - videoCallback(ppOutputBuffer, width, height, VIDEO_WIDTH_MAX * sizeof(mColor)); - } else + if (videoPostProcess) { + videoPostProcess(width, height); + videoCallback(ppOutputBuffer, width, height, VIDEO_WIDTH_MAX * sizeof(mColor)); + } else #endif - videoCallback(outputBuffer, width, height, VIDEO_WIDTH_MAX * sizeof(mColor)); + videoCallback(outputBuffer, width, height, VIDEO_WIDTH_MAX * sizeof(mColor)); + } } else { - videoCallback(NULL, width, height, VIDEO_WIDTH_MAX * sizeof(mColor)); + size_t outPitch = VIDEO_WIDTH_MAX * sizeof(mColor); + unsigned outWidth = width; + unsigned outHeight = height; + if (multiplayer.active) { + mLibretroMultiplayerComposeFrame(&multiplayer, outputBuffer, width, height, &outPitch, &outWidth, &outHeight); + } + videoCallback(NULL, outWidth, outHeight, outPitch); } /* Check whether audio sample rate has changed */ @@ -1995,6 +1964,12 @@ bool retro_load_game(const struct retro_game_info* game) { return false; } + if (game->path) { + snprintf(loadedRomPath, sizeof(loadedRomPath), "%s", game->path); + } else { + loadedRomPath[0] = '\0'; + } + if (game->data) { data = anonymousMemoryMap(game->size); dataSize = game->size; @@ -2087,6 +2062,8 @@ bool retro_load_game(const struct retro_game_info* game) { _reloadSettings(); core->loadROM(core, rom); deferredSetup = true; + mLibretroMultiplayerUpdateMode(&multiplayer, environCallback); + mLibretroMultiplayerApplyMode(&multiplayer, core, data, dataSize, loadedRomPath, logCallback); const char* sysDir = 0; const char* biosName = 0; @@ -2154,12 +2131,15 @@ void retro_unload_game(void) { if (!core) { return; } + mLibretroMultiplayerDeinit(&multiplayer, core); mCoreConfigDeinit(&core->config); core->deinit(core); + core = NULL; mappedMemoryFree(data, dataSize); data = 0; mappedMemoryFree(savedata, GBA_SIZE_FLASH1M); savedata = 0; + loadedRomPath[0] = '\0'; } size_t retro_serialize_size(void) { diff --git a/src/platform/libretro/libretro_core_options.h b/src/platform/libretro/libretro_core_options.h index 9bd553bdf23..f7aec11a0bf 100644 --- a/src/platform/libretro/libretro_core_options.h +++ b/src/platform/libretro/libretro_core_options.h @@ -101,6 +101,21 @@ struct retro_core_option_v2_definition option_defs_us[] = { }, "Autodetect" }, + { + "mgba_multiplayer_splitscreen", + "Multiplayer Splitscreen (Restart)", + NULL, + "Runs a linked second GBA instance and combines both player views into a single output frame.", + NULL, + "system", + { + { "OFF", "disabled" }, + { "Side by Side", "2-Player Side by Side" }, + { "Top/Bottom", "2-Player Top/Bottom" }, + { NULL, NULL }, + }, + "OFF" + }, { "mgba_use_bios", "Use BIOS File if Found (Restart)", diff --git a/src/platform/libretro/libretro_input.c b/src/platform/libretro/libretro_input.c new file mode 100644 index 00000000000..15099f18fec --- /dev/null +++ b/src/platform/libretro/libretro_input.c @@ -0,0 +1,81 @@ +#include "libretro_input.h" + +#include + +static const int keymap[] = { + RETRO_DEVICE_ID_JOYPAD_A, + RETRO_DEVICE_ID_JOYPAD_B, + RETRO_DEVICE_ID_JOYPAD_SELECT, + RETRO_DEVICE_ID_JOYPAD_START, + RETRO_DEVICE_ID_JOYPAD_RIGHT, + RETRO_DEVICE_ID_JOYPAD_LEFT, + RETRO_DEVICE_ID_JOYPAD_UP, + RETRO_DEVICE_ID_JOYPAD_DOWN, + RETRO_DEVICE_ID_JOYPAD_R, + RETRO_DEVICE_ID_JOYPAD_L, +}; + +void mLibretroTurboStateInit(struct mLibretroTurboState* state) { + memset(state, 0, sizeof(*state)); + state->downState[0] = true; + state->downState[1] = true; +} + +static int16_t _cycleTurbo(unsigned port, bool a, bool b, bool l, bool r, struct mLibretroTurboState* turboState) { + int16_t buttons = 0; + if (port > 1) { + port = 0; + } + turboState->clock[port]++; + if (turboState->clock[port] >= 2) { + turboState->clock[port] = 0; + turboState->downState[port] = !turboState->downState[port]; + } + + if (a) { + buttons |= turboState->downState[port] << 0; + } + + if (b) { + buttons |= turboState->downState[port] << 1; + } + + if (l) { + buttons |= turboState->downState[port] << 9; + } + + if (r) { + buttons |= turboState->downState[port] << 8; + } + + return buttons; +} + +uint16_t mLibretroInputReadKeys(unsigned port, retro_input_state_t inputCallback, bool useBitmasks, struct mLibretroTurboState* turboState) { + uint16_t keys = 0; + size_t i; + + if (useBitmasks) { + int16_t joypadMask = inputCallback(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_MASK); + for (i = 0; i < sizeof(keymap) / sizeof(*keymap); ++i) { + keys |= ((joypadMask >> keymap[i]) & 1) << i; + } +#define JOYPAD_BIT(BUTTON) (1 << RETRO_DEVICE_ID_JOYPAD_ ## BUTTON) + keys |= _cycleTurbo(port, joypadMask & JOYPAD_BIT(X), joypadMask & JOYPAD_BIT(Y), joypadMask & JOYPAD_BIT(L2), joypadMask & JOYPAD_BIT(R2), turboState); +#undef JOYPAD_BIT + } else { + for (i = 0; i < sizeof(keymap) / sizeof(*keymap); ++i) { + keys |= (!!inputCallback(port, RETRO_DEVICE_JOYPAD, 0, keymap[i])) << i; + } + keys |= _cycleTurbo( + port, + inputCallback(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X), + inputCallback(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y), + inputCallback(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L2), + inputCallback(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R2), + turboState + ); + } + + return keys; +} diff --git a/src/platform/libretro/libretro_input.h b/src/platform/libretro/libretro_input.h new file mode 100644 index 00000000000..9139f513f2a --- /dev/null +++ b/src/platform/libretro/libretro_input.h @@ -0,0 +1,20 @@ +#ifndef MGBA_LIBRETRO_INPUT_H +#define MGBA_LIBRETRO_INPUT_H + +#include + +#include "libretro.h" + +CXX_GUARD_START + +struct mLibretroTurboState { + int clock[2]; + bool downState[2]; +}; + +void mLibretroTurboStateInit(struct mLibretroTurboState* state); +uint16_t mLibretroInputReadKeys(unsigned port, retro_input_state_t inputCallback, bool useBitmasks, struct mLibretroTurboState* turboState); + +CXX_GUARD_END + +#endif diff --git a/src/platform/libretro/libretro_lockstep.c b/src/platform/libretro/libretro_lockstep.c new file mode 100644 index 00000000000..6ae596897a1 --- /dev/null +++ b/src/platform/libretro/libretro_lockstep.c @@ -0,0 +1,1065 @@ +/* Copyright (c) 2013-2024 Jeffrey Pfau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include + +#include +#include + +#define DRIVER_ID 0x6B636F4C +#define DRIVER_STATE_VERSION 1 +#define LOCKSTEP_INTERVAL 4096 +#define UNLOCKED_INTERVAL 4096 +#define HARD_SYNC_INTERVAL 0x80000 +#define TARGET(P) (1 << (P)) +#define TARGET_ALL 0xF +#define TARGET_PRIMARY 0x1 +#define TARGET_SECONDARY ((TARGET_ALL) & ~(TARGET_PRIMARY)) + +DECL_BITFIELD(GBASIOLockstepSerializedFlags, uint32_t); +DECL_BITS(GBASIOLockstepSerializedFlags, DriverMode, 0, 3); +DECL_BITS(GBASIOLockstepSerializedFlags, NumEvents, 3, 4); +DECL_BIT(GBASIOLockstepSerializedFlags, Asleep, 7); +DECL_BIT(GBASIOLockstepSerializedFlags, DataReceived, 8); +DECL_BIT(GBASIOLockstepSerializedFlags, EventScheduled, 9); +DECL_BITS(GBASIOLockstepSerializedFlags, Player0Mode, 10, 3); +DECL_BITS(GBASIOLockstepSerializedFlags, Player1Mode, 13, 3); +DECL_BITS(GBASIOLockstepSerializedFlags, Player2Mode, 16, 3); +DECL_BITS(GBASIOLockstepSerializedFlags, Player3Mode, 19, 3); +DECL_BITS(GBASIOLockstepSerializedFlags, TransferMode, 28, 3); +DECL_BIT(GBASIOLockstepSerializedFlags, TransferActive, 31); + +DECL_BITFIELD(GBASIOLockstepSerializedEventFlags, uint32_t); +DECL_BITS(GBASIOLockstepSerializedEventFlags, Type, 0, 3); + +struct GBASIOLockstepSerializedEvent { + int32_t timestamp; + int32_t playerId; + GBASIOLockstepSerializedEventFlags flags; + int32_t reserved[5]; + union { + int32_t mode; + int32_t finishCycle; + int32_t padding[4]; + }; +}; +static_assert(sizeof(struct GBASIOLockstepSerializedEvent) == 0x30, "GBA lockstep event savestate struct sized wrong"); + +struct GBASIOLockstepSerializedState { + uint32_t version; + GBASIOLockstepSerializedFlags flags; + uint32_t reserved[2]; + + struct { + int32_t nextEvent; + uint32_t reservedDriver[7]; + } driver; + + struct { + int32_t playerId; + int32_t cycleOffset; + uint32_t reservedPlayer[2]; + struct GBASIOLockstepSerializedEvent events[MAX_LOCKSTEP_EVENTS]; + } player; + + // playerId 0 only + struct { + int32_t cycle; + uint32_t waiting; + int32_t nextHardSync; + uint32_t reservedCoordinator[3]; + uint16_t multiData[4]; + uint32_t normalData[4]; + } coordinator; +}; +static_assert(offsetof(struct GBASIOLockstepSerializedState, driver) == 0x10, "GBA lockstep savestate driver offset wrong"); +static_assert(offsetof(struct GBASIOLockstepSerializedState, player) == 0x30, "GBA lockstep savestate player offset wrong"); +static_assert(offsetof(struct GBASIOLockstepSerializedState, coordinator) == 0x1C0, "GBA lockstep savestate coordinator offset wrong"); +static_assert(sizeof(struct GBASIOLockstepSerializedState) == 0x1F0, "GBA lockstep savestate struct sized wrong"); + +static bool GBASIOLockstepDriverInit(struct GBASIODriver* driver); +static void GBASIOLockstepDriverDeinit(struct GBASIODriver* driver); +static void GBASIOLockstepDriverReset(struct GBASIODriver* driver); +static uint32_t GBASIOLockstepDriverId(const struct GBASIODriver* driver); +static bool GBASIOLockstepDriverLoadState(struct GBASIODriver* driver, const void* state, size_t size); +static void GBASIOLockstepDriverSaveState(struct GBASIODriver* driver, void** state, size_t* size); +static void GBASIOLockstepDriverSetMode(struct GBASIODriver* driver, enum GBASIOMode mode); +static bool GBASIOLockstepDriverHandlesMode(struct GBASIODriver* driver, enum GBASIOMode mode); +static int GBASIOLockstepDriverConnectedDevices(struct GBASIODriver* driver); +static int GBASIOLockstepDriverDeviceId(struct GBASIODriver* driver); +static uint16_t GBASIOLockstepDriverWriteSIOCNT(struct GBASIODriver* driver, uint16_t value); +static uint16_t GBASIOLockstepDriverWriteRCNT(struct GBASIODriver* driver, uint16_t value); +static bool GBASIOLockstepDriverStart(struct GBASIODriver* driver); +static void GBASIOLockstepDriverFinishMultiplayer(struct GBASIODriver* driver, uint16_t data[4]); +static uint8_t GBASIOLockstepDriverFinishNormal8(struct GBASIODriver* driver); +static uint32_t GBASIOLockstepDriverFinishNormal32(struct GBASIODriver* driver); + +static void GBASIOLockstepCoordinatorWaitOnPlayers(struct GBASIOLockstepCoordinator*, struct GBASIOLockstepPlayer*); +static void GBASIOLockstepCoordinatorAckPlayer(struct GBASIOLockstepCoordinator*, struct GBASIOLockstepPlayer*); +static void GBASIOLockstepCoordinatorWakePlayers(struct GBASIOLockstepCoordinator*); + +static int32_t GBASIOLockstepTime(struct GBASIOLockstepPlayer*); +static void GBASIOLockstepPlayerWake(struct GBASIOLockstepPlayer*); +static void GBASIOLockstepPlayerSleep(struct GBASIOLockstepPlayer*); + +static void _advanceCycle(struct GBASIOLockstepCoordinator*, struct GBASIOLockstepPlayer*); +static void _removePlayer(struct GBASIOLockstepCoordinator*, struct GBASIOLockstepPlayer*); +static void _reconfigPlayers(struct GBASIOLockstepCoordinator*); +static int32_t _untilNextSync(struct GBASIOLockstepCoordinator*, struct GBASIOLockstepPlayer*); +static void _enqueueEvent(struct GBASIOLockstepCoordinator*, const struct GBASIOLockstepEvent*, uint32_t target); +static void _setData(struct GBASIOLockstepCoordinator*, uint32_t id, struct GBASIO* sio); +static void _setReady(struct GBASIOLockstepCoordinator*, struct GBASIOLockstepPlayer* activePlayer, int playerId, enum GBASIOMode mode); +static void _hardSync(struct GBASIOLockstepCoordinator*, struct GBASIOLockstepPlayer*); + +static void _lockstepEvent(struct mTiming*, void* context, uint32_t cyclesLate); + +static void _verifyAwake(struct GBASIOLockstepCoordinator* coordinator) { +#ifdef NDEBUG + UNUSED(coordinator); +#else + int i; + int asleep = 0; + for (i = 0; i < coordinator->nAttached; ++i) { + if (!coordinator->attachedPlayers[i]) { + continue; + } + struct GBASIOLockstepPlayer* player = TableLookup(&coordinator->players, coordinator->attachedPlayers[i]); + asleep += player->asleep; + } + mASSERT_DEBUG(!asleep || asleep < coordinator->nAttached); +#endif +} + +void GBASIOLockstepDriverCreate(struct GBASIOLockstepDriver* driver, struct mLockstepUser* user) { + memset(driver, 0, sizeof(*driver)); + driver->d.init = GBASIOLockstepDriverInit; + driver->d.deinit = GBASIOLockstepDriverDeinit; + driver->d.reset = GBASIOLockstepDriverReset; + driver->d.driverId = GBASIOLockstepDriverId; + driver->d.loadState = GBASIOLockstepDriverLoadState; + driver->d.saveState = GBASIOLockstepDriverSaveState; + driver->d.setMode = GBASIOLockstepDriverSetMode; + driver->d.handlesMode = GBASIOLockstepDriverHandlesMode; + driver->d.deviceId = GBASIOLockstepDriverDeviceId; + driver->d.connectedDevices = GBASIOLockstepDriverConnectedDevices; + driver->d.writeSIOCNT = GBASIOLockstepDriverWriteSIOCNT; + driver->d.writeRCNT = GBASIOLockstepDriverWriteRCNT; + driver->d.start = GBASIOLockstepDriverStart; + driver->d.finishMultiplayer = GBASIOLockstepDriverFinishMultiplayer; + driver->d.finishNormal8 = GBASIOLockstepDriverFinishNormal8; + driver->d.finishNormal32 = GBASIOLockstepDriverFinishNormal32; + driver->event.context = driver; + driver->event.callback = _lockstepEvent; + driver->event.name = "GBA SIO Lockstep"; + driver->event.priority = 0x80; + driver->user = user; +} + +static bool GBASIOLockstepDriverInit(struct GBASIODriver* driver) { + GBASIOLockstepDriverReset(driver); + return true; +} + +static void GBASIOLockstepDriverDeinit(struct GBASIODriver* driver) { + struct GBASIOLockstepDriver* lockstep = (struct GBASIOLockstepDriver*) driver; + struct GBASIOLockstepCoordinator* coordinator = lockstep->coordinator; + struct GBASIOLockstepPlayer* player = TableLookup(&coordinator->players, lockstep->lockstepId); + if (player) { + _removePlayer(coordinator, player); + } + mTimingDeschedule(&lockstep->d.p->p->timing, &lockstep->event); + lockstep->lockstepId = 0; +} + +static void GBASIOLockstepDriverReset(struct GBASIODriver* driver) { + struct GBASIOLockstepDriver* lockstep = (struct GBASIOLockstepDriver*) driver; + struct GBASIOLockstepCoordinator* coordinator = lockstep->coordinator; + struct GBASIOLockstepPlayer* player; + if (!lockstep->lockstepId) { + unsigned id; + player = calloc(1, sizeof(*player)); + player->driver = lockstep; + player->mode = driver->p->mode; + player->playerId = -1; + + int i; + for (i = 0; i < MAX_LOCKSTEP_EVENTS - 1; ++i) { + player->buffer[i].next = &player->buffer[i + 1]; + } + player->freeList = &player->buffer[0]; + + while (true) { + if (coordinator->nextId == UINT_MAX) { + coordinator->nextId = 0; + } + ++coordinator->nextId; + id = coordinator->nextId; + if (!TableLookup(&coordinator->players, id)) { + TableInsert(&coordinator->players, id, player); + lockstep->lockstepId = id; + break; + } + } + _reconfigPlayers(coordinator); + player->cycleOffset = mTimingCurrentTime(&driver->p->p->timing) - coordinator->cycle; + if (player->playerId != 0) { + struct GBASIOLockstepEvent event = { + .type = SIO_EV_ATTACH, + .playerId = player->playerId, + .timestamp = GBASIOLockstepTime(player), + }; + _enqueueEvent(coordinator, &event, TARGET_ALL & ~TARGET(player->playerId)); + } + } else { + player = TableLookup(&coordinator->players, lockstep->lockstepId); + player->cycleOffset = mTimingCurrentTime(&driver->p->p->timing) - coordinator->cycle; + } + + if (mTimingIsScheduled(&lockstep->d.p->p->timing, &lockstep->event)) { + return; + } + + int32_t nextEvent; + _setReady(coordinator, player, player->playerId, player->mode); + if (TableSize(&coordinator->players) == 1) { + coordinator->cycle = mTimingCurrentTime(&lockstep->d.p->p->timing); + nextEvent = LOCKSTEP_INTERVAL; + } else { + _setReady(coordinator, player, 0, coordinator->transferMode); + nextEvent = _untilNextSync(lockstep->coordinator, player); + } + mTimingSchedule(&lockstep->d.p->p->timing, &lockstep->event, nextEvent); +} + +static uint32_t GBASIOLockstepDriverId(const struct GBASIODriver* driver) { + UNUSED(driver); + return DRIVER_ID; +} + +static unsigned _modeEnumToInt(enum GBASIOMode mode) { + switch ((int) mode) { + case -1: + default: + return 0; + case GBA_SIO_MULTI: + return 1; + case GBA_SIO_NORMAL_8: + return 2; + case GBA_SIO_NORMAL_32: + return 3; + case GBA_SIO_GPIO: + return 4; + case GBA_SIO_UART: + return 5; + case GBA_SIO_JOYBUS: + return 6; + } +} + +static enum GBASIOMode _modeIntToEnum(unsigned mode) { + const enum GBASIOMode modes[8] = { + -1, GBA_SIO_MULTI, GBA_SIO_NORMAL_8, GBA_SIO_NORMAL_32, GBA_SIO_GPIO, GBA_SIO_UART, GBA_SIO_JOYBUS, -1 + }; + return modes[mode & 7]; +} + +static bool GBASIOLockstepDriverLoadState(struct GBASIODriver* driver, const void* data, size_t size) { + struct GBASIOLockstepDriver* lockstep = (struct GBASIOLockstepDriver*) driver; + struct GBASIOLockstepCoordinator* coordinator = lockstep->coordinator; + if (size != sizeof(struct GBASIOLockstepSerializedState)) { + mLOG(GBA_SIO, WARN, "Incorrect state size: expected %" PRIz "X, got %" PRIz "X", sizeof(struct GBASIOLockstepSerializedState), size); + return false; + } + const struct GBASIOLockstepSerializedState* state = data; + bool error = false; + uint32_t ucheck; + int32_t check; + LOAD_32LE(ucheck, 0, &state->version); + if (ucheck > DRIVER_STATE_VERSION) { + mLOG(GBA_SIO, WARN, "Invalid or too new save state: expected %u, got %u", DRIVER_STATE_VERSION, ucheck); + return false; + } + + struct GBASIOLockstepPlayer* player = TableLookup(&coordinator->players, lockstep->lockstepId); + LOAD_32LE(check, 0, &state->player.playerId); + if (check != player->playerId) { + mLOG(GBA_SIO, WARN, "State is for different player: expected %d, got %d", player->playerId, check); + error = true; + goto out; + } + + GBASIOLockstepSerializedFlags flags = 0; + LOAD_32LE(flags, 0, &state->flags); + LOAD_32LE(player->cycleOffset, 0, &state->player.cycleOffset); + player->dataReceived = GBASIOLockstepSerializedFlagsGetDataReceived(flags); + player->mode = _modeIntToEnum(GBASIOLockstepSerializedFlagsGetDriverMode(flags)); + + player->otherModes[0] = _modeIntToEnum(GBASIOLockstepSerializedFlagsGetPlayer0Mode(flags)); + player->otherModes[1] = _modeIntToEnum(GBASIOLockstepSerializedFlagsGetPlayer1Mode(flags)); + player->otherModes[2] = _modeIntToEnum(GBASIOLockstepSerializedFlagsGetPlayer2Mode(flags)); + player->otherModes[3] = _modeIntToEnum(GBASIOLockstepSerializedFlagsGetPlayer3Mode(flags)); + + if (GBASIOLockstepSerializedFlagsGetEventScheduled(flags)) { + int32_t when; + LOAD_32LE(when, 0, &state->driver.nextEvent); + mTimingSchedule(&driver->p->p->timing, &lockstep->event, when); + } + + if (GBASIOLockstepSerializedFlagsGetAsleep(flags)) { + if (!player->asleep && player->driver->user->sleep) { + player->driver->user->sleep(player->driver->user); + } + player->asleep = true; + } else { + if (player->asleep && player->driver->user->wake) { + player->driver->user->wake(player->driver->user); + } + player->asleep = false; + } + + unsigned i; + for (i = 0; i < MAX_LOCKSTEP_EVENTS - 1; ++i) { + player->buffer[i].next = &player->buffer[i + 1]; + } + player->freeList = &player->buffer[0]; + player->queue = NULL; + + struct GBASIOLockstepEvent** lastEvent = &player->queue; + for (i = 0; i < GBASIOLockstepSerializedFlagsGetNumEvents(flags) && i < MAX_LOCKSTEP_EVENTS; ++i) { + struct GBASIOLockstepEvent* event = player->freeList; + const struct GBASIOLockstepSerializedEvent* stateEvent = &state->player.events[i]; + player->freeList = player->freeList->next; + *lastEvent = event; + lastEvent = &event->next; + + GBASIOLockstepSerializedEventFlags flags; + LOAD_32LE(flags, 0, &stateEvent->flags); + LOAD_32LE(event->timestamp, 0, &stateEvent->timestamp); + LOAD_32LE(event->playerId, 0, &stateEvent->playerId); + event->type = GBASIOLockstepSerializedEventFlagsGetType(flags); + switch (event->type) { + case SIO_EV_ATTACH: + case SIO_EV_DETACH: + case SIO_EV_HARD_SYNC: + break; + case SIO_EV_MODE_SET: + LOAD_32LE(event->mode, 0, &stateEvent->mode); + break; + case SIO_EV_TRANSFER_START: + LOAD_32LE(event->finishCycle, 0, &stateEvent->finishCycle); + break; + } + } + + if (player->playerId == 0) { + LOAD_32LE(coordinator->cycle, 0, &state->coordinator.cycle); + LOAD_32LE(coordinator->waiting, 0, &state->coordinator.waiting); + LOAD_32LE(coordinator->nextHardSync, 0, &state->coordinator.nextHardSync); + for (i = 0; i < 4; ++i) { + LOAD_16LE(coordinator->multiData[i], 0, &state->coordinator.multiData[i]); + LOAD_32LE(coordinator->normalData[i], 0, &state->coordinator.normalData[i]); + } + coordinator->transferMode = _modeIntToEnum(GBASIOLockstepSerializedFlagsGetTransferMode(flags)); + coordinator->transferActive = GBASIOLockstepSerializedFlagsGetTransferActive(flags); + } +out: + if (!error) { + mTimingInterrupt(&driver->p->p->timing); + } + return !error; +} + +static void GBASIOLockstepDriverSaveState(struct GBASIODriver* driver, void** stateOut, size_t* size) { + struct GBASIOLockstepDriver* lockstep = (struct GBASIOLockstepDriver*) driver; + struct GBASIOLockstepCoordinator* coordinator = lockstep->coordinator; + struct GBASIOLockstepSerializedState* state = calloc(1, sizeof(*state)); + + STORE_32LE(DRIVER_STATE_VERSION, 0, &state->version); + + STORE_32LE(lockstep->event.when - mTimingCurrentTime(&driver->p->p->timing), 0, &state->driver.nextEvent); + + struct GBASIOLockstepPlayer* player = TableLookup(&coordinator->players, lockstep->lockstepId); + GBASIOLockstepSerializedFlags flags = 0; + STORE_32LE(player->playerId, 0, &state->player.playerId); + STORE_32LE(player->cycleOffset, 0, &state->player.cycleOffset); + flags = GBASIOLockstepSerializedFlagsSetAsleep(flags, player->asleep); + flags = GBASIOLockstepSerializedFlagsSetDataReceived(flags, player->dataReceived); + flags = GBASIOLockstepSerializedFlagsSetDriverMode(flags, _modeEnumToInt(player->mode)); + flags = GBASIOLockstepSerializedFlagsSetEventScheduled(flags, mTimingIsScheduled(&driver->p->p->timing, &lockstep->event)); + + flags = GBASIOLockstepSerializedFlagsSetPlayer0Mode(flags, _modeEnumToInt(player->otherModes[0])); + flags = GBASIOLockstepSerializedFlagsSetPlayer1Mode(flags, _modeEnumToInt(player->otherModes[1])); + flags = GBASIOLockstepSerializedFlagsSetPlayer2Mode(flags, _modeEnumToInt(player->otherModes[2])); + flags = GBASIOLockstepSerializedFlagsSetPlayer3Mode(flags, _modeEnumToInt(player->otherModes[3])); + + struct GBASIOLockstepEvent* event = player->queue; + size_t i; + for (i = 0; i < MAX_LOCKSTEP_EVENTS && event; ++i, event = event->next) { + struct GBASIOLockstepSerializedEvent* stateEvent = &state->player.events[i]; + GBASIOLockstepSerializedEventFlags flags = GBASIOLockstepSerializedEventFlagsSetType(0, event->type); + STORE_32LE(event->timestamp, 0, &stateEvent->timestamp); + STORE_32LE(event->playerId, 0, &stateEvent->playerId); + switch (event->type) { + case SIO_EV_ATTACH: + case SIO_EV_DETACH: + case SIO_EV_HARD_SYNC: + break; + case SIO_EV_MODE_SET: + STORE_32LE(event->mode, 0, &stateEvent->mode); + break; + case SIO_EV_TRANSFER_START: + STORE_32LE(event->finishCycle, 0, &stateEvent->finishCycle); + break; + } + STORE_32LE(flags, 0, &stateEvent->flags); + } + flags = GBASIOLockstepSerializedFlagsSetNumEvents(flags, i); + + if (player->playerId == 0) { + STORE_32LE(coordinator->cycle, 0, &state->coordinator.cycle); + STORE_32LE(coordinator->waiting, 0, &state->coordinator.waiting); + STORE_32LE(coordinator->nextHardSync, 0, &state->coordinator.nextHardSync); + for (i = 0; i < 4; ++i) { + STORE_16LE(coordinator->multiData[i], 0, &state->coordinator.multiData[i]); + STORE_32LE(coordinator->normalData[i], 0, &state->coordinator.normalData[i]); + } + flags = GBASIOLockstepSerializedFlagsSetTransferMode(flags, _modeEnumToInt(coordinator->transferMode)); + flags = GBASIOLockstepSerializedFlagsSetTransferActive(flags, coordinator->transferActive); + } + STORE_32LE(flags, 0, &state->flags); + *stateOut = state; + *size = sizeof(*state); +} + +static void GBASIOLockstepDriverSetMode(struct GBASIODriver* driver, enum GBASIOMode mode) { + struct GBASIOLockstepDriver* lockstep = (struct GBASIOLockstepDriver*) driver; + struct GBASIOLockstepCoordinator* coordinator = lockstep->coordinator; + struct GBASIOLockstepPlayer* player = TableLookup(&coordinator->players, lockstep->lockstepId); + bool waitOnPlayers = false; + if (mode != player->mode) { + player->mode = mode; + struct GBASIOLockstepEvent event = { + .type = SIO_EV_MODE_SET, + .playerId = player->playerId, + .timestamp = GBASIOLockstepTime(player), + .mode = mode, + }; + if (player->playerId == 0) { + mASSERT_DEBUG(!coordinator->transferActive); // TODO + coordinator->transferMode = mode; + waitOnPlayers = !coordinator->waiting; + } + _setReady(coordinator, player, player->playerId, mode); + _enqueueEvent(coordinator, &event, TARGET_ALL & ~TARGET(player->playerId)); + if (waitOnPlayers) { + GBASIOLockstepCoordinatorWaitOnPlayers(coordinator, player); + } else if (player->playerId == 0) { + mLOG(GBA_SIO, DEBUG, "Deferring mode wait while barrier %X is active", coordinator->waiting); + } + } +} + +static bool GBASIOLockstepDriverHandlesMode(struct GBASIODriver* driver, enum GBASIOMode mode) { + UNUSED(driver); + UNUSED(mode); + return true; +} + +static int GBASIOLockstepDriverConnectedDevices(struct GBASIODriver* driver) { + struct GBASIOLockstepDriver* lockstep = (struct GBASIOLockstepDriver*) driver; + struct GBASIOLockstepCoordinator* coordinator = lockstep->coordinator; + if (!lockstep->lockstepId) { + return 0; + } + int attached = coordinator->nAttached - 1; + return attached; +} + +static int GBASIOLockstepDriverDeviceId(struct GBASIODriver* driver) { + struct GBASIOLockstepDriver* lockstep = (struct GBASIOLockstepDriver*) driver; + struct GBASIOLockstepCoordinator* coordinator = lockstep->coordinator; + int playerId = 0; + struct GBASIOLockstepPlayer* player = TableLookup(&coordinator->players, lockstep->lockstepId); + if (player && player->playerId >= 0) { + playerId = player->playerId; + } + return playerId; +} + +static uint16_t GBASIOLockstepDriverWriteSIOCNT(struct GBASIODriver* driver, uint16_t value) { + UNUSED(driver); + mLOG(GBA_SIO, DEBUG, "Lockstep: SIOCNT <- %04X", value); + return value; +} + +static uint16_t GBASIOLockstepDriverWriteRCNT(struct GBASIODriver* driver, uint16_t value) { + UNUSED(driver); + mLOG(GBA_SIO, DEBUG, "Lockstep: RCNT <- %04X", value); + return value; +} + +static bool GBASIOLockstepDriverStart(struct GBASIODriver* driver) { + struct GBASIOLockstepDriver* lockstep = (struct GBASIOLockstepDriver*) driver; + struct GBASIOLockstepCoordinator* coordinator = lockstep->coordinator; + bool ret = false; + bool waitOnPlayers = true; + if (coordinator->transferActive) { + mLOG(GBA_SIO, ERROR, "Transfer restarted unexpectedly"); + goto out; + } + struct GBASIOLockstepPlayer* player = TableLookup(&coordinator->players, lockstep->lockstepId); + if (player->playerId != 0) { + mLOG(GBA_SIO, DEBUG, "Secondary player attempted to start transfer"); + goto out; + } + mLOG(GBA_SIO, DEBUG, "Transfer starting at %08X", coordinator->cycle); + memset(coordinator->multiData, 0xFF, sizeof(coordinator->multiData)); + _setData(coordinator, 0, player->driver->d.p); + + int32_t timestamp = GBASIOLockstepTime(player); + struct GBASIOLockstepEvent event = { + .type = SIO_EV_TRANSFER_START, + .timestamp = timestamp, + .finishCycle = timestamp + GBASIOTransferCycles(player->mode, player->driver->d.p->siocnt, coordinator->nAttached - 1), + }; + _enqueueEvent(coordinator, &event, TARGET_SECONDARY); + coordinator->transferActive = true; + if (coordinator->waiting) { + waitOnPlayers = false; + mLOG(GBA_SIO, DEBUG, "Deferring transfer wait while barrier %X is active", coordinator->waiting); + } + if (waitOnPlayers) { + GBASIOLockstepCoordinatorWaitOnPlayers(coordinator, player); + } + ret = true; +out: + return ret; +} + +static void GBASIOLockstepDriverFinishMultiplayer(struct GBASIODriver* driver, uint16_t data[4]) { + struct GBASIOLockstepDriver* lockstep = (struct GBASIOLockstepDriver*) driver; + struct GBASIOLockstepCoordinator* coordinator = lockstep->coordinator; + if (coordinator->transferMode == GBA_SIO_MULTI) { + struct GBASIOLockstepPlayer* player = TableLookup(&coordinator->players, lockstep->lockstepId); + if (!player->dataReceived) { + mLOG(GBA_SIO, WARN, "MULTI did not receive data. Are we running behind?"); + memset(data, 0xFF, sizeof(uint16_t) * 4); + } else { + mLOG(GBA_SIO, INFO, "MULTI transfer finished: %04X %04X %04X %04X", + coordinator->multiData[0], + coordinator->multiData[1], + coordinator->multiData[2], + coordinator->multiData[3]); + memcpy(data, coordinator->multiData, sizeof(uint16_t) * 4); + } + player->dataReceived = false; + if (player->playerId == 0) { + if (coordinator->waiting) { + mLOG(GBA_SIO, DEBUG, "Deferring hard sync while barrier %X is active", coordinator->waiting); + coordinator->nextHardSync = -1; + } else { + _hardSync(coordinator, player); + } + } + } +} + +static uint8_t GBASIOLockstepDriverFinishNormal8(struct GBASIODriver* driver) { + struct GBASIOLockstepDriver* lockstep = (struct GBASIOLockstepDriver*) driver; + struct GBASIOLockstepCoordinator* coordinator = lockstep->coordinator; + uint8_t data = 0xFF; + if (coordinator->transferMode == GBA_SIO_NORMAL_8) { + struct GBASIOLockstepPlayer* player = TableLookup(&coordinator->players, lockstep->lockstepId); + if (player->playerId > 0) { + if (!player->dataReceived) { + mLOG(GBA_SIO, WARN, "NORMAL did not receive data. Are we running behind?"); + } else { + data = coordinator->normalData[player->playerId - 1]; + mLOG(GBA_SIO, INFO, "NORMAL8 transfer finished: %02X", data); + } + } + player->dataReceived = false; + if (player->playerId == 0) { + if (coordinator->waiting) { + mLOG(GBA_SIO, DEBUG, "Deferring hard sync while barrier %X is active", coordinator->waiting); + coordinator->nextHardSync = -1; + } else { + _hardSync(coordinator, player); + } + } + } + return data; +} + +static uint32_t GBASIOLockstepDriverFinishNormal32(struct GBASIODriver* driver) { + struct GBASIOLockstepDriver* lockstep = (struct GBASIOLockstepDriver*) driver; + struct GBASIOLockstepCoordinator* coordinator = lockstep->coordinator; + uint32_t data = 0xFFFFFFFF; + if (coordinator->transferMode == GBA_SIO_NORMAL_32) { + struct GBASIOLockstepPlayer* player = TableLookup(&coordinator->players, lockstep->lockstepId); + if (player->playerId > 0) { + if (!player->dataReceived) { + mLOG(GBA_SIO, WARN, "Did not receive data. Are we running behind?"); + } else { + data = coordinator->normalData[player->playerId - 1]; + mLOG(GBA_SIO, INFO, "NORMAL32 transfer finished: %08X", data); + } + } + player->dataReceived = false; + if (player->playerId == 0) { + if (coordinator->waiting) { + mLOG(GBA_SIO, DEBUG, "Deferring hard sync while barrier %X is active", coordinator->waiting); + coordinator->nextHardSync = -1; + } else { + _hardSync(coordinator, player); + } + } + } + return data; +} + +void GBASIOLockstepCoordinatorInit(struct GBASIOLockstepCoordinator* coordinator) { + memset(coordinator, 0, sizeof(*coordinator)); + TableInit(&coordinator->players, 8, free); +} + +void GBASIOLockstepCoordinatorDeinit(struct GBASIOLockstepCoordinator* coordinator) { + TableDeinit(&coordinator->players); +} + +void GBASIOLockstepCoordinatorAttach(struct GBASIOLockstepCoordinator* coordinator, struct GBASIOLockstepDriver* driver) { + if (driver->coordinator && driver->coordinator != coordinator) { + // TODO + abort(); + } + driver->coordinator = coordinator; +} + +void GBASIOLockstepCoordinatorDetach(struct GBASIOLockstepCoordinator* coordinator, struct GBASIOLockstepDriver* driver) { + if (driver->coordinator != coordinator) { + // TODO + abort(); + return; + } + struct GBASIOLockstepPlayer* player = TableLookup(&coordinator->players, driver->lockstepId); + if (player) { + _removePlayer(coordinator, player); + } + driver->coordinator = NULL; +} + +int32_t _untilNextSync(struct GBASIOLockstepCoordinator* coordinator, struct GBASIOLockstepPlayer* player) { + int32_t cycle = coordinator->cycle - GBASIOLockstepTime(player); + if (player->playerId == 0) { + if (coordinator->nAttached < 2) { + cycle += UNLOCKED_INTERVAL; + } else { + cycle += LOCKSTEP_INTERVAL; + } + } + return cycle; +} + +void _advanceCycle(struct GBASIOLockstepCoordinator* coordinator, struct GBASIOLockstepPlayer* player) { + int32_t newCycle = GBASIOLockstepTime(player); + mASSERT_DEBUG(newCycle - coordinator->cycle >= 0); + coordinator->nextHardSync -= newCycle - coordinator->cycle; + coordinator->cycle = newCycle; +} + +void _removePlayer(struct GBASIOLockstepCoordinator* coordinator, struct GBASIOLockstepPlayer* player) { + struct GBASIOLockstepEvent event = { + .type = SIO_EV_DETACH, + .playerId = player->playerId, + .timestamp = GBASIOLockstepTime(player), + }; + _enqueueEvent(coordinator, &event, TARGET_ALL & ~TARGET(player->playerId)); + + coordinator->waiting = 0; + coordinator->transferActive = false; + + TableRemove(&coordinator->players, player->driver->lockstepId); + _reconfigPlayers(coordinator); + + struct GBASIOLockstepPlayer* runner = TableLookup(&coordinator->players, coordinator->attachedPlayers[0]); + if (runner) { + GBASIOLockstepPlayerWake(runner); + } + _verifyAwake(coordinator); +} + +void _reconfigPlayers(struct GBASIOLockstepCoordinator* coordinator) { + size_t players = TableSize(&coordinator->players); + memset(coordinator->attachedPlayers, 0, sizeof(coordinator->attachedPlayers)); + if (players == 0) { + mLOG(GBA_SIO, WARN, "Reconfiguring player IDs with no players attached somehow?"); + } else if (players == 1) { + struct TableIterator iter; + mASSERT(TableIteratorStart(&coordinator->players, &iter)); + unsigned p0 = TableIteratorGetKey(&coordinator->players, &iter); + coordinator->attachedPlayers[0] = p0; + + struct GBASIOLockstepPlayer* player = TableIteratorGetValue(&coordinator->players, &iter); + coordinator->cycle = mTimingCurrentTime(&player->driver->d.p->p->timing); + coordinator->nextHardSync = HARD_SYNC_INTERVAL; + + if (player->playerId != 0) { + player->playerId = 0; + if (player->driver->user->playerIdChanged) { + player->driver->user->playerIdChanged(player->driver->user, player->playerId); + } + } + + if (!coordinator->transferActive) { + coordinator->transferMode = player->mode; + } + } else { + struct UIntList playerPreferences[MAX_GBAS]; + + int i; + for (i = 0; i < MAX_GBAS; ++i) { + UIntListInit(&playerPreferences[i], 4); + } + + // Collect the first four players' requested player IDs so we can sort through them later + int seen = 0; + struct TableIterator iter; + mASSERT(TableIteratorStart(&coordinator->players, &iter)); + do { + unsigned pid = TableIteratorGetKey(&coordinator->players, &iter); + struct GBASIOLockstepPlayer* player = TableIteratorGetValue(&coordinator->players, &iter); + int requested = MAX_GBAS - 1; + if (player->driver->user->requestedId) { + requested = player->driver->user->requestedId(player->driver->user); + } + if (requested < 0) { + continue; + } + if (requested >= MAX_GBAS) { + requested = MAX_GBAS - 1; + } + + *UIntListAppend(&playerPreferences[requested]) = pid; + ++seen; + } while (TableIteratorNext(&coordinator->players, &iter) && seen < MAX_GBAS); + + // Now sort each requested player ID to figure out who gets which ID + seen = 0; + for (i = 0; i < MAX_GBAS; ++i) { + int j; + for (j = 0; j <= i; ++j) { + while (UIntListSize(&playerPreferences[j]) && seen < MAX_GBAS) { + unsigned pid = *UIntListGetPointer(&playerPreferences[j], 0); + UIntListShift(&playerPreferences[j], 0, 1); + struct GBASIOLockstepPlayer* player = TableLookup(&coordinator->players, pid); + if (!player) { + mLOG(GBA_SIO, ERROR, "Player list appears to have changed unexpectedly. PID %u missing.", pid); + continue; + } + coordinator->attachedPlayers[seen] = pid; + if (player->playerId != seen) { + player->playerId = seen; + if (player->driver->user->playerIdChanged) { + player->driver->user->playerIdChanged(player->driver->user, player->playerId); + } + } + ++seen; + } + } + } + + for (i = 0; i < MAX_GBAS; ++i) { + UIntListDeinit(&playerPreferences[i]); + } + } + + int nAttached = 0; + size_t i; + for (i = 0; i < MAX_GBAS; ++i) { + unsigned pid = coordinator->attachedPlayers[i]; + if (!pid) { + continue; + } + struct GBASIOLockstepPlayer* player = TableLookup(&coordinator->players, pid); + if (!player) { + coordinator->attachedPlayers[i] = 0; + } else { + ++nAttached; + } + } + coordinator->nAttached = nAttached; +} + +static void _setData(struct GBASIOLockstepCoordinator* coordinator, uint32_t id, struct GBASIO* sio) { + switch (coordinator->transferMode) { + case GBA_SIO_MULTI: + coordinator->multiData[id] = sio->p->memory.io[GBA_REG(SIOMLT_SEND)]; + break; + case GBA_SIO_NORMAL_8: + coordinator->normalData[id] = sio->p->memory.io[GBA_REG(SIODATA8)]; + break; + case GBA_SIO_NORMAL_32: + coordinator->normalData[id] = sio->p->memory.io[GBA_REG(SIODATA32_LO)]; + coordinator->normalData[id] |= sio->p->memory.io[GBA_REG(SIODATA32_HI)] << 16; + break; + case GBA_SIO_UART: + case GBA_SIO_GPIO: + case GBA_SIO_JOYBUS: + mLOG(GBA_SIO, ERROR, "Unsupported mode %i in lockstep", coordinator->transferMode); + // TODO: Should we handle this or just abort? + break; + } +} + +void _setReady(struct GBASIOLockstepCoordinator* coordinator, struct GBASIOLockstepPlayer* activePlayer, int playerId, enum GBASIOMode mode) { + activePlayer->otherModes[playerId] = mode; + bool ready = true; + int i; + for (i = 0; ready && i < coordinator->nAttached; ++i) { + ready = activePlayer->otherModes[i] == activePlayer->mode; + } + if (activePlayer->mode == GBA_SIO_MULTI) { + struct GBASIO* sio = activePlayer->driver->d.p; + sio->siocnt = GBASIOMultiplayerSetReady(sio->siocnt, ready); + sio->rcnt = GBASIORegisterRCNTSetSd(sio->rcnt, ready); + } +} + +void _hardSync(struct GBASIOLockstepCoordinator* coordinator, struct GBASIOLockstepPlayer* player) { + mASSERT_DEBUG(player->playerId == 0); + struct GBASIOLockstepEvent event = { + .type = SIO_EV_HARD_SYNC, + .playerId = 0, + .timestamp = GBASIOLockstepTime(player), + }; + _enqueueEvent(coordinator, &event, TARGET_SECONDARY); + GBASIOLockstepCoordinatorWaitOnPlayers(coordinator, player); +} + +void _enqueueEvent(struct GBASIOLockstepCoordinator* coordinator, const struct GBASIOLockstepEvent* event, uint32_t target) { + mLOG(GBA_SIO, DEBUG, "Enqueuing event of type %X from %i for target %X at timestamp %X", + event->type, event->playerId, target, event->timestamp); + + int i; + for (i = 0; i < coordinator->nAttached; ++i) { + if (!(target & TARGET(i))) { + continue; + } + struct GBASIOLockstepPlayer* player = TableLookup(&coordinator->players, coordinator->attachedPlayers[i]); + mASSERT_LOG(GBA_SIO, player->freeList, "No free events"); + struct GBASIOLockstepEvent* newEvent = player->freeList; + player->freeList = newEvent->next; + + memcpy(newEvent, event, sizeof(*event)); + struct GBASIOLockstepEvent** previous = &player->queue; + struct GBASIOLockstepEvent* next = player->queue; + while (next) { + int32_t until = newEvent->timestamp - next->timestamp; + if (until < 0) { + break; + } + previous = &next->next; + next = next->next; + } + newEvent->next = next; + *previous = newEvent; + } +} + +void _lockstepEvent(struct mTiming* timing, void* context, uint32_t cyclesLate) { + struct GBASIOLockstepDriver* lockstep = context; + struct GBASIOLockstepCoordinator* coordinator = lockstep->coordinator; + struct GBASIOLockstepPlayer* player = TableLookup(&coordinator->players, lockstep->lockstepId); + struct GBASIO* sio = player->driver->d.p; + mASSERT(player->playerId >= 0 && player->playerId < 4); + + bool wasDetach = false; + if (player->queue && player->queue->type == SIO_EV_DETACH) { + mLOG(GBA_SIO, DEBUG, "Player %i detached at timestamp %X, picking up the pieces", + player->queue->playerId, player->queue->timestamp); + wasDetach = true; + } + if (player->playerId == 0 && GBASIOLockstepTime(player) - coordinator->cycle >= 0) { + // We are the clock owner; advance the shared clock. However, if we just became + // the clock owner (by the previous one disconnecting) we might be slightly + // behind the shared clock. We should wait a bit if needed in that case. + _advanceCycle(coordinator, player); + if (!coordinator->transferActive) { + GBASIOLockstepCoordinatorWakePlayers(coordinator); + } + if (coordinator->nextHardSync < 0) { + if (!coordinator->waiting) { + _hardSync(coordinator, player); + } + coordinator->nextHardSync += HARD_SYNC_INTERVAL; + } + } + + int32_t nextEvent = _untilNextSync(coordinator, player); + while (true) { + struct GBASIOLockstepEvent* event = player->queue; + if (!event) { + break; + } + if (event->timestamp > GBASIOLockstepTime(player)) { + break; + } + player->queue = event->next; + struct GBASIOLockstepEvent reply = { + .playerId = player->playerId, + .timestamp = GBASIOLockstepTime(player), + }; + mLOG(GBA_SIO, DEBUG, "Got event of type %X from %i at timestamp %X", + event->type, event->playerId, event->timestamp); + switch (event->type) { + case SIO_EV_ATTACH: + _setReady(coordinator, player, event->playerId, -1); + if (player->playerId == 0) { + struct GBASIO* sio = player->driver->d.p; + sio->siocnt = GBASIOMultiplayerClearSlave(sio->siocnt); + } + reply.mode = player->mode; + reply.type = SIO_EV_MODE_SET; + _enqueueEvent(coordinator, &reply, TARGET(event->playerId)); + break; + case SIO_EV_HARD_SYNC: + GBASIOLockstepCoordinatorAckPlayer(coordinator, player); + break; + case SIO_EV_TRANSFER_START: + _setData(coordinator, player->playerId, sio); + nextEvent = event->finishCycle - GBASIOLockstepTime(player) - cyclesLate; + player->driver->d.p->siocnt |= 0x80; + mTimingDeschedule(&sio->p->timing, &sio->completeEvent); + mTimingSchedule(&sio->p->timing, &sio->completeEvent, nextEvent); + GBASIOLockstepCoordinatorAckPlayer(coordinator, player); + break; + case SIO_EV_MODE_SET: + _setReady(coordinator, player, event->playerId, event->mode); + if (event->playerId == 0) { + GBASIOLockstepCoordinatorAckPlayer(coordinator, player); + } + break; + case SIO_EV_DETACH: + _setReady(coordinator, player, event->playerId, -1); + _setReady(coordinator, player, player->playerId, player->mode); + reply.mode = player->mode; + reply.type = SIO_EV_MODE_SET; + _enqueueEvent(coordinator, &reply, ~TARGET(event->playerId)); + if (player->mode == GBA_SIO_MULTI) { + sio->siocnt = GBASIOMultiplayerSetId(sio->siocnt, player->playerId); + sio->siocnt = GBASIOMultiplayerSetSlave(sio->siocnt, player->playerId || coordinator->nAttached < 2); + } + wasDetach = true; + break; + } + event->next = player->freeList; + player->freeList = event; + } + if (player->queue && player->queue->timestamp - GBASIOLockstepTime(player) < nextEvent) { + nextEvent = player->queue->timestamp - GBASIOLockstepTime(player); + } + + if (player->playerId != 0 && nextEvent <= LOCKSTEP_INTERVAL) { + if (!player->queue || wasDetach) { + GBASIOLockstepPlayerSleep(player); + // XXX: Is there a better way to gain sync lock at the beginning? + if (nextEvent < 4) { + nextEvent = 4; + } + _verifyAwake(coordinator); + } + } + + mASSERT_DEBUG(nextEvent > 0); + mTimingSchedule(timing, &lockstep->event, nextEvent); +} + +int32_t GBASIOLockstepTime(struct GBASIOLockstepPlayer* player) { + return mTimingCurrentTime(&player->driver->d.p->p->timing) - player->cycleOffset; +} + +void GBASIOLockstepCoordinatorWaitOnPlayers(struct GBASIOLockstepCoordinator* coordinator, struct GBASIOLockstepPlayer* player) { + mASSERT(!coordinator->waiting); + mASSERT(!player->asleep); + mASSERT(player->playerId == 0); + if (coordinator->nAttached < 2) { + return; + } + + _advanceCycle(coordinator, player); + mLOG(GBA_SIO, DEBUG, "Primary waiting for players to ack"); + coordinator->waiting = ((1 << coordinator->nAttached) - 1) & ~TARGET(player->playerId); + GBASIOLockstepPlayerSleep(player); + GBASIOLockstepCoordinatorWakePlayers(coordinator); + + _verifyAwake(coordinator); +} + +void GBASIOLockstepCoordinatorWakePlayers(struct GBASIOLockstepCoordinator* coordinator) { + int i; + for (i = 1; i < coordinator->nAttached; ++i) { + if (!coordinator->attachedPlayers[i]) { + continue; + } + struct GBASIOLockstepPlayer* player = TableLookup(&coordinator->players, coordinator->attachedPlayers[i]); + GBASIOLockstepPlayerWake(player); + } +} + +void GBASIOLockstepPlayerWake(struct GBASIOLockstepPlayer* player) { + if (!player->asleep) { + return; + } + player->asleep = false; + if (player->driver->user && player->driver->user->wake) { + player->driver->user->wake(player->driver->user); + } +} + +void GBASIOLockstepCoordinatorAckPlayer(struct GBASIOLockstepCoordinator* coordinator, struct GBASIOLockstepPlayer* player) { + if (player->playerId == 0) { + return; + } + if (!(coordinator->waiting & TARGET(player->playerId))) { + mLOG(GBA_SIO, DEBUG, "Ignoring stray ack from player %d with no pending wait bit", player->playerId); + return; + } + coordinator->waiting &= ~TARGET(player->playerId); + if (!coordinator->waiting) { + mLOG(GBA_SIO, DEBUG, "All players acked, waking primary"); + if (coordinator->transferActive) { + int i; + for (i = 0; i < coordinator->nAttached; ++i) { + if (!coordinator->attachedPlayers[i]) { + continue; + } + struct GBASIOLockstepPlayer* player = TableLookup(&coordinator->players, coordinator->attachedPlayers[i]); + player->dataReceived = true; + } + + coordinator->transferActive = false; + } + + struct GBASIOLockstepPlayer* runner = TableLookup(&coordinator->players, coordinator->attachedPlayers[0]); + GBASIOLockstepPlayerWake(runner); + } + GBASIOLockstepPlayerSleep(player); +} + +void GBASIOLockstepPlayerSleep(struct GBASIOLockstepPlayer* player) { + if (player->asleep) { + return; + } + player->asleep = true; + if (player->driver->user && player->driver->user->sleep) { + player->driver->user->sleep(player->driver->user); + } + player->driver->d.p->p->cpu->nextEvent = 0; + player->driver->d.p->p->earlyExit = true; +} + +size_t GBASIOLockstepCoordinatorAttached(struct GBASIOLockstepCoordinator* coordinator) { + return TableSize(&coordinator->players); +} diff --git a/src/platform/libretro/libretro_multiplayer.c b/src/platform/libretro/libretro_multiplayer.c new file mode 100644 index 00000000000..bb35727d4f6 --- /dev/null +++ b/src/platform/libretro/libretro_multiplayer.c @@ -0,0 +1,434 @@ +#include "libretro_multiplayer.h" + +#include +#include +#include +#include +#include + +#include +#include + +#define VIDEO_BYTES_PER_PIXEL sizeof(mColor) +#define LOCKSTEP_PUMP_WATCHDOG 2000000 + +static void _stepRunnerCore(struct mCore* core) { + if (!core) { + return; + } + if (core->runLoop) { + core->runLoop(core); + return; + } + if (core->step) { + core->step(core); + return; + } + if (core->runFrame) { + core->runFrame(core); + } +} + +static void _lockstepSleep(struct mLockstepUser* user) { + struct mLibretroLockstepUser* lockstepUser = (struct mLibretroLockstepUser*) user; + lockstepUser->blocked = true; + + if (!lockstepUser->multiplayer || !lockstepUser->multiplayer->active || !lockstepUser->multiplayer->secondaryCore || !lockstepUser->multiplayer->primaryCore) { + return; + } + + if (lockstepUser->stepping || lockstepUser->multiplayer->pumping) { + return; + } + + struct mCore* runner = lockstepUser->playerIndex == 0 ? + lockstepUser->multiplayer->secondaryCore : + lockstepUser->multiplayer->primaryCore; + unsigned runnerIndex = lockstepUser->playerIndex == 0 ? 1 : 0; + if (!runner || (!runner->step && !runner->runLoop && !runner->runFrame)) { + return; + } + + lockstepUser->multiplayer->pumping = true; + lockstepUser->stepping = true; + int watchdog = LOCKSTEP_PUMP_WATCHDOG; + while (lockstepUser->blocked && watchdog-- > 0) { + if (!lockstepUser->multiplayer->active) { + break; + } + lockstepUser->multiplayer->pumpedThisFrame[runnerIndex] = true; + _stepRunnerCore(runner); + } + lockstepUser->stepping = false; + lockstepUser->multiplayer->pumping = false; + mASSERT_LOG(GBA_SIO, !lockstepUser->blocked, "Lockstep single-thread watchdog expired while waiting for peer wake"); +} + +static void _lockstepWake(struct mLockstepUser* user) { + struct mLibretroLockstepUser* lockstepUser = (struct mLibretroLockstepUser*) user; + lockstepUser->blocked = false; +} + +static int _requestedId(struct mLockstepUser* user) { + struct mLibretroLockstepUser* lockstepUser = (struct mLibretroLockstepUser*) user; + return lockstepUser->requestedId; +} + +static enum mLibretroSplitscreenMode _parseMode(retro_environment_t environCallback) { + struct retro_variable var = { + .key = "mgba_multiplayer_splitscreen", + .value = 0, + }; + + if (!environCallback || !environCallback(RETRO_ENVIRONMENT_GET_VARIABLE, &var) || !var.value) { + return mLIBRETRO_SPLITSCREEN_OFF; + } + + if (strcmp(var.value, "Side by Side") == 0) { + return mLIBRETRO_SPLITSCREEN_2P_VERTICAL; + } + + if (strcmp(var.value, "Top/Bottom") == 0) { + return mLIBRETRO_SPLITSCREEN_2P_HORIZONTAL; + } + + return mLIBRETRO_SPLITSCREEN_OFF; +} + +static size_t _compositePixels(unsigned maxVideoWidth, unsigned maxVideoHeight) { + return (size_t) maxVideoWidth * 2 * maxVideoHeight * 2; +} + +static void _clearPrimaryLinkPeripheral(struct mCore* primaryCore) { + if (!primaryCore || primaryCore->platform(primaryCore) != mPLATFORM_GBA) { + return; + } + primaryCore->setPeripheral(primaryCore, mPERIPH_GBA_LINK_PORT, NULL); +} + +static void _detachLockstep(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore) { + if (!multiplayer->coordinatorInitialized) { + return; + } + + if (primaryCore && primaryCore->platform(primaryCore) == mPLATFORM_GBA) { + GBASIOLockstepCoordinatorDetach(&multiplayer->coordinator, &multiplayer->drivers[0]); + } + + if (multiplayer->secondaryCore && multiplayer->secondaryCore->platform(multiplayer->secondaryCore) == mPLATFORM_GBA) { + multiplayer->secondaryCore->setPeripheral(multiplayer->secondaryCore, mPERIPH_GBA_LINK_PORT, NULL); + GBASIOLockstepCoordinatorDetach(&multiplayer->coordinator, &multiplayer->drivers[1]); + } + + GBASIOLockstepCoordinatorDeinit(&multiplayer->coordinator); + multiplayer->coordinatorInitialized = false; +} + +static void _destroySecondaryCore(struct mLibretroMultiplayer* multiplayer) { + if (!multiplayer->secondaryCore) { + return; + } + + mCoreConfigDeinit(&multiplayer->secondaryCore->config); + multiplayer->secondaryCore->deinit(multiplayer->secondaryCore); + multiplayer->secondaryCore = NULL; +} + +static void _destroyBuffers(struct mLibretroMultiplayer* multiplayer) { + if (multiplayer->secondaryOutputBuffer) { + free(multiplayer->secondaryOutputBuffer); + multiplayer->secondaryOutputBuffer = NULL; + } + + if (multiplayer->compositeBuffer) { + free(multiplayer->compositeBuffer); + multiplayer->compositeBuffer = NULL; + multiplayer->compositeBufferPixels = 0; + } +} + +static void _destroySecondaryRom(struct mLibretroMultiplayer* multiplayer) { + if (multiplayer->secondaryRomData) { + mappedMemoryFree(multiplayer->secondaryRomData, multiplayer->secondaryRomSize); + multiplayer->secondaryRomData = NULL; + multiplayer->secondaryRomSize = 0; + } +} + +static void _stopSession(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore) { + _clearPrimaryLinkPeripheral(primaryCore); + _detachLockstep(multiplayer, primaryCore); + _destroySecondaryCore(multiplayer); + _destroyBuffers(multiplayer); + _destroySecondaryRom(multiplayer); + multiplayer->primaryCore = NULL; + multiplayer->active = false; +} + +static bool _initSecondaryCore(struct mLibretroMultiplayer* multiplayer, const void* romData, size_t romSize, const char* romPath) { + struct VFile* rom; + + if (romData && romSize) { + multiplayer->secondaryRomData = anonymousMemoryMap(romSize); + if (!multiplayer->secondaryRomData) { + return false; + } + multiplayer->secondaryRomSize = romSize; + memcpy(multiplayer->secondaryRomData, romData, romSize); + + rom = VFileFromMemory(multiplayer->secondaryRomData, romSize); + if (!rom) { + return false; + } + } else if (romPath && *romPath) { + rom = VFileOpen(romPath, O_RDONLY); + if (!rom) { + return false; + } + } else { + return false; + } + + multiplayer->secondaryCore = mCoreFindVF(rom); + if (!multiplayer->secondaryCore) { + rom->close(rom); + return false; + } + + mCoreInitConfig(multiplayer->secondaryCore, NULL); + multiplayer->secondaryCore->init(multiplayer->secondaryCore); + + multiplayer->secondaryOutputBuffer = malloc((size_t) multiplayer->maxVideoWidth * multiplayer->maxVideoHeight * VIDEO_BYTES_PER_PIXEL); + if (!multiplayer->secondaryOutputBuffer) { + rom->close(rom); + return false; + } + memset(multiplayer->secondaryOutputBuffer, 0xFF, (size_t) multiplayer->maxVideoWidth * multiplayer->maxVideoHeight * VIDEO_BYTES_PER_PIXEL); + multiplayer->secondaryCore->setVideoBuffer(multiplayer->secondaryCore, multiplayer->secondaryOutputBuffer, multiplayer->maxVideoWidth); + + memset(&multiplayer->secondaryStream, 0, sizeof(multiplayer->secondaryStream)); + multiplayer->secondaryCore->setAVStream(multiplayer->secondaryCore, &multiplayer->secondaryStream); + + if (!multiplayer->secondaryCore->loadROM(multiplayer->secondaryCore, rom)) { + rom->close(rom); + return false; + } + + multiplayer->secondaryCore->reset(multiplayer->secondaryCore); + return true; +} + +static bool _attachLockstep(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore) { + if (!primaryCore || primaryCore->platform(primaryCore) != mPLATFORM_GBA) { + return false; + } + if (!multiplayer->secondaryCore || multiplayer->secondaryCore->platform(multiplayer->secondaryCore) != mPLATFORM_GBA) { + return false; + } + + GBASIOLockstepCoordinatorInit(&multiplayer->coordinator); + multiplayer->coordinatorInitialized = true; + + multiplayer->users[0].d.sleep = _lockstepSleep; + multiplayer->users[0].d.wake = _lockstepWake; + multiplayer->users[0].d.requestedId = _requestedId; + multiplayer->users[0].d.playerIdChanged = NULL; + multiplayer->users[0].requestedId = 0; + multiplayer->users[0].multiplayer = multiplayer; + multiplayer->users[0].playerIndex = 0; + multiplayer->users[0].blocked = false; + multiplayer->users[0].stepping = false; + + multiplayer->users[1].d.sleep = _lockstepSleep; + multiplayer->users[1].d.wake = _lockstepWake; + multiplayer->users[1].d.requestedId = _requestedId; + multiplayer->users[1].d.playerIdChanged = NULL; + multiplayer->users[1].requestedId = 1; + multiplayer->users[1].multiplayer = multiplayer; + multiplayer->users[1].playerIndex = 1; + multiplayer->users[1].blocked = false; + multiplayer->users[1].stepping = false; + + GBASIOLockstepDriverCreate(&multiplayer->drivers[0], &multiplayer->users[0].d); + GBASIOLockstepDriverCreate(&multiplayer->drivers[1], &multiplayer->users[1].d); + + GBASIOLockstepCoordinatorAttach(&multiplayer->coordinator, &multiplayer->drivers[0]); + GBASIOLockstepCoordinatorAttach(&multiplayer->coordinator, &multiplayer->drivers[1]); + + primaryCore->setPeripheral(primaryCore, mPERIPH_GBA_LINK_PORT, &multiplayer->drivers[0].d); + multiplayer->secondaryCore->setPeripheral(multiplayer->secondaryCore, mPERIPH_GBA_LINK_PORT, &multiplayer->drivers[1].d); + return true; +} + +static bool _startSession(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore, const void* romData, size_t romSize, const char* romPath) { + if (!primaryCore || primaryCore->platform(primaryCore) != mPLATFORM_GBA || !romData || !romSize) { + if (!(romPath && *romPath)) { + return false; + } + } + + if (!_initSecondaryCore(multiplayer, romData, romSize, romPath)) { + _stopSession(multiplayer, primaryCore); + return false; + } + + multiplayer->compositeBufferPixels = _compositePixels(multiplayer->maxVideoWidth, multiplayer->maxVideoHeight); + multiplayer->compositeBuffer = malloc(multiplayer->compositeBufferPixels * VIDEO_BYTES_PER_PIXEL); + if (!multiplayer->compositeBuffer) { + _stopSession(multiplayer, primaryCore); + return false; + } + memset(multiplayer->compositeBuffer, 0xFF, multiplayer->compositeBufferPixels * VIDEO_BYTES_PER_PIXEL); + + if (!_attachLockstep(multiplayer, primaryCore)) { + _stopSession(multiplayer, primaryCore); + return false; + } + multiplayer->primaryCore = primaryCore; + + multiplayer->active = true; + return true; +} + +void mLibretroMultiplayerInit(struct mLibretroMultiplayer* multiplayer, unsigned maxVideoWidth, unsigned maxVideoHeight) { + memset(multiplayer, 0, sizeof(*multiplayer)); + multiplayer->maxVideoWidth = maxVideoWidth; + multiplayer->maxVideoHeight = maxVideoHeight; + multiplayer->mode = mLIBRETRO_SPLITSCREEN_OFF; + multiplayer->primaryCore = NULL; +} + +void mLibretroMultiplayerDeinit(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore) { + _stopSession(multiplayer, primaryCore); + multiplayer->mode = mLIBRETRO_SPLITSCREEN_OFF; +} + +void mLibretroMultiplayerUpdateMode(struct mLibretroMultiplayer* multiplayer, retro_environment_t environCallback) { + multiplayer->mode = _parseMode(environCallback); +} + +bool mLibretroMultiplayerApplyMode(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore, const void* romData, size_t romSize, const char* romPath, retro_log_printf_t logCallback) { + if (multiplayer->mode == mLIBRETRO_SPLITSCREEN_OFF) { + if (multiplayer->active) { + _stopSession(multiplayer, primaryCore); + } + return true; + } + + if (multiplayer->active) { + return true; + } + + if (!_startSession(multiplayer, primaryCore, romData, romSize, romPath)) { + if (logCallback) { + logCallback(RETRO_LOG_WARN, "libretro: failed to start multiplayer splitscreen session; continuing in single-player mode\n"); + } + multiplayer->mode = mLIBRETRO_SPLITSCREEN_OFF; + return false; + } + + if (logCallback) { + logCallback(RETRO_LOG_INFO, "libretro: started 2-player splitscreen multiplayer session\n"); + } + + return true; +} + +void mLibretroMultiplayerSetKeys(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore, uint16_t player1Keys, uint16_t player2Keys) { + if (!primaryCore) { + return; + } + + primaryCore->setKeys(primaryCore, player1Keys); + + if (!multiplayer->active || !multiplayer->secondaryCore) { + return; + } + + multiplayer->secondaryCore->setKeys(multiplayer->secondaryCore, player2Keys); +} + +void mLibretroMultiplayerRunFrame(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore) { + if (!primaryCore) { + return; + } + multiplayer->primaryCore = primaryCore; + multiplayer->pumpedThisFrame[0] = false; + multiplayer->pumpedThisFrame[1] = false; + + primaryCore->runFrame(primaryCore); + + if (multiplayer->active && multiplayer->secondaryCore && !multiplayer->pumpedThisFrame[1]) { + multiplayer->secondaryCore->runFrame(multiplayer->secondaryCore); + } +} + +void mLibretroMultiplayerAdjustGeometry(const struct mLibretroMultiplayer* multiplayer, unsigned* baseWidth, unsigned* baseHeight, unsigned* maxWidth, unsigned* maxHeight, float* aspectRatio) { + if (!multiplayer->active) { + return; + } + + if (multiplayer->mode == mLIBRETRO_SPLITSCREEN_2P_VERTICAL) { + *baseWidth *= 2; + *maxWidth *= 2; + *aspectRatio *= 2.0; + return; + } + + if (multiplayer->mode == mLIBRETRO_SPLITSCREEN_2P_HORIZONTAL) { + *baseHeight *= 2; + *maxHeight *= 2; + *aspectRatio *= 0.5; + } +} + +const mColor* mLibretroMultiplayerComposeFrame(struct mLibretroMultiplayer* multiplayer, const mColor* primaryFrame, unsigned primaryWidth, unsigned primaryHeight, size_t* outPitch, unsigned* outWidth, unsigned* outHeight) { + if (!multiplayer->active || !multiplayer->secondaryCore || !multiplayer->compositeBuffer || !multiplayer->secondaryOutputBuffer) { + *outPitch = (size_t) primaryWidth * VIDEO_BYTES_PER_PIXEL; + *outWidth = primaryWidth; + *outHeight = primaryHeight; + return primaryFrame; + } + + unsigned secondaryWidth, secondaryHeight; + multiplayer->secondaryCore->currentVideoSize(multiplayer->secondaryCore, &secondaryWidth, &secondaryHeight); + + if (multiplayer->mode == mLIBRETRO_SPLITSCREEN_2P_VERTICAL) { + size_t y; + *outWidth = primaryWidth + secondaryWidth; + *outHeight = primaryHeight > secondaryHeight ? primaryHeight : secondaryHeight; + *outPitch = (size_t) *outWidth * VIDEO_BYTES_PER_PIXEL; + memset(multiplayer->compositeBuffer, 0xFF, (size_t) *outWidth * *outHeight * VIDEO_BYTES_PER_PIXEL); + for (y = 0; y < primaryHeight; ++y) { + mColor* row = &multiplayer->compositeBuffer[y * (*outWidth)]; + memcpy(row, &primaryFrame[y * multiplayer->maxVideoWidth], (size_t) primaryWidth * VIDEO_BYTES_PER_PIXEL); + } + for (y = 0; y < secondaryHeight; ++y) { + mColor* row = &multiplayer->compositeBuffer[y * (*outWidth) + primaryWidth]; + memcpy(row, &multiplayer->secondaryOutputBuffer[y * multiplayer->maxVideoWidth], (size_t) secondaryWidth * VIDEO_BYTES_PER_PIXEL); + } + return multiplayer->compositeBuffer; + } + + if (multiplayer->mode == mLIBRETRO_SPLITSCREEN_2P_HORIZONTAL) { + size_t y; + *outWidth = primaryWidth > secondaryWidth ? primaryWidth : secondaryWidth; + *outHeight = primaryHeight + secondaryHeight; + *outPitch = (size_t) *outWidth * VIDEO_BYTES_PER_PIXEL; + memset(multiplayer->compositeBuffer, 0xFF, (size_t) *outWidth * *outHeight * VIDEO_BYTES_PER_PIXEL); + for (y = 0; y < primaryHeight; ++y) { + mColor* row = &multiplayer->compositeBuffer[y * (*outWidth)]; + memcpy(row, &primaryFrame[y * multiplayer->maxVideoWidth], (size_t) primaryWidth * VIDEO_BYTES_PER_PIXEL); + } + for (y = 0; y < secondaryHeight; ++y) { + mColor* row = &multiplayer->compositeBuffer[(y + primaryHeight) * (*outWidth)]; + memcpy(row, &multiplayer->secondaryOutputBuffer[y * multiplayer->maxVideoWidth], (size_t) secondaryWidth * VIDEO_BYTES_PER_PIXEL); + } + return multiplayer->compositeBuffer; + } + + *outPitch = (size_t) primaryWidth * VIDEO_BYTES_PER_PIXEL; + *outWidth = primaryWidth; + *outHeight = primaryHeight; + return primaryFrame; +} diff --git a/src/platform/libretro/libretro_multiplayer.h b/src/platform/libretro/libretro_multiplayer.h new file mode 100644 index 00000000000..5ee5cc2522a --- /dev/null +++ b/src/platform/libretro/libretro_multiplayer.h @@ -0,0 +1,65 @@ +#ifndef MGBA_LIBRETRO_MULTIPLAYER_H +#define MGBA_LIBRETRO_MULTIPLAYER_H + +#include + +#include +#include +#include + +#include "libretro.h" + +CXX_GUARD_START + +enum mLibretroSplitscreenMode { + mLIBRETRO_SPLITSCREEN_OFF = 0, + mLIBRETRO_SPLITSCREEN_2P_VERTICAL, + mLIBRETRO_SPLITSCREEN_2P_HORIZONTAL, +}; + +struct mLibretroLockstepUser { + struct mLockstepUser d; + int requestedId; + struct mLibretroMultiplayer* multiplayer; + unsigned playerIndex; + bool blocked; + bool stepping; +}; + +struct mLibretroMultiplayer { + enum mLibretroSplitscreenMode mode; + bool active; + unsigned maxVideoWidth; + unsigned maxVideoHeight; + struct mCore* primaryCore; + + struct mCore* secondaryCore; + mColor* secondaryOutputBuffer; + mColor* compositeBuffer; + size_t compositeBufferPixels; + + void* secondaryRomData; + size_t secondaryRomSize; + + struct mAVStream secondaryStream; + bool pumping; + bool pumpedThisFrame[2]; + + struct mLibretroLockstepUser users[2]; + struct GBASIOLockstepCoordinator coordinator; + struct GBASIOLockstepDriver drivers[2]; + bool coordinatorInitialized; +}; + +void mLibretroMultiplayerInit(struct mLibretroMultiplayer* multiplayer, unsigned maxVideoWidth, unsigned maxVideoHeight); +void mLibretroMultiplayerDeinit(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore); +void mLibretroMultiplayerUpdateMode(struct mLibretroMultiplayer* multiplayer, retro_environment_t environCallback); +bool mLibretroMultiplayerApplyMode(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore, const void* romData, size_t romSize, const char* romPath, retro_log_printf_t logCallback); +void mLibretroMultiplayerSetKeys(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore, uint16_t player1Keys, uint16_t player2Keys); +void mLibretroMultiplayerRunFrame(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore); +void mLibretroMultiplayerAdjustGeometry(const struct mLibretroMultiplayer* multiplayer, unsigned* baseWidth, unsigned* baseHeight, unsigned* maxWidth, unsigned* maxHeight, float* aspectRatio); +const mColor* mLibretroMultiplayerComposeFrame(struct mLibretroMultiplayer* multiplayer, const mColor* primaryFrame, unsigned primaryWidth, unsigned primaryHeight, size_t* outPitch, unsigned* outWidth, unsigned* outHeight); + +CXX_GUARD_END + +#endif From 989c44d7d6487073fb876f423965371c394434d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Tue, 24 Feb 2026 11:08:52 +0300 Subject: [PATCH 02/11] libretro: reset to reset all cores --- src/platform/libretro/libretro.c | 2 +- src/platform/libretro/libretro_multiplayer.c | 13 +++++++++++++ src/platform/libretro/libretro_multiplayer.h | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/platform/libretro/libretro.c b/src/platform/libretro/libretro.c index 3c58c543eb4..1ee2e164c00 100644 --- a/src/platform/libretro/libretro.c +++ b/src/platform/libretro/libretro.c @@ -1917,7 +1917,7 @@ static void _setupMaps(struct mCore* core) { } void retro_reset(void) { - core->reset(core); + mLibretroMultiplayerReset(&multiplayer, core); mRumbleIntegratorReset(&rumble); _setupMaps(core); } diff --git a/src/platform/libretro/libretro_multiplayer.c b/src/platform/libretro/libretro_multiplayer.c index bb35727d4f6..7c5a8e6ecdd 100644 --- a/src/platform/libretro/libretro_multiplayer.c +++ b/src/platform/libretro/libretro_multiplayer.c @@ -334,6 +334,19 @@ bool mLibretroMultiplayerApplyMode(struct mLibretroMultiplayer* multiplayer, str return true; } +void mLibretroMultiplayerReset(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore) { + if (!primaryCore) { + return; + } + multiplayer->primaryCore = primaryCore; + + if (multiplayer->active && multiplayer->secondaryCore) { + multiplayer->secondaryCore->reset(multiplayer->secondaryCore); + } + + primaryCore->reset(primaryCore); +} + void mLibretroMultiplayerSetKeys(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore, uint16_t player1Keys, uint16_t player2Keys) { if (!primaryCore) { return; diff --git a/src/platform/libretro/libretro_multiplayer.h b/src/platform/libretro/libretro_multiplayer.h index 5ee5cc2522a..4e3b04bd545 100644 --- a/src/platform/libretro/libretro_multiplayer.h +++ b/src/platform/libretro/libretro_multiplayer.h @@ -55,6 +55,7 @@ void mLibretroMultiplayerInit(struct mLibretroMultiplayer* multiplayer, unsigned void mLibretroMultiplayerDeinit(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore); void mLibretroMultiplayerUpdateMode(struct mLibretroMultiplayer* multiplayer, retro_environment_t environCallback); bool mLibretroMultiplayerApplyMode(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore, const void* romData, size_t romSize, const char* romPath, retro_log_printf_t logCallback); +void mLibretroMultiplayerReset(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore); void mLibretroMultiplayerSetKeys(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore, uint16_t player1Keys, uint16_t player2Keys); void mLibretroMultiplayerRunFrame(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore); void mLibretroMultiplayerAdjustGeometry(const struct mLibretroMultiplayer* multiplayer, unsigned* baseWidth, unsigned* baseHeight, unsigned* maxWidth, unsigned* maxHeight, float* aspectRatio); From 984f51f75bb1232c1c5fa635c382909e999d7087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Tue, 24 Feb 2026 18:48:21 +0300 Subject: [PATCH 03/11] libretro: simplify multiplayer function signatures --- src/platform/libretro/libretro.c | 31 +- src/platform/libretro/libretro_multiplayer.c | 317 ++++++++++--------- src/platform/libretro/libretro_multiplayer.h | 17 +- 3 files changed, 192 insertions(+), 173 deletions(-) diff --git a/src/platform/libretro/libretro.c b/src/platform/libretro/libretro.c index 1ee2e164c00..2ca2dc832f7 100644 --- a/src/platform/libretro/libretro.c +++ b/src/platform/libretro/libretro.c @@ -1356,12 +1356,7 @@ void retro_get_system_av_info(struct retro_system_av_info* info) { info->geometry.max_height = height; info->geometry.aspect_ratio = width / (double) height; - mLibretroMultiplayerAdjustGeometry(&multiplayer, - &info->geometry.base_width, - &info->geometry.base_height, - &info->geometry.max_width, - &info->geometry.max_height, - &info->geometry.aspect_ratio); + mLibretroMultiplayerAdjustGeometry(&info->geometry); info->timing.fps = core->frequency(core) / (float) core->frameCycles(core); #ifdef M_CORE_GBA @@ -1477,7 +1472,7 @@ void retro_init(void) { } void retro_deinit(void) { - mLibretroMultiplayerDeinit(&multiplayer, core); + mLibretroMultiplayerDeinit(); if (outputBuffer) { #ifdef _3DS @@ -1550,8 +1545,8 @@ void retro_run(void) { #if defined(COLOR_16_BIT) && defined(COLOR_5_6_5) _loadPostProcessingSettings(); #endif - mLibretroMultiplayerUpdateMode(&multiplayer, environCallback); - mLibretroMultiplayerApplyMode(&multiplayer, core, data, dataSize, loadedRomPath, logCallback); + mLibretroMultiplayerUpdateMode(environCallback); + mLibretroMultiplayerApplyMode(data, dataSize, loadedRomPath, logCallback); #ifdef M_CORE_GB _updateGbPal(); #endif @@ -1559,7 +1554,7 @@ void retro_run(void) { player1Keys = mLibretroInputReadKeys(0, inputCallback, useBitmasks, &turboState); player2Keys = mLibretroInputReadKeys(1, inputCallback, useBitmasks, &turboState); - mLibretroMultiplayerSetKeys(&multiplayer, core, player1Keys, player2Keys); + mLibretroMultiplayerSetKeys(player1Keys, player2Keys); if (!luxSensorUsed) { static bool wasAdjustingLux = false; @@ -1638,7 +1633,7 @@ void retro_run(void) { updateAudioLatency = false; } - mLibretroMultiplayerRunFrame(&multiplayer, core); + mLibretroMultiplayerRunFrame(); unsigned width, height; core->currentVideoSize(core, &width, &height); @@ -1666,7 +1661,7 @@ void retro_run(void) { size_t outPitch; unsigned outWidth; unsigned outHeight; - const mColor* frame = mLibretroMultiplayerComposeFrame(&multiplayer, outputBuffer, width, height, &outPitch, &outWidth, &outHeight); + const mColor* frame = mLibretroMultiplayerComposeFrame(outputBuffer, width, height, &outPitch, &outWidth, &outHeight); videoCallback(frame, outWidth, outHeight, outPitch); } else { #if defined(COLOR_16_BIT) && defined(COLOR_5_6_5) @@ -1682,7 +1677,7 @@ void retro_run(void) { unsigned outWidth = width; unsigned outHeight = height; if (multiplayer.active) { - mLibretroMultiplayerComposeFrame(&multiplayer, outputBuffer, width, height, &outPitch, &outWidth, &outHeight); + mLibretroMultiplayerComposeFrame(outputBuffer, width, height, &outPitch, &outWidth, &outHeight); } videoCallback(NULL, outWidth, outHeight, outPitch); } @@ -1917,7 +1912,7 @@ static void _setupMaps(struct mCore* core) { } void retro_reset(void) { - mLibretroMultiplayerReset(&multiplayer, core); + mLibretroMultiplayerReset(); mRumbleIntegratorReset(&rumble); _setupMaps(core); } @@ -2000,6 +1995,7 @@ bool retro_load_game(const struct retro_game_info* game) { } mCoreInitConfig(core, NULL); core->init(core); + mLibretroMultiplayerSetPrimaryCore(core); #ifdef _3DS outputBuffer = linearMemAlign(VIDEO_BUFF_SIZE, 0x80); @@ -2062,8 +2058,8 @@ bool retro_load_game(const struct retro_game_info* game) { _reloadSettings(); core->loadROM(core, rom); deferredSetup = true; - mLibretroMultiplayerUpdateMode(&multiplayer, environCallback); - mLibretroMultiplayerApplyMode(&multiplayer, core, data, dataSize, loadedRomPath, logCallback); + mLibretroMultiplayerUpdateMode(environCallback); + mLibretroMultiplayerApplyMode(data, dataSize, loadedRomPath, logCallback); const char* sysDir = 0; const char* biosName = 0; @@ -2131,10 +2127,11 @@ void retro_unload_game(void) { if (!core) { return; } - mLibretroMultiplayerDeinit(&multiplayer, core); + mLibretroMultiplayerDeinit(); mCoreConfigDeinit(&core->config); core->deinit(core); core = NULL; + mLibretroMultiplayerSetPrimaryCore(NULL); mappedMemoryFree(data, dataSize); data = 0; mappedMemoryFree(savedata, GBA_SIZE_FLASH1M); diff --git a/src/platform/libretro/libretro_multiplayer.c b/src/platform/libretro/libretro_multiplayer.c index 7c5a8e6ecdd..bfa2f1861d3 100644 --- a/src/platform/libretro/libretro_multiplayer.c +++ b/src/platform/libretro/libretro_multiplayer.c @@ -12,6 +12,8 @@ #define VIDEO_BYTES_PER_PIXEL sizeof(mColor) #define LOCKSTEP_PUMP_WATCHDOG 2000000 +static struct mLibretroMultiplayer* sMultiplayer; + static void _stepRunnerCore(struct mCore* core) { if (!core) { return; @@ -106,77 +108,79 @@ static void _clearPrimaryLinkPeripheral(struct mCore* primaryCore) { primaryCore->setPeripheral(primaryCore, mPERIPH_GBA_LINK_PORT, NULL); } -static void _detachLockstep(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore) { - if (!multiplayer->coordinatorInitialized) { +static void _detachLockstep(void) { + struct mCore* primaryCore = sMultiplayer->primaryCore; + if (!sMultiplayer->coordinatorInitialized) { return; } if (primaryCore && primaryCore->platform(primaryCore) == mPLATFORM_GBA) { - GBASIOLockstepCoordinatorDetach(&multiplayer->coordinator, &multiplayer->drivers[0]); + GBASIOLockstepCoordinatorDetach(&sMultiplayer->coordinator, &sMultiplayer->drivers[0]); } - if (multiplayer->secondaryCore && multiplayer->secondaryCore->platform(multiplayer->secondaryCore) == mPLATFORM_GBA) { - multiplayer->secondaryCore->setPeripheral(multiplayer->secondaryCore, mPERIPH_GBA_LINK_PORT, NULL); - GBASIOLockstepCoordinatorDetach(&multiplayer->coordinator, &multiplayer->drivers[1]); + if (sMultiplayer->secondaryCore && sMultiplayer->secondaryCore->platform(sMultiplayer->secondaryCore) == mPLATFORM_GBA) { + sMultiplayer->secondaryCore->setPeripheral(sMultiplayer->secondaryCore, mPERIPH_GBA_LINK_PORT, NULL); + GBASIOLockstepCoordinatorDetach(&sMultiplayer->coordinator, &sMultiplayer->drivers[1]); } - GBASIOLockstepCoordinatorDeinit(&multiplayer->coordinator); - multiplayer->coordinatorInitialized = false; + GBASIOLockstepCoordinatorDeinit(&sMultiplayer->coordinator); + sMultiplayer->coordinatorInitialized = false; } -static void _destroySecondaryCore(struct mLibretroMultiplayer* multiplayer) { - if (!multiplayer->secondaryCore) { +static void _destroySecondaryCore(void) { + if (!sMultiplayer->secondaryCore) { return; } - mCoreConfigDeinit(&multiplayer->secondaryCore->config); - multiplayer->secondaryCore->deinit(multiplayer->secondaryCore); - multiplayer->secondaryCore = NULL; + mCoreConfigDeinit(&sMultiplayer->secondaryCore->config); + sMultiplayer->secondaryCore->deinit(sMultiplayer->secondaryCore); + sMultiplayer->secondaryCore = NULL; } -static void _destroyBuffers(struct mLibretroMultiplayer* multiplayer) { - if (multiplayer->secondaryOutputBuffer) { - free(multiplayer->secondaryOutputBuffer); - multiplayer->secondaryOutputBuffer = NULL; +static void _destroyBuffers(void) { + if (sMultiplayer->secondaryOutputBuffer) { + free(sMultiplayer->secondaryOutputBuffer); + sMultiplayer->secondaryOutputBuffer = NULL; } - if (multiplayer->compositeBuffer) { - free(multiplayer->compositeBuffer); - multiplayer->compositeBuffer = NULL; - multiplayer->compositeBufferPixels = 0; + if (sMultiplayer->compositeBuffer) { + free(sMultiplayer->compositeBuffer); + sMultiplayer->compositeBuffer = NULL; + sMultiplayer->compositeBufferPixels = 0; } } -static void _destroySecondaryRom(struct mLibretroMultiplayer* multiplayer) { - if (multiplayer->secondaryRomData) { - mappedMemoryFree(multiplayer->secondaryRomData, multiplayer->secondaryRomSize); - multiplayer->secondaryRomData = NULL; - multiplayer->secondaryRomSize = 0; +static void _destroySecondaryRom(void) { + if (sMultiplayer->secondaryRomData) { + mappedMemoryFree(sMultiplayer->secondaryRomData, sMultiplayer->secondaryRomSize); + sMultiplayer->secondaryRomData = NULL; + sMultiplayer->secondaryRomSize = 0; } } -static void _stopSession(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore) { +static void _stopSession(void) { + struct mCore* primaryCore = sMultiplayer->primaryCore; _clearPrimaryLinkPeripheral(primaryCore); - _detachLockstep(multiplayer, primaryCore); - _destroySecondaryCore(multiplayer); - _destroyBuffers(multiplayer); - _destroySecondaryRom(multiplayer); - multiplayer->primaryCore = NULL; - multiplayer->active = false; + _detachLockstep(); + _destroySecondaryCore(); + _destroyBuffers(); + _destroySecondaryRom(); + sMultiplayer->primaryCore = NULL; + sMultiplayer->active = false; } -static bool _initSecondaryCore(struct mLibretroMultiplayer* multiplayer, const void* romData, size_t romSize, const char* romPath) { +static bool _initSecondaryCore(const void* romData, size_t romSize, const char* romPath) { struct VFile* rom; if (romData && romSize) { - multiplayer->secondaryRomData = anonymousMemoryMap(romSize); - if (!multiplayer->secondaryRomData) { + sMultiplayer->secondaryRomData = anonymousMemoryMap(romSize); + if (!sMultiplayer->secondaryRomData) { return false; } - multiplayer->secondaryRomSize = romSize; - memcpy(multiplayer->secondaryRomData, romData, romSize); + sMultiplayer->secondaryRomSize = romSize; + memcpy(sMultiplayer->secondaryRomData, romData, romSize); - rom = VFileFromMemory(multiplayer->secondaryRomData, romSize); + rom = VFileFromMemory(sMultiplayer->secondaryRomData, romSize); if (!rom) { return false; } @@ -189,104 +193,106 @@ static bool _initSecondaryCore(struct mLibretroMultiplayer* multiplayer, const v return false; } - multiplayer->secondaryCore = mCoreFindVF(rom); - if (!multiplayer->secondaryCore) { + sMultiplayer->secondaryCore = mCoreFindVF(rom); + if (!sMultiplayer->secondaryCore) { rom->close(rom); return false; } - mCoreInitConfig(multiplayer->secondaryCore, NULL); - multiplayer->secondaryCore->init(multiplayer->secondaryCore); + mCoreInitConfig(sMultiplayer->secondaryCore, NULL); + sMultiplayer->secondaryCore->init(sMultiplayer->secondaryCore); - multiplayer->secondaryOutputBuffer = malloc((size_t) multiplayer->maxVideoWidth * multiplayer->maxVideoHeight * VIDEO_BYTES_PER_PIXEL); - if (!multiplayer->secondaryOutputBuffer) { + sMultiplayer->secondaryOutputBuffer = malloc((size_t) sMultiplayer->maxVideoWidth * sMultiplayer->maxVideoHeight * VIDEO_BYTES_PER_PIXEL); + if (!sMultiplayer->secondaryOutputBuffer) { rom->close(rom); return false; } - memset(multiplayer->secondaryOutputBuffer, 0xFF, (size_t) multiplayer->maxVideoWidth * multiplayer->maxVideoHeight * VIDEO_BYTES_PER_PIXEL); - multiplayer->secondaryCore->setVideoBuffer(multiplayer->secondaryCore, multiplayer->secondaryOutputBuffer, multiplayer->maxVideoWidth); + memset(sMultiplayer->secondaryOutputBuffer, 0xFF, (size_t) sMultiplayer->maxVideoWidth * sMultiplayer->maxVideoHeight * VIDEO_BYTES_PER_PIXEL); + sMultiplayer->secondaryCore->setVideoBuffer(sMultiplayer->secondaryCore, sMultiplayer->secondaryOutputBuffer, sMultiplayer->maxVideoWidth); - memset(&multiplayer->secondaryStream, 0, sizeof(multiplayer->secondaryStream)); - multiplayer->secondaryCore->setAVStream(multiplayer->secondaryCore, &multiplayer->secondaryStream); + memset(&sMultiplayer->secondaryStream, 0, sizeof(sMultiplayer->secondaryStream)); + sMultiplayer->secondaryCore->setAVStream(sMultiplayer->secondaryCore, &sMultiplayer->secondaryStream); - if (!multiplayer->secondaryCore->loadROM(multiplayer->secondaryCore, rom)) { + if (!sMultiplayer->secondaryCore->loadROM(sMultiplayer->secondaryCore, rom)) { rom->close(rom); return false; } - multiplayer->secondaryCore->reset(multiplayer->secondaryCore); + sMultiplayer->secondaryCore->reset(sMultiplayer->secondaryCore); return true; } -static bool _attachLockstep(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore) { +static bool _attachLockstep(void) { + struct mCore* primaryCore = sMultiplayer->primaryCore; if (!primaryCore || primaryCore->platform(primaryCore) != mPLATFORM_GBA) { return false; } - if (!multiplayer->secondaryCore || multiplayer->secondaryCore->platform(multiplayer->secondaryCore) != mPLATFORM_GBA) { + if (!sMultiplayer->secondaryCore || sMultiplayer->secondaryCore->platform(sMultiplayer->secondaryCore) != mPLATFORM_GBA) { return false; } - GBASIOLockstepCoordinatorInit(&multiplayer->coordinator); - multiplayer->coordinatorInitialized = true; - - multiplayer->users[0].d.sleep = _lockstepSleep; - multiplayer->users[0].d.wake = _lockstepWake; - multiplayer->users[0].d.requestedId = _requestedId; - multiplayer->users[0].d.playerIdChanged = NULL; - multiplayer->users[0].requestedId = 0; - multiplayer->users[0].multiplayer = multiplayer; - multiplayer->users[0].playerIndex = 0; - multiplayer->users[0].blocked = false; - multiplayer->users[0].stepping = false; - - multiplayer->users[1].d.sleep = _lockstepSleep; - multiplayer->users[1].d.wake = _lockstepWake; - multiplayer->users[1].d.requestedId = _requestedId; - multiplayer->users[1].d.playerIdChanged = NULL; - multiplayer->users[1].requestedId = 1; - multiplayer->users[1].multiplayer = multiplayer; - multiplayer->users[1].playerIndex = 1; - multiplayer->users[1].blocked = false; - multiplayer->users[1].stepping = false; - - GBASIOLockstepDriverCreate(&multiplayer->drivers[0], &multiplayer->users[0].d); - GBASIOLockstepDriverCreate(&multiplayer->drivers[1], &multiplayer->users[1].d); - - GBASIOLockstepCoordinatorAttach(&multiplayer->coordinator, &multiplayer->drivers[0]); - GBASIOLockstepCoordinatorAttach(&multiplayer->coordinator, &multiplayer->drivers[1]); - - primaryCore->setPeripheral(primaryCore, mPERIPH_GBA_LINK_PORT, &multiplayer->drivers[0].d); - multiplayer->secondaryCore->setPeripheral(multiplayer->secondaryCore, mPERIPH_GBA_LINK_PORT, &multiplayer->drivers[1].d); + GBASIOLockstepCoordinatorInit(&sMultiplayer->coordinator); + sMultiplayer->coordinatorInitialized = true; + + sMultiplayer->users[0].d.sleep = _lockstepSleep; + sMultiplayer->users[0].d.wake = _lockstepWake; + sMultiplayer->users[0].d.requestedId = _requestedId; + sMultiplayer->users[0].d.playerIdChanged = NULL; + sMultiplayer->users[0].requestedId = 0; + sMultiplayer->users[0].multiplayer = sMultiplayer; + sMultiplayer->users[0].playerIndex = 0; + sMultiplayer->users[0].blocked = false; + sMultiplayer->users[0].stepping = false; + + sMultiplayer->users[1].d.sleep = _lockstepSleep; + sMultiplayer->users[1].d.wake = _lockstepWake; + sMultiplayer->users[1].d.requestedId = _requestedId; + sMultiplayer->users[1].d.playerIdChanged = NULL; + sMultiplayer->users[1].requestedId = 1; + sMultiplayer->users[1].multiplayer = sMultiplayer; + sMultiplayer->users[1].playerIndex = 1; + sMultiplayer->users[1].blocked = false; + sMultiplayer->users[1].stepping = false; + + GBASIOLockstepDriverCreate(&sMultiplayer->drivers[0], &sMultiplayer->users[0].d); + GBASIOLockstepDriverCreate(&sMultiplayer->drivers[1], &sMultiplayer->users[1].d); + + GBASIOLockstepCoordinatorAttach(&sMultiplayer->coordinator, &sMultiplayer->drivers[0]); + GBASIOLockstepCoordinatorAttach(&sMultiplayer->coordinator, &sMultiplayer->drivers[1]); + + primaryCore->setPeripheral(primaryCore, mPERIPH_GBA_LINK_PORT, &sMultiplayer->drivers[0].d); + sMultiplayer->secondaryCore->setPeripheral(sMultiplayer->secondaryCore, mPERIPH_GBA_LINK_PORT, &sMultiplayer->drivers[1].d); return true; } -static bool _startSession(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore, const void* romData, size_t romSize, const char* romPath) { +static bool _startSession(const void* romData, size_t romSize, const char* romPath) { + struct mCore* primaryCore = sMultiplayer->primaryCore; if (!primaryCore || primaryCore->platform(primaryCore) != mPLATFORM_GBA || !romData || !romSize) { if (!(romPath && *romPath)) { return false; } } - if (!_initSecondaryCore(multiplayer, romData, romSize, romPath)) { - _stopSession(multiplayer, primaryCore); + if (!_initSecondaryCore(romData, romSize, romPath)) { + _stopSession(); return false; } - multiplayer->compositeBufferPixels = _compositePixels(multiplayer->maxVideoWidth, multiplayer->maxVideoHeight); - multiplayer->compositeBuffer = malloc(multiplayer->compositeBufferPixels * VIDEO_BYTES_PER_PIXEL); - if (!multiplayer->compositeBuffer) { - _stopSession(multiplayer, primaryCore); + sMultiplayer->compositeBufferPixels = _compositePixels(sMultiplayer->maxVideoWidth, sMultiplayer->maxVideoHeight); + sMultiplayer->compositeBuffer = malloc(sMultiplayer->compositeBufferPixels * VIDEO_BYTES_PER_PIXEL); + if (!sMultiplayer->compositeBuffer) { + _stopSession(); return false; } - memset(multiplayer->compositeBuffer, 0xFF, multiplayer->compositeBufferPixels * VIDEO_BYTES_PER_PIXEL); + memset(sMultiplayer->compositeBuffer, 0xFF, sMultiplayer->compositeBufferPixels * VIDEO_BYTES_PER_PIXEL); - if (!_attachLockstep(multiplayer, primaryCore)) { - _stopSession(multiplayer, primaryCore); + if (!_attachLockstep()) { + _stopSession(); return false; } - multiplayer->primaryCore = primaryCore; + sMultiplayer->primaryCore = primaryCore; - multiplayer->active = true; + sMultiplayer->active = true; return true; } @@ -296,34 +302,43 @@ void mLibretroMultiplayerInit(struct mLibretroMultiplayer* multiplayer, unsigned multiplayer->maxVideoHeight = maxVideoHeight; multiplayer->mode = mLIBRETRO_SPLITSCREEN_OFF; multiplayer->primaryCore = NULL; + sMultiplayer = multiplayer; } -void mLibretroMultiplayerDeinit(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore) { - _stopSession(multiplayer, primaryCore); - multiplayer->mode = mLIBRETRO_SPLITSCREEN_OFF; +void mLibretroMultiplayerSetPrimaryCore(struct mCore* primaryCore) { + mASSERT(sMultiplayer); + sMultiplayer->primaryCore = primaryCore; +} + +void mLibretroMultiplayerDeinit(void) { + mASSERT(sMultiplayer); + _stopSession(); + sMultiplayer->mode = mLIBRETRO_SPLITSCREEN_OFF; } -void mLibretroMultiplayerUpdateMode(struct mLibretroMultiplayer* multiplayer, retro_environment_t environCallback) { - multiplayer->mode = _parseMode(environCallback); +void mLibretroMultiplayerUpdateMode(retro_environment_t environCallback) { + mASSERT(sMultiplayer); + sMultiplayer->mode = _parseMode(environCallback); } -bool mLibretroMultiplayerApplyMode(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore, const void* romData, size_t romSize, const char* romPath, retro_log_printf_t logCallback) { - if (multiplayer->mode == mLIBRETRO_SPLITSCREEN_OFF) { - if (multiplayer->active) { - _stopSession(multiplayer, primaryCore); +bool mLibretroMultiplayerApplyMode(const void* romData, size_t romSize, const char* romPath, retro_log_printf_t logCallback) { + mASSERT(sMultiplayer); + if (sMultiplayer->mode == mLIBRETRO_SPLITSCREEN_OFF) { + if (sMultiplayer->active) { + _stopSession(); } return true; } - if (multiplayer->active) { + if (sMultiplayer->active) { return true; } - if (!_startSession(multiplayer, primaryCore, romData, romSize, romPath)) { + if (!_startSession(romData, romSize, romPath)) { if (logCallback) { logCallback(RETRO_LOG_WARN, "libretro: failed to start multiplayer splitscreen session; continuing in single-player mode\n"); } - multiplayer->mode = mLIBRETRO_SPLITSCREEN_OFF; + sMultiplayer->mode = mLIBRETRO_SPLITSCREEN_OFF; return false; } @@ -334,69 +349,75 @@ bool mLibretroMultiplayerApplyMode(struct mLibretroMultiplayer* multiplayer, str return true; } -void mLibretroMultiplayerReset(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore) { +void mLibretroMultiplayerReset(void) { + mASSERT(sMultiplayer); + struct mCore* primaryCore = sMultiplayer->primaryCore; if (!primaryCore) { return; } - multiplayer->primaryCore = primaryCore; - if (multiplayer->active && multiplayer->secondaryCore) { - multiplayer->secondaryCore->reset(multiplayer->secondaryCore); + if (sMultiplayer->active && sMultiplayer->secondaryCore) { + sMultiplayer->secondaryCore->reset(sMultiplayer->secondaryCore); } primaryCore->reset(primaryCore); } -void mLibretroMultiplayerSetKeys(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore, uint16_t player1Keys, uint16_t player2Keys) { +void mLibretroMultiplayerSetKeys(uint16_t player1Keys, uint16_t player2Keys) { + mASSERT(sMultiplayer); + struct mCore* primaryCore = sMultiplayer->primaryCore; if (!primaryCore) { return; } primaryCore->setKeys(primaryCore, player1Keys); - if (!multiplayer->active || !multiplayer->secondaryCore) { + if (!sMultiplayer->active || !sMultiplayer->secondaryCore) { return; } - multiplayer->secondaryCore->setKeys(multiplayer->secondaryCore, player2Keys); + sMultiplayer->secondaryCore->setKeys(sMultiplayer->secondaryCore, player2Keys); } -void mLibretroMultiplayerRunFrame(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore) { +void mLibretroMultiplayerRunFrame(void) { + mASSERT(sMultiplayer); + struct mCore* primaryCore = sMultiplayer->primaryCore; if (!primaryCore) { return; } - multiplayer->primaryCore = primaryCore; - multiplayer->pumpedThisFrame[0] = false; - multiplayer->pumpedThisFrame[1] = false; + sMultiplayer->pumpedThisFrame[0] = false; + sMultiplayer->pumpedThisFrame[1] = false; primaryCore->runFrame(primaryCore); - if (multiplayer->active && multiplayer->secondaryCore && !multiplayer->pumpedThisFrame[1]) { - multiplayer->secondaryCore->runFrame(multiplayer->secondaryCore); + if (sMultiplayer->active && sMultiplayer->secondaryCore && !sMultiplayer->pumpedThisFrame[1]) { + sMultiplayer->secondaryCore->runFrame(sMultiplayer->secondaryCore); } } -void mLibretroMultiplayerAdjustGeometry(const struct mLibretroMultiplayer* multiplayer, unsigned* baseWidth, unsigned* baseHeight, unsigned* maxWidth, unsigned* maxHeight, float* aspectRatio) { - if (!multiplayer->active) { +void mLibretroMultiplayerAdjustGeometry(struct retro_game_geometry* geometry) { + mASSERT(sMultiplayer); + if (!sMultiplayer->active || !geometry) { return; } - if (multiplayer->mode == mLIBRETRO_SPLITSCREEN_2P_VERTICAL) { - *baseWidth *= 2; - *maxWidth *= 2; - *aspectRatio *= 2.0; + if (sMultiplayer->mode == mLIBRETRO_SPLITSCREEN_2P_VERTICAL) { + geometry->base_width *= 2; + geometry->max_width *= 2; + geometry->aspect_ratio *= 2.0; return; } - if (multiplayer->mode == mLIBRETRO_SPLITSCREEN_2P_HORIZONTAL) { - *baseHeight *= 2; - *maxHeight *= 2; - *aspectRatio *= 0.5; + if (sMultiplayer->mode == mLIBRETRO_SPLITSCREEN_2P_HORIZONTAL) { + geometry->base_height *= 2; + geometry->max_height *= 2; + geometry->aspect_ratio *= 0.5; } } -const mColor* mLibretroMultiplayerComposeFrame(struct mLibretroMultiplayer* multiplayer, const mColor* primaryFrame, unsigned primaryWidth, unsigned primaryHeight, size_t* outPitch, unsigned* outWidth, unsigned* outHeight) { - if (!multiplayer->active || !multiplayer->secondaryCore || !multiplayer->compositeBuffer || !multiplayer->secondaryOutputBuffer) { +const mColor* mLibretroMultiplayerComposeFrame(const mColor* primaryFrame, unsigned primaryWidth, unsigned primaryHeight, size_t* outPitch, unsigned* outWidth, unsigned* outHeight) { + mASSERT(sMultiplayer); + if (!sMultiplayer->active || !sMultiplayer->secondaryCore || !sMultiplayer->compositeBuffer || !sMultiplayer->secondaryOutputBuffer) { *outPitch = (size_t) primaryWidth * VIDEO_BYTES_PER_PIXEL; *outWidth = primaryWidth; *outHeight = primaryHeight; @@ -404,40 +425,40 @@ const mColor* mLibretroMultiplayerComposeFrame(struct mLibretroMultiplayer* mult } unsigned secondaryWidth, secondaryHeight; - multiplayer->secondaryCore->currentVideoSize(multiplayer->secondaryCore, &secondaryWidth, &secondaryHeight); + sMultiplayer->secondaryCore->currentVideoSize(sMultiplayer->secondaryCore, &secondaryWidth, &secondaryHeight); - if (multiplayer->mode == mLIBRETRO_SPLITSCREEN_2P_VERTICAL) { + if (sMultiplayer->mode == mLIBRETRO_SPLITSCREEN_2P_VERTICAL) { size_t y; *outWidth = primaryWidth + secondaryWidth; *outHeight = primaryHeight > secondaryHeight ? primaryHeight : secondaryHeight; *outPitch = (size_t) *outWidth * VIDEO_BYTES_PER_PIXEL; - memset(multiplayer->compositeBuffer, 0xFF, (size_t) *outWidth * *outHeight * VIDEO_BYTES_PER_PIXEL); + memset(sMultiplayer->compositeBuffer, 0xFF, (size_t) *outWidth * *outHeight * VIDEO_BYTES_PER_PIXEL); for (y = 0; y < primaryHeight; ++y) { - mColor* row = &multiplayer->compositeBuffer[y * (*outWidth)]; - memcpy(row, &primaryFrame[y * multiplayer->maxVideoWidth], (size_t) primaryWidth * VIDEO_BYTES_PER_PIXEL); + mColor* row = &sMultiplayer->compositeBuffer[y * (*outWidth)]; + memcpy(row, &primaryFrame[y * sMultiplayer->maxVideoWidth], (size_t) primaryWidth * VIDEO_BYTES_PER_PIXEL); } for (y = 0; y < secondaryHeight; ++y) { - mColor* row = &multiplayer->compositeBuffer[y * (*outWidth) + primaryWidth]; - memcpy(row, &multiplayer->secondaryOutputBuffer[y * multiplayer->maxVideoWidth], (size_t) secondaryWidth * VIDEO_BYTES_PER_PIXEL); + mColor* row = &sMultiplayer->compositeBuffer[y * (*outWidth) + primaryWidth]; + memcpy(row, &sMultiplayer->secondaryOutputBuffer[y * sMultiplayer->maxVideoWidth], (size_t) secondaryWidth * VIDEO_BYTES_PER_PIXEL); } - return multiplayer->compositeBuffer; + return sMultiplayer->compositeBuffer; } - if (multiplayer->mode == mLIBRETRO_SPLITSCREEN_2P_HORIZONTAL) { + if (sMultiplayer->mode == mLIBRETRO_SPLITSCREEN_2P_HORIZONTAL) { size_t y; *outWidth = primaryWidth > secondaryWidth ? primaryWidth : secondaryWidth; *outHeight = primaryHeight + secondaryHeight; *outPitch = (size_t) *outWidth * VIDEO_BYTES_PER_PIXEL; - memset(multiplayer->compositeBuffer, 0xFF, (size_t) *outWidth * *outHeight * VIDEO_BYTES_PER_PIXEL); + memset(sMultiplayer->compositeBuffer, 0xFF, (size_t) *outWidth * *outHeight * VIDEO_BYTES_PER_PIXEL); for (y = 0; y < primaryHeight; ++y) { - mColor* row = &multiplayer->compositeBuffer[y * (*outWidth)]; - memcpy(row, &primaryFrame[y * multiplayer->maxVideoWidth], (size_t) primaryWidth * VIDEO_BYTES_PER_PIXEL); + mColor* row = &sMultiplayer->compositeBuffer[y * (*outWidth)]; + memcpy(row, &primaryFrame[y * sMultiplayer->maxVideoWidth], (size_t) primaryWidth * VIDEO_BYTES_PER_PIXEL); } for (y = 0; y < secondaryHeight; ++y) { - mColor* row = &multiplayer->compositeBuffer[(y + primaryHeight) * (*outWidth)]; - memcpy(row, &multiplayer->secondaryOutputBuffer[y * multiplayer->maxVideoWidth], (size_t) secondaryWidth * VIDEO_BYTES_PER_PIXEL); + mColor* row = &sMultiplayer->compositeBuffer[(y + primaryHeight) * (*outWidth)]; + memcpy(row, &sMultiplayer->secondaryOutputBuffer[y * sMultiplayer->maxVideoWidth], (size_t) secondaryWidth * VIDEO_BYTES_PER_PIXEL); } - return multiplayer->compositeBuffer; + return sMultiplayer->compositeBuffer; } *outPitch = (size_t) primaryWidth * VIDEO_BYTES_PER_PIXEL; diff --git a/src/platform/libretro/libretro_multiplayer.h b/src/platform/libretro/libretro_multiplayer.h index 4e3b04bd545..ec9e377526d 100644 --- a/src/platform/libretro/libretro_multiplayer.h +++ b/src/platform/libretro/libretro_multiplayer.h @@ -52,14 +52,15 @@ struct mLibretroMultiplayer { }; void mLibretroMultiplayerInit(struct mLibretroMultiplayer* multiplayer, unsigned maxVideoWidth, unsigned maxVideoHeight); -void mLibretroMultiplayerDeinit(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore); -void mLibretroMultiplayerUpdateMode(struct mLibretroMultiplayer* multiplayer, retro_environment_t environCallback); -bool mLibretroMultiplayerApplyMode(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore, const void* romData, size_t romSize, const char* romPath, retro_log_printf_t logCallback); -void mLibretroMultiplayerReset(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore); -void mLibretroMultiplayerSetKeys(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore, uint16_t player1Keys, uint16_t player2Keys); -void mLibretroMultiplayerRunFrame(struct mLibretroMultiplayer* multiplayer, struct mCore* primaryCore); -void mLibretroMultiplayerAdjustGeometry(const struct mLibretroMultiplayer* multiplayer, unsigned* baseWidth, unsigned* baseHeight, unsigned* maxWidth, unsigned* maxHeight, float* aspectRatio); -const mColor* mLibretroMultiplayerComposeFrame(struct mLibretroMultiplayer* multiplayer, const mColor* primaryFrame, unsigned primaryWidth, unsigned primaryHeight, size_t* outPitch, unsigned* outWidth, unsigned* outHeight); +void mLibretroMultiplayerSetPrimaryCore(struct mCore* primaryCore); +void mLibretroMultiplayerDeinit(void); +void mLibretroMultiplayerUpdateMode(retro_environment_t environCallback); +bool mLibretroMultiplayerApplyMode(const void* romData, size_t romSize, const char* romPath, retro_log_printf_t logCallback); +void mLibretroMultiplayerReset(void); +void mLibretroMultiplayerSetKeys(uint16_t player1Keys, uint16_t player2Keys); +void mLibretroMultiplayerRunFrame(void); +void mLibretroMultiplayerAdjustGeometry(struct retro_game_geometry* geometry); +const mColor* mLibretroMultiplayerComposeFrame(const mColor* primaryFrame, unsigned primaryWidth, unsigned primaryHeight, size_t* outPitch, unsigned* outWidth, unsigned* outHeight); CXX_GUARD_END From 96c5cc069482e612512958e7a7d23e006c982a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Tue, 24 Feb 2026 19:17:47 +0300 Subject: [PATCH 04/11] libretro: fix mgba_multiplayer_splitscreen so it only applies on reset (and properly applies on reset) --- src/platform/libretro/libretro.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/platform/libretro/libretro.c b/src/platform/libretro/libretro.c index 2ca2dc832f7..a161356bdee 100644 --- a/src/platform/libretro/libretro.c +++ b/src/platform/libretro/libretro.c @@ -1545,8 +1545,6 @@ void retro_run(void) { #if defined(COLOR_16_BIT) && defined(COLOR_5_6_5) _loadPostProcessingSettings(); #endif - mLibretroMultiplayerUpdateMode(environCallback); - mLibretroMultiplayerApplyMode(data, dataSize, loadedRomPath, logCallback); #ifdef M_CORE_GB _updateGbPal(); #endif @@ -1912,9 +1910,16 @@ static void _setupMaps(struct mCore* core) { } void retro_reset(void) { + mLibretroMultiplayerUpdateMode(environCallback); + mLibretroMultiplayerApplyMode(data, dataSize, loadedRomPath, logCallback); + mLibretroMultiplayerSetPrimaryCore(core); mLibretroMultiplayerReset(); mRumbleIntegratorReset(&rumble); _setupMaps(core); + + struct retro_system_av_info info; + retro_get_system_av_info(&info); + environCallback(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &info); } #ifdef GEKKO From 4c367c443c4f00ef13df97b206ca9f202220a56d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Wed, 25 Feb 2026 04:14:17 +0300 Subject: [PATCH 05/11] libretro: implement multiplayer savestate serialization and deserialization --- libretro-build/Makefile.common | 1 + src/platform/libretro/libretro.c | 25 +--- src/platform/libretro/libretro_multiplayer.c | 63 ++++++++ src/platform/libretro/libretro_multiplayer.h | 4 + src/platform/libretro/libretro_savestate.c | 146 +++++++++++++++++++ src/platform/libretro/libretro_savestate.h | 18 +++ 6 files changed, 236 insertions(+), 21 deletions(-) create mode 100644 src/platform/libretro/libretro_savestate.c create mode 100644 src/platform/libretro/libretro_savestate.h diff --git a/libretro-build/Makefile.common b/libretro-build/Makefile.common index 870752ae8a1..20a6330a31a 100644 --- a/libretro-build/Makefile.common +++ b/libretro-build/Makefile.common @@ -114,6 +114,7 @@ SOURCES_C := $(CORE_DIR)/src/arm/arm.c \ $(CORE_DIR)/src/platform/libretro/memory.c \ $(CORE_DIR)/src/platform/libretro/libretro_input.c \ $(CORE_DIR)/src/platform/libretro/libretro_lockstep.c \ + $(CORE_DIR)/src/platform/libretro/libretro_savestate.c \ $(CORE_DIR)/src/platform/libretro/libretro_multiplayer.c \ $(CORE_DIR)/src/platform/libretro/libretro.c \ $(CORE_DIR)/src/sm83/isa-sm83.c \ diff --git a/src/platform/libretro/libretro.c b/src/platform/libretro/libretro.c index a161356bdee..12ab7db0f09 100644 --- a/src/platform/libretro/libretro.c +++ b/src/platform/libretro/libretro.c @@ -44,6 +44,7 @@ FS_Archive sdmcArchive; #include "libretro_core_options.h" #include "libretro_input.h" #include "libretro_multiplayer.h" +#include "libretro_savestate.h" #define GBA_RESAMPLED_RATE 65536 static unsigned targetSampleRate = GBA_RESAMPLED_RATE; @@ -2148,39 +2149,21 @@ size_t retro_serialize_size(void) { if (deferredSetup) { _doDeferredSetup(); } - struct VFile* vfm = VFileMemChunk(NULL, 0); - mCoreSaveStateNamed(core, vfm, SAVESTATE_SAVEDATA | SAVESTATE_RTC); - size_t size = vfm->size(vfm); - vfm->close(vfm); - return size; + return mLibretroSerializeSize(core); } bool retro_serialize(void* data, size_t size) { if (deferredSetup) { _doDeferredSetup(); } - struct VFile* vfm = VFileMemChunk(NULL, 0); - mCoreSaveStateNamed(core, vfm, SAVESTATE_SAVEDATA | SAVESTATE_RTC); - if ((ssize_t) size > vfm->size(vfm)) { - size = vfm->size(vfm); - } else if ((ssize_t) size < vfm->size(vfm)) { - vfm->close(vfm); - return false; - } - vfm->seek(vfm, 0, SEEK_SET); - vfm->read(vfm, data, size); - vfm->close(vfm); - return true; + return mLibretroSerialize(core, data, size); } bool retro_unserialize(const void* data, size_t size) { if (deferredSetup) { _doDeferredSetup(); } - struct VFile* vfm = VFileFromConstMemory(data, size); - bool success = mCoreLoadStateNamed(core, vfm, SAVESTATE_RTC); - vfm->close(vfm); - return success; + return mLibretroUnserialize(core, data, size, logCallback); } void retro_cheat_reset(void) { diff --git a/src/platform/libretro/libretro_multiplayer.c b/src/platform/libretro/libretro_multiplayer.c index bfa2f1861d3..5adf878f970 100644 --- a/src/platform/libretro/libretro_multiplayer.c +++ b/src/platform/libretro/libretro_multiplayer.c @@ -1,6 +1,7 @@ #include "libretro_multiplayer.h" #include +#include #include #include #include @@ -466,3 +467,65 @@ const mColor* mLibretroMultiplayerComposeFrame(const mColor* primaryFrame, unsig *outHeight = primaryHeight; return primaryFrame; } + +bool mLibretroMultiplayerStateActive(void) { + mASSERT(sMultiplayer); + return sMultiplayer->active && sMultiplayer->secondaryCore; +} + +size_t mLibretroMultiplayerSerializeSize(void) { + mASSERT(sMultiplayer); + if (!mLibretroMultiplayerStateActive()) { + return 0; + } + + struct VFile* vfm = VFileMemChunk(NULL, 0); + if (!vfm) { + return 0; + } + + mCoreSaveStateNamed(sMultiplayer->secondaryCore, vfm, SAVESTATE_SAVEDATA | SAVESTATE_RTC); + size_t size = vfm->size(vfm); + vfm->close(vfm); + return size; +} + +bool mLibretroMultiplayerSerialize(void* data, size_t size) { + mASSERT(sMultiplayer); + if (!mLibretroMultiplayerStateActive() || !data) { + return false; + } + + struct VFile* vfm = VFileMemChunk(NULL, 0); + if (!vfm) { + return false; + } + + mCoreSaveStateNamed(sMultiplayer->secondaryCore, vfm, SAVESTATE_SAVEDATA | SAVESTATE_RTC); + ssize_t stateSize = vfm->size(vfm); + if ((ssize_t) size != stateSize) { + vfm->close(vfm); + return false; + } + + vfm->seek(vfm, 0, SEEK_SET); + vfm->read(vfm, data, size); + vfm->close(vfm); + return true; +} + +bool mLibretroMultiplayerUnserialize(const void* data, size_t size) { + mASSERT(sMultiplayer); + if (!mLibretroMultiplayerStateActive() || !data) { + return false; + } + + struct VFile* vfm = VFileFromConstMemory(data, size); + if (!vfm) { + return false; + } + + bool success = mCoreLoadStateNamed(sMultiplayer->secondaryCore, vfm, SAVESTATE_RTC); + vfm->close(vfm); + return success; +} diff --git a/src/platform/libretro/libretro_multiplayer.h b/src/platform/libretro/libretro_multiplayer.h index ec9e377526d..d75eee0dbd5 100644 --- a/src/platform/libretro/libretro_multiplayer.h +++ b/src/platform/libretro/libretro_multiplayer.h @@ -61,6 +61,10 @@ void mLibretroMultiplayerSetKeys(uint16_t player1Keys, uint16_t player2Keys); void mLibretroMultiplayerRunFrame(void); void mLibretroMultiplayerAdjustGeometry(struct retro_game_geometry* geometry); const mColor* mLibretroMultiplayerComposeFrame(const mColor* primaryFrame, unsigned primaryWidth, unsigned primaryHeight, size_t* outPitch, unsigned* outWidth, unsigned* outHeight); +bool mLibretroMultiplayerStateActive(void); +size_t mLibretroMultiplayerSerializeSize(void); +bool mLibretroMultiplayerSerialize(void* data, size_t size); +bool mLibretroMultiplayerUnserialize(const void* data, size_t size); CXX_GUARD_END diff --git a/src/platform/libretro/libretro_savestate.c b/src/platform/libretro/libretro_savestate.c new file mode 100644 index 00000000000..7acd560dbbd --- /dev/null +++ b/src/platform/libretro/libretro_savestate.c @@ -0,0 +1,146 @@ +#include "libretro_savestate.h" + +#include +#include + +#include + +#include "libretro_multiplayer.h" + +struct mLibretroSerializedMultiplayerHeader { + uint32_t magic; + uint32_t version; + uint32_t primarySize; + uint32_t secondarySize; +}; + +#define LIBRETRO_MULTIPLAYER_STATE_MAGIC 0x314C504D +#define LIBRETRO_MULTIPLAYER_STATE_VERSION 1 + +size_t mLibretroSerializeSize(struct mCore* core) { + if (!core) { + return 0; + } + + struct VFile* vfm = VFileMemChunk(NULL, 0); + if (!vfm) { + return 0; + } + mCoreSaveStateNamed(core, vfm, SAVESTATE_SAVEDATA | SAVESTATE_RTC); + size_t size = vfm->size(vfm); + vfm->close(vfm); + + if (!mLibretroMultiplayerStateActive()) { + return size; + } + + size_t secondarySize = mLibretroMultiplayerSerializeSize(); + if (!secondarySize) { + return size; + } + + return sizeof(struct mLibretroSerializedMultiplayerHeader) + size + secondarySize; +} + +bool mLibretroSerialize(struct mCore* core, void* data, size_t size) { + if (!core || !data) { + return false; + } + + struct VFile* vfm = VFileMemChunk(NULL, 0); + if (!vfm) { + return false; + } + mCoreSaveStateNamed(core, vfm, SAVESTATE_SAVEDATA | SAVESTATE_RTC); + ssize_t primarySize = vfm->size(vfm); + if (primarySize <= 0) { + vfm->close(vfm); + return false; + } + + if (!mLibretroMultiplayerStateActive()) { + if ((ssize_t) size > primarySize) { + size = primarySize; + } else if ((ssize_t) size < primarySize) { + vfm->close(vfm); + return false; + } + vfm->seek(vfm, 0, SEEK_SET); + vfm->read(vfm, data, size); + vfm->close(vfm); + return true; + } + + size_t secondarySize = mLibretroMultiplayerSerializeSize(); + if (!secondarySize) { + vfm->close(vfm); + return false; + } + + size_t totalSize = sizeof(struct mLibretroSerializedMultiplayerHeader) + (size_t) primarySize + secondarySize; + if (size != totalSize) { + vfm->close(vfm); + return false; + } + + struct mLibretroSerializedMultiplayerHeader header = { + .magic = LIBRETRO_MULTIPLAYER_STATE_MAGIC, + .version = LIBRETRO_MULTIPLAYER_STATE_VERSION, + .primarySize = (uint32_t) primarySize, + .secondarySize = (uint32_t) secondarySize, + }; + + memcpy(data, &header, sizeof(header)); + vfm->seek(vfm, 0, SEEK_SET); + vfm->read(vfm, (uint8_t*) data + sizeof(header), (size_t) primarySize); + vfm->close(vfm); + + return mLibretroMultiplayerSerialize((uint8_t*) data + sizeof(header) + (size_t) primarySize, secondarySize); +} + +bool mLibretroUnserialize(struct mCore* core, const void* data, size_t size, retro_log_printf_t logCallback) { + if (!core || !data) { + return false; + } + + if (size >= sizeof(struct mLibretroSerializedMultiplayerHeader)) { + const struct mLibretroSerializedMultiplayerHeader* header = data; + if (header->magic == LIBRETRO_MULTIPLAYER_STATE_MAGIC && header->version == LIBRETRO_MULTIPLAYER_STATE_VERSION) { + size_t totalSize = sizeof(*header) + (size_t) header->primarySize + (size_t) header->secondarySize; + if (size != totalSize || !header->primarySize || !header->secondarySize) { + return false; + } + + if (!mLibretroMultiplayerStateActive()) { + if (logCallback) { + logCallback(RETRO_LOG_WARN, "libretro: multiplayer savestate requires active splitscreen session\n"); + } + return false; + } + + const uint8_t* payload = (const uint8_t*) data + sizeof(*header); + const void* primaryData = payload; + const void* secondaryData = payload + header->primarySize; + + if (!mLibretroMultiplayerUnserialize(secondaryData, header->secondarySize)) { + return false; + } + + struct VFile* primary = VFileFromConstMemory(primaryData, header->primarySize); + if (!primary) { + return false; + } + bool success = mCoreLoadStateNamed(core, primary, SAVESTATE_RTC); + primary->close(primary); + return success; + } + } + + struct VFile* vfm = VFileFromConstMemory(data, size); + if (!vfm) { + return false; + } + bool success = mCoreLoadStateNamed(core, vfm, SAVESTATE_RTC); + vfm->close(vfm); + return success; +} diff --git a/src/platform/libretro/libretro_savestate.h b/src/platform/libretro/libretro_savestate.h new file mode 100644 index 00000000000..683a16b402e --- /dev/null +++ b/src/platform/libretro/libretro_savestate.h @@ -0,0 +1,18 @@ +#ifndef MGBA_LIBRETRO_SAVESTATE_H +#define MGBA_LIBRETRO_SAVESTATE_H + +#include + +#include + +#include "libretro.h" + +CXX_GUARD_START + +size_t mLibretroSerializeSize(struct mCore* core); +bool mLibretroSerialize(struct mCore* core, void* data, size_t size); +bool mLibretroUnserialize(struct mCore* core, const void* data, size_t size, retro_log_printf_t logCallback); + +CXX_GUARD_END + +#endif From 5c920a1975bf17daf59ecb10e81efa91a6ee7a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Sun, 1 Mar 2026 21:48:08 +0300 Subject: [PATCH 06/11] libretro_multiplayer: freeze guards --- src/platform/libretro/libretro_lockstep.c | 3 +-- src/platform/libretro/libretro_multiplayer.c | 8 ++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/platform/libretro/libretro_lockstep.c b/src/platform/libretro/libretro_lockstep.c index 6ae596897a1..fef64a8c650 100644 --- a/src/platform/libretro/libretro_lockstep.c +++ b/src/platform/libretro/libretro_lockstep.c @@ -971,8 +971,7 @@ void _lockstepEvent(struct mTiming* timing, void* context, uint32_t cyclesLate) _verifyAwake(coordinator); } } - - mASSERT_DEBUG(nextEvent > 0); + mASSERT(nextEvent > 0); mTimingSchedule(timing, &lockstep->event, nextEvent); } diff --git a/src/platform/libretro/libretro_multiplayer.c b/src/platform/libretro/libretro_multiplayer.c index 5adf878f970..256f2ae4b87 100644 --- a/src/platform/libretro/libretro_multiplayer.c +++ b/src/platform/libretro/libretro_multiplayer.c @@ -34,8 +34,6 @@ static void _stepRunnerCore(struct mCore* core) { static void _lockstepSleep(struct mLockstepUser* user) { struct mLibretroLockstepUser* lockstepUser = (struct mLibretroLockstepUser*) user; - lockstepUser->blocked = true; - if (!lockstepUser->multiplayer || !lockstepUser->multiplayer->active || !lockstepUser->multiplayer->secondaryCore || !lockstepUser->multiplayer->primaryCore) { return; } @@ -44,6 +42,8 @@ static void _lockstepSleep(struct mLockstepUser* user) { return; } + lockstepUser->blocked = true; + struct mCore* runner = lockstepUser->playerIndex == 0 ? lockstepUser->multiplayer->secondaryCore : lockstepUser->multiplayer->primaryCore; @@ -64,6 +64,10 @@ static void _lockstepSleep(struct mLockstepUser* user) { } lockstepUser->stepping = false; lockstepUser->multiplayer->pumping = false; + if (watchdog <= 0) { + mLOG(GBA_SIO, FATAL, "Lockstep single-thread watchdog expired while waiting for peer wake"); + mASSERT(false); + } mASSERT_LOG(GBA_SIO, !lockstepUser->blocked, "Lockstep single-thread watchdog expired while waiting for peer wake"); } From 45c167b284727b71c5a0475d62535084839af231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Sun, 1 Mar 2026 23:31:20 +0300 Subject: [PATCH 07/11] libretro_multiplayer: replaced pumping with controlled run --- src/platform/libretro/libretro_lockstep.c | 2 +- src/platform/libretro/libretro_multiplayer.c | 100 +++++++++---------- src/platform/libretro/libretro_multiplayer.h | 3 - 3 files changed, 46 insertions(+), 59 deletions(-) diff --git a/src/platform/libretro/libretro_lockstep.c b/src/platform/libretro/libretro_lockstep.c index fef64a8c650..7b676115937 100644 --- a/src/platform/libretro/libretro_lockstep.c +++ b/src/platform/libretro/libretro_lockstep.c @@ -971,7 +971,7 @@ void _lockstepEvent(struct mTiming* timing, void* context, uint32_t cyclesLate) _verifyAwake(coordinator); } } - mASSERT(nextEvent > 0); + mASSERT_DEBUG(nextEvent > 0); mTimingSchedule(timing, &lockstep->event, nextEvent); } diff --git a/src/platform/libretro/libretro_multiplayer.c b/src/platform/libretro/libretro_multiplayer.c index 256f2ae4b87..e38fe5f0020 100644 --- a/src/platform/libretro/libretro_multiplayer.c +++ b/src/platform/libretro/libretro_multiplayer.c @@ -11,64 +11,17 @@ #include #define VIDEO_BYTES_PER_PIXEL sizeof(mColor) -#define LOCKSTEP_PUMP_WATCHDOG 2000000 +#define COOPERATIVE_WATCHDOG 4000000 static struct mLibretroMultiplayer* sMultiplayer; -static void _stepRunnerCore(struct mCore* core) { - if (!core) { - return; - } - if (core->runLoop) { - core->runLoop(core); - return; - } - if (core->step) { - core->step(core); - return; - } - if (core->runFrame) { - core->runFrame(core); - } -} - static void _lockstepSleep(struct mLockstepUser* user) { struct mLibretroLockstepUser* lockstepUser = (struct mLibretroLockstepUser*) user; if (!lockstepUser->multiplayer || !lockstepUser->multiplayer->active || !lockstepUser->multiplayer->secondaryCore || !lockstepUser->multiplayer->primaryCore) { return; } - if (lockstepUser->stepping || lockstepUser->multiplayer->pumping) { - return; - } - lockstepUser->blocked = true; - - struct mCore* runner = lockstepUser->playerIndex == 0 ? - lockstepUser->multiplayer->secondaryCore : - lockstepUser->multiplayer->primaryCore; - unsigned runnerIndex = lockstepUser->playerIndex == 0 ? 1 : 0; - if (!runner || (!runner->step && !runner->runLoop && !runner->runFrame)) { - return; - } - - lockstepUser->multiplayer->pumping = true; - lockstepUser->stepping = true; - int watchdog = LOCKSTEP_PUMP_WATCHDOG; - while (lockstepUser->blocked && watchdog-- > 0) { - if (!lockstepUser->multiplayer->active) { - break; - } - lockstepUser->multiplayer->pumpedThisFrame[runnerIndex] = true; - _stepRunnerCore(runner); - } - lockstepUser->stepping = false; - lockstepUser->multiplayer->pumping = false; - if (watchdog <= 0) { - mLOG(GBA_SIO, FATAL, "Lockstep single-thread watchdog expired while waiting for peer wake"); - mASSERT(false); - } - mASSERT_LOG(GBA_SIO, !lockstepUser->blocked, "Lockstep single-thread watchdog expired while waiting for peer wake"); } static void _lockstepWake(struct mLockstepUser* user) { @@ -247,7 +200,6 @@ static bool _attachLockstep(void) { sMultiplayer->users[0].multiplayer = sMultiplayer; sMultiplayer->users[0].playerIndex = 0; sMultiplayer->users[0].blocked = false; - sMultiplayer->users[0].stepping = false; sMultiplayer->users[1].d.sleep = _lockstepSleep; sMultiplayer->users[1].d.wake = _lockstepWake; @@ -257,7 +209,6 @@ static bool _attachLockstep(void) { sMultiplayer->users[1].multiplayer = sMultiplayer; sMultiplayer->users[1].playerIndex = 1; sMultiplayer->users[1].blocked = false; - sMultiplayer->users[1].stepping = false; GBASIOLockstepDriverCreate(&sMultiplayer->drivers[0], &sMultiplayer->users[0].d); GBASIOLockstepDriverCreate(&sMultiplayer->drivers[1], &sMultiplayer->users[1].d); @@ -390,13 +341,52 @@ void mLibretroMultiplayerRunFrame(void) { if (!primaryCore) { return; } - sMultiplayer->pumpedThisFrame[0] = false; - sMultiplayer->pumpedThisFrame[1] = false; - primaryCore->runFrame(primaryCore); + if (!sMultiplayer->active || !sMultiplayer->secondaryCore) { + primaryCore->runFrame(primaryCore); + return; + } - if (sMultiplayer->active && sMultiplayer->secondaryCore && !sMultiplayer->pumpedThisFrame[1]) { - sMultiplayer->secondaryCore->runFrame(sMultiplayer->secondaryCore); + struct mCore* secondaryCore = sMultiplayer->secondaryCore; + uint32_t primaryFrame = primaryCore->frameCounter(primaryCore); + uint32_t secondaryFrame = secondaryCore->frameCounter(secondaryCore); + bool primaryDone = false; + bool secondaryDone = false; + + int watchdog = COOPERATIVE_WATCHDOG; + while ((!primaryDone || !secondaryDone) && watchdog > 0) { + bool ranAny = false; + + // Run primary if it hasn't finished its frame, OR if the secondary + // is blocked (needs primary's lockstep events to unblock it) + if (!sMultiplayer->users[0].blocked && (!primaryDone || sMultiplayer->users[1].blocked)) { + primaryCore->runLoop(primaryCore); + --watchdog; + ranAny = true; + if (!primaryDone && primaryCore->frameCounter(primaryCore) != primaryFrame) { + primaryDone = true; + } + } + + // Same for secondary + if (!sMultiplayer->users[1].blocked && (!secondaryDone || sMultiplayer->users[0].blocked)) { + secondaryCore->runLoop(secondaryCore); + --watchdog; + ranAny = true; + if (!secondaryDone && secondaryCore->frameCounter(secondaryCore) != secondaryFrame) { + secondaryDone = true; + } + } + + if (!ranAny) { + mLOG(GBA_SIO, FATAL, "Both players blocked simultaneously -- deadlock"); + mASSERT(false); + break; + } + } + if (watchdog <= 0) { + mLOG(GBA_SIO, FATAL, "Cooperative scheduling watchdog expired"); + mASSERT(false); } } diff --git a/src/platform/libretro/libretro_multiplayer.h b/src/platform/libretro/libretro_multiplayer.h index d75eee0dbd5..2334bcc15ed 100644 --- a/src/platform/libretro/libretro_multiplayer.h +++ b/src/platform/libretro/libretro_multiplayer.h @@ -23,7 +23,6 @@ struct mLibretroLockstepUser { struct mLibretroMultiplayer* multiplayer; unsigned playerIndex; bool blocked; - bool stepping; }; struct mLibretroMultiplayer { @@ -42,8 +41,6 @@ struct mLibretroMultiplayer { size_t secondaryRomSize; struct mAVStream secondaryStream; - bool pumping; - bool pumpedThisFrame[2]; struct mLibretroLockstepUser users[2]; struct GBASIOLockstepCoordinator coordinator; From 5df8f80559d571776d70a44b571e50f547fbb652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Mon, 2 Mar 2026 00:04:10 +0300 Subject: [PATCH 08/11] libretro: organized multiplayer into cores array, added 4 player support --- src/platform/libretro/libretro.c | 31 +- src/platform/libretro/libretro_core_options.h | 3 +- src/platform/libretro/libretro_multiplayer.c | 582 +++++++++++------- src/platform/libretro/libretro_multiplayer.h | 27 +- 4 files changed, 397 insertions(+), 246 deletions(-) diff --git a/src/platform/libretro/libretro.c b/src/platform/libretro/libretro.c index 12ab7db0f09..20fc2a1abd0 100644 --- a/src/platform/libretro/libretro.c +++ b/src/platform/libretro/libretro.c @@ -1521,8 +1521,7 @@ void retro_run(void) { if (deferredSetup) { _doDeferredSetup(); } - uint16_t player1Keys; - uint16_t player2Keys; + uint16_t playerKeys[MAX_GBAS]; bool skipFrame = false; inputPollCallback(); @@ -1551,9 +1550,11 @@ void retro_run(void) { #endif } - player1Keys = mLibretroInputReadKeys(0, inputCallback, useBitmasks, &turboState); - player2Keys = mLibretroInputReadKeys(1, inputCallback, useBitmasks, &turboState); - mLibretroMultiplayerSetKeys(player1Keys, player2Keys); + int p; + for (p = 0; p < MAX_GBAS; ++p) { + playerKeys[p] = mLibretroInputReadKeys(p, inputCallback, useBitmasks, &turboState); + } + mLibretroMultiplayerSetKeys(playerKeys); if (!luxSensorUsed) { static bool wasAdjustingLux = false; @@ -1656,13 +1657,11 @@ void retro_run(void) { } if (!skipFrame) { - if (multiplayer.active) { - size_t outPitch; - unsigned outWidth; - unsigned outHeight; - const mColor* frame = mLibretroMultiplayerComposeFrame(outputBuffer, width, height, &outPitch, &outWidth, &outHeight); - videoCallback(frame, outWidth, outHeight, outPitch); - } else { + size_t outPitch; + unsigned outWidth; + unsigned outHeight; + const mColor* frame = mLibretroMultiplayerComposeFrame(outputBuffer, width, height, &outPitch, &outWidth, &outHeight); + if (frame == outputBuffer) { #if defined(COLOR_16_BIT) && defined(COLOR_5_6_5) if (videoPostProcess) { videoPostProcess(width, height); @@ -1670,14 +1669,14 @@ void retro_run(void) { } else #endif videoCallback(outputBuffer, width, height, VIDEO_WIDTH_MAX * sizeof(mColor)); + } else { + videoCallback(frame, outWidth, outHeight, outPitch); } } else { size_t outPitch = VIDEO_WIDTH_MAX * sizeof(mColor); unsigned outWidth = width; unsigned outHeight = height; - if (multiplayer.active) { - mLibretroMultiplayerComposeFrame(outputBuffer, width, height, &outPitch, &outWidth, &outHeight); - } + mLibretroMultiplayerComposeFrame(outputBuffer, width, height, &outPitch, &outWidth, &outHeight); videoCallback(NULL, outWidth, outHeight, outPitch); } @@ -1911,9 +1910,9 @@ static void _setupMaps(struct mCore* core) { } void retro_reset(void) { + mLibretroMultiplayerSetPrimaryCore(core); mLibretroMultiplayerUpdateMode(environCallback); mLibretroMultiplayerApplyMode(data, dataSize, loadedRomPath, logCallback); - mLibretroMultiplayerSetPrimaryCore(core); mLibretroMultiplayerReset(); mRumbleIntegratorReset(&rumble); _setupMaps(core); diff --git a/src/platform/libretro/libretro_core_options.h b/src/platform/libretro/libretro_core_options.h index f7aec11a0bf..caaf8d31c96 100644 --- a/src/platform/libretro/libretro_core_options.h +++ b/src/platform/libretro/libretro_core_options.h @@ -105,13 +105,14 @@ struct retro_core_option_v2_definition option_defs_us[] = { "mgba_multiplayer_splitscreen", "Multiplayer Splitscreen (Restart)", NULL, - "Runs a linked second GBA instance and combines both player views into a single output frame.", + "Runs linked GBA instances and combines all player views into a single output frame.", NULL, "system", { { "OFF", "disabled" }, { "Side by Side", "2-Player Side by Side" }, { "Top/Bottom", "2-Player Top/Bottom" }, + { "4-Player Grid", "4-Player 2x2 Grid" }, { NULL, NULL }, }, "OFF" diff --git a/src/platform/libretro/libretro_multiplayer.c b/src/platform/libretro/libretro_multiplayer.c index e38fe5f0020..65cc0824a62 100644 --- a/src/platform/libretro/libretro_multiplayer.c +++ b/src/platform/libretro/libretro_multiplayer.c @@ -17,7 +17,7 @@ static struct mLibretroMultiplayer* sMultiplayer; static void _lockstepSleep(struct mLockstepUser* user) { struct mLibretroLockstepUser* lockstepUser = (struct mLibretroLockstepUser*) user; - if (!lockstepUser->multiplayer || !lockstepUser->multiplayer->active || !lockstepUser->multiplayer->secondaryCore || !lockstepUser->multiplayer->primaryCore) { + if (!lockstepUser->multiplayer || lockstepUser->multiplayer->numPlayers < 2) { return; } @@ -34,6 +34,18 @@ static int _requestedId(struct mLockstepUser* user) { return lockstepUser->requestedId; } +static int _modePlayerCount(enum mLibretroSplitscreenMode mode) { + switch (mode) { + case mLIBRETRO_SPLITSCREEN_2P_VERTICAL: + case mLIBRETRO_SPLITSCREEN_2P_HORIZONTAL: + return 2; + case mLIBRETRO_SPLITSCREEN_4P_GRID: + return 4; + default: + return 1; + } +} + static enum mLibretroSplitscreenMode _parseMode(retro_environment_t environCallback) { struct retro_variable var = { .key = "mgba_multiplayer_splitscreen", @@ -52,6 +64,10 @@ static enum mLibretroSplitscreenMode _parseMode(retro_environment_t environCallb return mLIBRETRO_SPLITSCREEN_2P_HORIZONTAL; } + if (strcmp(var.value, "4-Player Grid") == 0) { + return mLIBRETRO_SPLITSCREEN_4P_GRID; + } + return mLIBRETRO_SPLITSCREEN_OFF; } @@ -59,46 +75,52 @@ static size_t _compositePixels(unsigned maxVideoWidth, unsigned maxVideoHeight) return (size_t) maxVideoWidth * 2 * maxVideoHeight * 2; } -static void _clearPrimaryLinkPeripheral(struct mCore* primaryCore) { - if (!primaryCore || primaryCore->platform(primaryCore) != mPLATFORM_GBA) { +static void _clearPrimaryLinkPeripheral(void) { + struct mCore* primary = sMultiplayer->cores[0]; + if (!primary || primary->platform(primary) != mPLATFORM_GBA) { return; } - primaryCore->setPeripheral(primaryCore, mPERIPH_GBA_LINK_PORT, NULL); + primary->setPeripheral(primary, mPERIPH_GBA_LINK_PORT, NULL); } static void _detachLockstep(void) { - struct mCore* primaryCore = sMultiplayer->primaryCore; if (!sMultiplayer->coordinatorInitialized) { return; } - if (primaryCore && primaryCore->platform(primaryCore) == mPLATFORM_GBA) { - GBASIOLockstepCoordinatorDetach(&sMultiplayer->coordinator, &sMultiplayer->drivers[0]); - } - - if (sMultiplayer->secondaryCore && sMultiplayer->secondaryCore->platform(sMultiplayer->secondaryCore) == mPLATFORM_GBA) { - sMultiplayer->secondaryCore->setPeripheral(sMultiplayer->secondaryCore, mPERIPH_GBA_LINK_PORT, NULL); - GBASIOLockstepCoordinatorDetach(&sMultiplayer->coordinator, &sMultiplayer->drivers[1]); + int i; + for (i = 0; i < sMultiplayer->numPlayers; ++i) { + struct mCore* core = sMultiplayer->cores[i]; + if (core && core->platform(core) == mPLATFORM_GBA) { + if (i > 0) { + core->setPeripheral(core, mPERIPH_GBA_LINK_PORT, NULL); + } + GBASIOLockstepCoordinatorDetach(&sMultiplayer->coordinator, &sMultiplayer->drivers[i]); + } } GBASIOLockstepCoordinatorDeinit(&sMultiplayer->coordinator); sMultiplayer->coordinatorInitialized = false; } -static void _destroySecondaryCore(void) { - if (!sMultiplayer->secondaryCore) { - return; +static void _destroySecondaryCores(void) { + int i; + for (i = 1; i < sMultiplayer->numPlayers; ++i) { + if (sMultiplayer->cores[i]) { + mCoreConfigDeinit(&sMultiplayer->cores[i]->config); + sMultiplayer->cores[i]->deinit(sMultiplayer->cores[i]); + sMultiplayer->cores[i] = NULL; + } } - - mCoreConfigDeinit(&sMultiplayer->secondaryCore->config); - sMultiplayer->secondaryCore->deinit(sMultiplayer->secondaryCore); - sMultiplayer->secondaryCore = NULL; } static void _destroyBuffers(void) { - if (sMultiplayer->secondaryOutputBuffer) { - free(sMultiplayer->secondaryOutputBuffer); - sMultiplayer->secondaryOutputBuffer = NULL; + int i; + for (i = 1; i < MAX_GBAS; ++i) { + if (sMultiplayer->outputBuffers[i]) { + free(sMultiplayer->outputBuffers[i]); + sMultiplayer->outputBuffers[i] = NULL; + } } if (sMultiplayer->compositeBuffer) { @@ -108,128 +130,127 @@ static void _destroyBuffers(void) { } } -static void _destroySecondaryRom(void) { - if (sMultiplayer->secondaryRomData) { - mappedMemoryFree(sMultiplayer->secondaryRomData, sMultiplayer->secondaryRomSize); - sMultiplayer->secondaryRomData = NULL; - sMultiplayer->secondaryRomSize = 0; +static void _destroyRomData(void) { + if (sMultiplayer->romData) { + mappedMemoryFree(sMultiplayer->romData, sMultiplayer->romSize); + sMultiplayer->romData = NULL; + sMultiplayer->romSize = 0; } } static void _stopSession(void) { - struct mCore* primaryCore = sMultiplayer->primaryCore; - _clearPrimaryLinkPeripheral(primaryCore); + _clearPrimaryLinkPeripheral(); _detachLockstep(); - _destroySecondaryCore(); + _destroySecondaryCores(); _destroyBuffers(); - _destroySecondaryRom(); - sMultiplayer->primaryCore = NULL; - sMultiplayer->active = false; + _destroyRomData(); + sMultiplayer->numPlayers = sMultiplayer->cores[0] ? 1 : 0; } -static bool _initSecondaryCore(const void* romData, size_t romSize, const char* romPath) { - struct VFile* rom; +static bool _initSecondaryCores(int numPlayers, const void* romData, size_t romSize, const char* romPath) { + if (numPlayers < 2 || numPlayers > MAX_GBAS) { + return false; + } if (romData && romSize) { - sMultiplayer->secondaryRomData = anonymousMemoryMap(romSize); - if (!sMultiplayer->secondaryRomData) { + sMultiplayer->romData = anonymousMemoryMap(romSize); + if (!sMultiplayer->romData) { return false; } - sMultiplayer->secondaryRomSize = romSize; - memcpy(sMultiplayer->secondaryRomData, romData, romSize); + sMultiplayer->romSize = romSize; + memcpy(sMultiplayer->romData, romData, romSize); + } else if (!(romPath && *romPath)) { + return false; + } - rom = VFileFromMemory(sMultiplayer->secondaryRomData, romSize); + int i; + for (i = 1; i < numPlayers; ++i) { + struct VFile* rom; + if (sMultiplayer->romData) { + rom = VFileFromMemory(sMultiplayer->romData, sMultiplayer->romSize); + } else { + rom = VFileOpen(romPath, O_RDONLY); + } if (!rom) { return false; } - } else if (romPath && *romPath) { - rom = VFileOpen(romPath, O_RDONLY); - if (!rom) { + + sMultiplayer->cores[i] = mCoreFindVF(rom); + if (!sMultiplayer->cores[i]) { + rom->close(rom); return false; } - } else { - return false; - } - sMultiplayer->secondaryCore = mCoreFindVF(rom); - if (!sMultiplayer->secondaryCore) { - rom->close(rom); - return false; - } + mCoreInitConfig(sMultiplayer->cores[i], NULL); + sMultiplayer->cores[i]->init(sMultiplayer->cores[i]); - mCoreInitConfig(sMultiplayer->secondaryCore, NULL); - sMultiplayer->secondaryCore->init(sMultiplayer->secondaryCore); + sMultiplayer->outputBuffers[i] = malloc((size_t) sMultiplayer->maxVideoWidth * sMultiplayer->maxVideoHeight * VIDEO_BYTES_PER_PIXEL); + if (!sMultiplayer->outputBuffers[i]) { + return false; + } + memset(sMultiplayer->outputBuffers[i], 0xFF, (size_t) sMultiplayer->maxVideoWidth * sMultiplayer->maxVideoHeight * VIDEO_BYTES_PER_PIXEL); + sMultiplayer->cores[i]->setVideoBuffer(sMultiplayer->cores[i], sMultiplayer->outputBuffers[i], sMultiplayer->maxVideoWidth); - sMultiplayer->secondaryOutputBuffer = malloc((size_t) sMultiplayer->maxVideoWidth * sMultiplayer->maxVideoHeight * VIDEO_BYTES_PER_PIXEL); - if (!sMultiplayer->secondaryOutputBuffer) { - rom->close(rom); - return false; - } - memset(sMultiplayer->secondaryOutputBuffer, 0xFF, (size_t) sMultiplayer->maxVideoWidth * sMultiplayer->maxVideoHeight * VIDEO_BYTES_PER_PIXEL); - sMultiplayer->secondaryCore->setVideoBuffer(sMultiplayer->secondaryCore, sMultiplayer->secondaryOutputBuffer, sMultiplayer->maxVideoWidth); + memset(&sMultiplayer->streams[i], 0, sizeof(sMultiplayer->streams[i])); + sMultiplayer->cores[i]->setAVStream(sMultiplayer->cores[i], &sMultiplayer->streams[i]); - memset(&sMultiplayer->secondaryStream, 0, sizeof(sMultiplayer->secondaryStream)); - sMultiplayer->secondaryCore->setAVStream(sMultiplayer->secondaryCore, &sMultiplayer->secondaryStream); + if (!sMultiplayer->cores[i]->loadROM(sMultiplayer->cores[i], rom)) { + rom->close(rom); + return false; + } - if (!sMultiplayer->secondaryCore->loadROM(sMultiplayer->secondaryCore, rom)) { - rom->close(rom); - return false; + sMultiplayer->cores[i]->reset(sMultiplayer->cores[i]); + sMultiplayer->numPlayers = i + 1; } - sMultiplayer->secondaryCore->reset(sMultiplayer->secondaryCore); return true; } static bool _attachLockstep(void) { - struct mCore* primaryCore = sMultiplayer->primaryCore; - if (!primaryCore || primaryCore->platform(primaryCore) != mPLATFORM_GBA) { + struct mCore* primary = sMultiplayer->cores[0]; + if (!primary || primary->platform(primary) != mPLATFORM_GBA) { return false; } - if (!sMultiplayer->secondaryCore || sMultiplayer->secondaryCore->platform(sMultiplayer->secondaryCore) != mPLATFORM_GBA) { - return false; + + int i; + for (i = 1; i < sMultiplayer->numPlayers; ++i) { + if (!sMultiplayer->cores[i] || sMultiplayer->cores[i]->platform(sMultiplayer->cores[i]) != mPLATFORM_GBA) { + return false; + } } GBASIOLockstepCoordinatorInit(&sMultiplayer->coordinator); sMultiplayer->coordinatorInitialized = true; - sMultiplayer->users[0].d.sleep = _lockstepSleep; - sMultiplayer->users[0].d.wake = _lockstepWake; - sMultiplayer->users[0].d.requestedId = _requestedId; - sMultiplayer->users[0].d.playerIdChanged = NULL; - sMultiplayer->users[0].requestedId = 0; - sMultiplayer->users[0].multiplayer = sMultiplayer; - sMultiplayer->users[0].playerIndex = 0; - sMultiplayer->users[0].blocked = false; - - sMultiplayer->users[1].d.sleep = _lockstepSleep; - sMultiplayer->users[1].d.wake = _lockstepWake; - sMultiplayer->users[1].d.requestedId = _requestedId; - sMultiplayer->users[1].d.playerIdChanged = NULL; - sMultiplayer->users[1].requestedId = 1; - sMultiplayer->users[1].multiplayer = sMultiplayer; - sMultiplayer->users[1].playerIndex = 1; - sMultiplayer->users[1].blocked = false; - - GBASIOLockstepDriverCreate(&sMultiplayer->drivers[0], &sMultiplayer->users[0].d); - GBASIOLockstepDriverCreate(&sMultiplayer->drivers[1], &sMultiplayer->users[1].d); - - GBASIOLockstepCoordinatorAttach(&sMultiplayer->coordinator, &sMultiplayer->drivers[0]); - GBASIOLockstepCoordinatorAttach(&sMultiplayer->coordinator, &sMultiplayer->drivers[1]); - - primaryCore->setPeripheral(primaryCore, mPERIPH_GBA_LINK_PORT, &sMultiplayer->drivers[0].d); - sMultiplayer->secondaryCore->setPeripheral(sMultiplayer->secondaryCore, mPERIPH_GBA_LINK_PORT, &sMultiplayer->drivers[1].d); + for (i = 0; i < sMultiplayer->numPlayers; ++i) { + sMultiplayer->users[i].d.sleep = _lockstepSleep; + sMultiplayer->users[i].d.wake = _lockstepWake; + sMultiplayer->users[i].d.requestedId = _requestedId; + sMultiplayer->users[i].d.playerIdChanged = NULL; + sMultiplayer->users[i].requestedId = i; + sMultiplayer->users[i].multiplayer = sMultiplayer; + sMultiplayer->users[i].playerIndex = i; + sMultiplayer->users[i].blocked = false; + + GBASIOLockstepDriverCreate(&sMultiplayer->drivers[i], &sMultiplayer->users[i].d); + GBASIOLockstepCoordinatorAttach(&sMultiplayer->coordinator, &sMultiplayer->drivers[i]); + } + + for (i = 0; i < sMultiplayer->numPlayers; ++i) { + sMultiplayer->cores[i]->setPeripheral(sMultiplayer->cores[i], mPERIPH_GBA_LINK_PORT, &sMultiplayer->drivers[i].d); + } return true; } -static bool _startSession(const void* romData, size_t romSize, const char* romPath) { - struct mCore* primaryCore = sMultiplayer->primaryCore; - if (!primaryCore || primaryCore->platform(primaryCore) != mPLATFORM_GBA || !romData || !romSize) { +static bool _startSession(int numPlayers, const void* romData, size_t romSize, const char* romPath) { + struct mCore* primary = sMultiplayer->cores[0]; + if (!primary || primary->platform(primary) != mPLATFORM_GBA || !romData || !romSize) { if (!(romPath && *romPath)) { return false; } } - if (!_initSecondaryCore(romData, romSize, romPath)) { + if (!_initSecondaryCores(numPlayers, romData, romSize, romPath)) { _stopSession(); return false; } @@ -246,9 +267,7 @@ static bool _startSession(const void* romData, size_t romSize, const char* romPa _stopSession(); return false; } - sMultiplayer->primaryCore = primaryCore; - sMultiplayer->active = true; return true; } @@ -257,13 +276,17 @@ void mLibretroMultiplayerInit(struct mLibretroMultiplayer* multiplayer, unsigned multiplayer->maxVideoWidth = maxVideoWidth; multiplayer->maxVideoHeight = maxVideoHeight; multiplayer->mode = mLIBRETRO_SPLITSCREEN_OFF; - multiplayer->primaryCore = NULL; sMultiplayer = multiplayer; } void mLibretroMultiplayerSetPrimaryCore(struct mCore* primaryCore) { mASSERT(sMultiplayer); - sMultiplayer->primaryCore = primaryCore; + sMultiplayer->cores[0] = primaryCore; + if (!primaryCore) { + sMultiplayer->numPlayers = 0; + } else if (sMultiplayer->numPlayers < 1) { + sMultiplayer->numPlayers = 1; + } } void mLibretroMultiplayerDeinit(void) { @@ -279,18 +302,22 @@ void mLibretroMultiplayerUpdateMode(retro_environment_t environCallback) { bool mLibretroMultiplayerApplyMode(const void* romData, size_t romSize, const char* romPath, retro_log_printf_t logCallback) { mASSERT(sMultiplayer); - if (sMultiplayer->mode == mLIBRETRO_SPLITSCREEN_OFF) { - if (sMultiplayer->active) { - _stopSession(); - } + int desiredPlayers = _modePlayerCount(sMultiplayer->mode); + + /* Tear down if the player count changed or if switching to single-player. */ + if (sMultiplayer->numPlayers > 1 && sMultiplayer->numPlayers != desiredPlayers) { + _stopSession(); + } + + if (desiredPlayers <= 1) { return true; } - if (sMultiplayer->active) { + if (sMultiplayer->numPlayers == desiredPlayers) { return true; } - if (!_startSession(romData, romSize, romPath)) { + if (!_startSession(desiredPlayers, romData, romSize, romPath)) { if (logCallback) { logCallback(RETRO_LOG_WARN, "libretro: failed to start multiplayer splitscreen session; continuing in single-player mode\n"); } @@ -299,7 +326,7 @@ bool mLibretroMultiplayerApplyMode(const void* romData, size_t romSize, const ch } if (logCallback) { - logCallback(RETRO_LOG_INFO, "libretro: started 2-player splitscreen multiplayer session\n"); + logCallback(RETRO_LOG_INFO, "libretro: started %d-player splitscreen multiplayer session\n", sMultiplayer->numPlayers); } return true; @@ -307,82 +334,97 @@ bool mLibretroMultiplayerApplyMode(const void* romData, size_t romSize, const ch void mLibretroMultiplayerReset(void) { mASSERT(sMultiplayer); - struct mCore* primaryCore = sMultiplayer->primaryCore; - if (!primaryCore) { + if (!sMultiplayer->cores[0]) { return; } - if (sMultiplayer->active && sMultiplayer->secondaryCore) { - sMultiplayer->secondaryCore->reset(sMultiplayer->secondaryCore); + int i; + for (i = 1; i < sMultiplayer->numPlayers; ++i) { + if (sMultiplayer->cores[i]) { + sMultiplayer->cores[i]->reset(sMultiplayer->cores[i]); + } } - primaryCore->reset(primaryCore); + sMultiplayer->cores[0]->reset(sMultiplayer->cores[0]); } -void mLibretroMultiplayerSetKeys(uint16_t player1Keys, uint16_t player2Keys) { +void mLibretroMultiplayerSetKeys(uint16_t keys[MAX_GBAS]) { mASSERT(sMultiplayer); - struct mCore* primaryCore = sMultiplayer->primaryCore; - if (!primaryCore) { + if (!sMultiplayer->cores[0]) { return; } - primaryCore->setKeys(primaryCore, player1Keys); - - if (!sMultiplayer->active || !sMultiplayer->secondaryCore) { - return; + int i; + for (i = 0; i < sMultiplayer->numPlayers; ++i) { + if (sMultiplayer->cores[i]) { + sMultiplayer->cores[i]->setKeys(sMultiplayer->cores[i], keys[i]); + } } - - sMultiplayer->secondaryCore->setKeys(sMultiplayer->secondaryCore, player2Keys); } void mLibretroMultiplayerRunFrame(void) { mASSERT(sMultiplayer); - struct mCore* primaryCore = sMultiplayer->primaryCore; - if (!primaryCore) { + struct mCore* primary = sMultiplayer->cores[0]; + if (!primary) { return; } - if (!sMultiplayer->active || !sMultiplayer->secondaryCore) { - primaryCore->runFrame(primaryCore); + if (sMultiplayer->numPlayers < 2) { + primary->runFrame(primary); return; } - struct mCore* secondaryCore = sMultiplayer->secondaryCore; - uint32_t primaryFrame = primaryCore->frameCounter(primaryCore); - uint32_t secondaryFrame = secondaryCore->frameCounter(secondaryCore); - bool primaryDone = false; - bool secondaryDone = false; + uint32_t startFrames[MAX_GBAS]; + bool done[MAX_GBAS]; + + int i; + for (i = 0; i < sMultiplayer->numPlayers; ++i) { + startFrames[i] = sMultiplayer->cores[i]->frameCounter(sMultiplayer->cores[i]); + done[i] = false; + } int watchdog = COOPERATIVE_WATCHDOG; - while ((!primaryDone || !secondaryDone) && watchdog > 0) { + bool allDone = false; + while (!allDone && watchdog > 0) { bool ranAny = false; + int blockedCount = 0; - // Run primary if it hasn't finished its frame, OR if the secondary - // is blocked (needs primary's lockstep events to unblock it) - if (!sMultiplayer->users[0].blocked && (!primaryDone || sMultiplayer->users[1].blocked)) { - primaryCore->runLoop(primaryCore); - --watchdog; - ranAny = true; - if (!primaryDone && primaryCore->frameCounter(primaryCore) != primaryFrame) { - primaryDone = true; + for (i = 0; i < sMultiplayer->numPlayers; ++i) { + if (sMultiplayer->users[i].blocked) { + ++blockedCount; } } - // Same for secondary - if (!sMultiplayer->users[1].blocked && (!secondaryDone || sMultiplayer->users[0].blocked)) { - secondaryCore->runLoop(secondaryCore); + for (i = 0; i < sMultiplayer->numPlayers; ++i) { + if (sMultiplayer->users[i].blocked) { + continue; + } + if (done[i] && blockedCount == 0) { + continue; + } + + sMultiplayer->cores[i]->runLoop(sMultiplayer->cores[i]); --watchdog; ranAny = true; - if (!secondaryDone && secondaryCore->frameCounter(secondaryCore) != secondaryFrame) { - secondaryDone = true; + + if (!done[i] && sMultiplayer->cores[i]->frameCounter(sMultiplayer->cores[i]) != startFrames[i]) { + done[i] = true; } } if (!ranAny) { - mLOG(GBA_SIO, FATAL, "Both players blocked simultaneously -- deadlock"); + mLOG(GBA_SIO, FATAL, "All players blocked simultaneously -- deadlock"); mASSERT(false); break; } + + allDone = true; + for (i = 0; i < sMultiplayer->numPlayers; ++i) { + if (!done[i]) { + allDone = false; + break; + } + } } if (watchdog <= 0) { mLOG(GBA_SIO, FATAL, "Cooperative scheduling watchdog expired"); @@ -392,68 +434,140 @@ void mLibretroMultiplayerRunFrame(void) { void mLibretroMultiplayerAdjustGeometry(struct retro_game_geometry* geometry) { mASSERT(sMultiplayer); - if (!sMultiplayer->active || !geometry) { + if (sMultiplayer->numPlayers < 2 || !geometry) { return; } - if (sMultiplayer->mode == mLIBRETRO_SPLITSCREEN_2P_VERTICAL) { + switch (sMultiplayer->mode) { + case mLIBRETRO_SPLITSCREEN_2P_VERTICAL: geometry->base_width *= 2; geometry->max_width *= 2; geometry->aspect_ratio *= 2.0; - return; - } - - if (sMultiplayer->mode == mLIBRETRO_SPLITSCREEN_2P_HORIZONTAL) { + break; + case mLIBRETRO_SPLITSCREEN_2P_HORIZONTAL: geometry->base_height *= 2; geometry->max_height *= 2; geometry->aspect_ratio *= 0.5; + break; + case mLIBRETRO_SPLITSCREEN_4P_GRID: + geometry->base_width *= 2; + geometry->max_width *= 2; + geometry->base_height *= 2; + geometry->max_height *= 2; + break; + default: + break; + } +} + +static const mColor* _compose2PVertical(const mColor* primaryFrame, unsigned primaryWidth, unsigned primaryHeight, size_t* outPitch, unsigned* outWidth, unsigned* outHeight) { + unsigned secWidth, secHeight; + sMultiplayer->cores[1]->currentVideoSize(sMultiplayer->cores[1], &secWidth, &secHeight); + + *outWidth = primaryWidth + secWidth; + *outHeight = primaryHeight > secHeight ? primaryHeight : secHeight; + *outPitch = (size_t) *outWidth * VIDEO_BYTES_PER_PIXEL; + memset(sMultiplayer->compositeBuffer, 0xFF, (size_t) *outWidth * *outHeight * VIDEO_BYTES_PER_PIXEL); + + size_t y; + for (y = 0; y < primaryHeight; ++y) { + mColor* row = &sMultiplayer->compositeBuffer[y * (*outWidth)]; + memcpy(row, &primaryFrame[y * sMultiplayer->maxVideoWidth], (size_t) primaryWidth * VIDEO_BYTES_PER_PIXEL); + } + for (y = 0; y < secHeight; ++y) { + mColor* row = &sMultiplayer->compositeBuffer[y * (*outWidth) + primaryWidth]; + memcpy(row, &sMultiplayer->outputBuffers[1][y * sMultiplayer->maxVideoWidth], (size_t) secWidth * VIDEO_BYTES_PER_PIXEL); + } + return sMultiplayer->compositeBuffer; +} + +static const mColor* _compose2PHorizontal(const mColor* primaryFrame, unsigned primaryWidth, unsigned primaryHeight, size_t* outPitch, unsigned* outWidth, unsigned* outHeight) { + unsigned secWidth, secHeight; + sMultiplayer->cores[1]->currentVideoSize(sMultiplayer->cores[1], &secWidth, &secHeight); + + *outWidth = primaryWidth > secWidth ? primaryWidth : secWidth; + *outHeight = primaryHeight + secHeight; + *outPitch = (size_t) *outWidth * VIDEO_BYTES_PER_PIXEL; + memset(sMultiplayer->compositeBuffer, 0xFF, (size_t) *outWidth * *outHeight * VIDEO_BYTES_PER_PIXEL); + + size_t y; + for (y = 0; y < primaryHeight; ++y) { + mColor* row = &sMultiplayer->compositeBuffer[y * (*outWidth)]; + memcpy(row, &primaryFrame[y * sMultiplayer->maxVideoWidth], (size_t) primaryWidth * VIDEO_BYTES_PER_PIXEL); + } + for (y = 0; y < secHeight; ++y) { + mColor* row = &sMultiplayer->compositeBuffer[(y + primaryHeight) * (*outWidth)]; + memcpy(row, &sMultiplayer->outputBuffers[1][y * sMultiplayer->maxVideoWidth], (size_t) secWidth * VIDEO_BYTES_PER_PIXEL); + } + return sMultiplayer->compositeBuffer; +} + +static const mColor* _compose4PGrid(const mColor* primaryFrame, unsigned primaryWidth, unsigned primaryHeight, size_t* outPitch, unsigned* outWidth, unsigned* outHeight) { + unsigned widths[MAX_GBAS], heights[MAX_GBAS]; + widths[0] = primaryWidth; + heights[0] = primaryHeight; + + int i; + for (i = 1; i < sMultiplayer->numPlayers; ++i) { + sMultiplayer->cores[i]->currentVideoSize(sMultiplayer->cores[i], &widths[i], &heights[i]); } + /* Fill any unused slots with the primary size for layout calculation. */ + for (i = sMultiplayer->numPlayers; i < MAX_GBAS; ++i) { + widths[i] = primaryWidth; + heights[i] = primaryHeight; + } + + unsigned topWidth = widths[0] + widths[1]; + unsigned botWidth = widths[2] + widths[3]; + *outWidth = topWidth > botWidth ? topWidth : botWidth; + + unsigned topHeight = heights[0] > heights[1] ? heights[0] : heights[1]; + unsigned botHeight = heights[2] > heights[3] ? heights[2] : heights[3]; + *outHeight = topHeight + botHeight; + *outPitch = (size_t) *outWidth * VIDEO_BYTES_PER_PIXEL; + + memset(sMultiplayer->compositeBuffer, 0xFF, (size_t) *outWidth * *outHeight * VIDEO_BYTES_PER_PIXEL); + + /* Quadrant positions: [0]=top-left, [1]=top-right, [2]=bottom-left, [3]=bottom-right */ + unsigned offX[4] = { 0, widths[0], 0, widths[2] }; + unsigned offY[4] = { 0, 0, topHeight, topHeight }; + + const mColor* framePtrs[MAX_GBAS]; + framePtrs[0] = primaryFrame; + for (i = 1; i < sMultiplayer->numPlayers; ++i) { + framePtrs[i] = sMultiplayer->outputBuffers[i]; + } + + for (i = 0; i < sMultiplayer->numPlayers && i < MAX_GBAS; ++i) { + size_t y; + for (y = 0; y < heights[i]; ++y) { + mColor* dst = &sMultiplayer->compositeBuffer[(offY[i] + y) * (*outWidth) + offX[i]]; + const mColor* src = &framePtrs[i][y * sMultiplayer->maxVideoWidth]; + memcpy(dst, src, (size_t) widths[i] * VIDEO_BYTES_PER_PIXEL); + } + } + + return sMultiplayer->compositeBuffer; } const mColor* mLibretroMultiplayerComposeFrame(const mColor* primaryFrame, unsigned primaryWidth, unsigned primaryHeight, size_t* outPitch, unsigned* outWidth, unsigned* outHeight) { mASSERT(sMultiplayer); - if (!sMultiplayer->active || !sMultiplayer->secondaryCore || !sMultiplayer->compositeBuffer || !sMultiplayer->secondaryOutputBuffer) { + if (sMultiplayer->numPlayers < 2 || !sMultiplayer->compositeBuffer) { *outPitch = (size_t) primaryWidth * VIDEO_BYTES_PER_PIXEL; *outWidth = primaryWidth; *outHeight = primaryHeight; return primaryFrame; } - unsigned secondaryWidth, secondaryHeight; - sMultiplayer->secondaryCore->currentVideoSize(sMultiplayer->secondaryCore, &secondaryWidth, &secondaryHeight); - - if (sMultiplayer->mode == mLIBRETRO_SPLITSCREEN_2P_VERTICAL) { - size_t y; - *outWidth = primaryWidth + secondaryWidth; - *outHeight = primaryHeight > secondaryHeight ? primaryHeight : secondaryHeight; - *outPitch = (size_t) *outWidth * VIDEO_BYTES_PER_PIXEL; - memset(sMultiplayer->compositeBuffer, 0xFF, (size_t) *outWidth * *outHeight * VIDEO_BYTES_PER_PIXEL); - for (y = 0; y < primaryHeight; ++y) { - mColor* row = &sMultiplayer->compositeBuffer[y * (*outWidth)]; - memcpy(row, &primaryFrame[y * sMultiplayer->maxVideoWidth], (size_t) primaryWidth * VIDEO_BYTES_PER_PIXEL); - } - for (y = 0; y < secondaryHeight; ++y) { - mColor* row = &sMultiplayer->compositeBuffer[y * (*outWidth) + primaryWidth]; - memcpy(row, &sMultiplayer->secondaryOutputBuffer[y * sMultiplayer->maxVideoWidth], (size_t) secondaryWidth * VIDEO_BYTES_PER_PIXEL); - } - return sMultiplayer->compositeBuffer; - } - - if (sMultiplayer->mode == mLIBRETRO_SPLITSCREEN_2P_HORIZONTAL) { - size_t y; - *outWidth = primaryWidth > secondaryWidth ? primaryWidth : secondaryWidth; - *outHeight = primaryHeight + secondaryHeight; - *outPitch = (size_t) *outWidth * VIDEO_BYTES_PER_PIXEL; - memset(sMultiplayer->compositeBuffer, 0xFF, (size_t) *outWidth * *outHeight * VIDEO_BYTES_PER_PIXEL); - for (y = 0; y < primaryHeight; ++y) { - mColor* row = &sMultiplayer->compositeBuffer[y * (*outWidth)]; - memcpy(row, &primaryFrame[y * sMultiplayer->maxVideoWidth], (size_t) primaryWidth * VIDEO_BYTES_PER_PIXEL); - } - for (y = 0; y < secondaryHeight; ++y) { - mColor* row = &sMultiplayer->compositeBuffer[(y + primaryHeight) * (*outWidth)]; - memcpy(row, &sMultiplayer->secondaryOutputBuffer[y * sMultiplayer->maxVideoWidth], (size_t) secondaryWidth * VIDEO_BYTES_PER_PIXEL); - } - return sMultiplayer->compositeBuffer; + switch (sMultiplayer->mode) { + case mLIBRETRO_SPLITSCREEN_2P_VERTICAL: + return _compose2PVertical(primaryFrame, primaryWidth, primaryHeight, outPitch, outWidth, outHeight); + case mLIBRETRO_SPLITSCREEN_2P_HORIZONTAL: + return _compose2PHorizontal(primaryFrame, primaryWidth, primaryHeight, outPitch, outWidth, outHeight); + case mLIBRETRO_SPLITSCREEN_4P_GRID: + return _compose4PGrid(primaryFrame, primaryWidth, primaryHeight, outPitch, outWidth, outHeight); + default: + break; } *outPitch = (size_t) primaryWidth * VIDEO_BYTES_PER_PIXEL; @@ -464,7 +578,7 @@ const mColor* mLibretroMultiplayerComposeFrame(const mColor* primaryFrame, unsig bool mLibretroMultiplayerStateActive(void) { mASSERT(sMultiplayer); - return sMultiplayer->active && sMultiplayer->secondaryCore; + return sMultiplayer->numPlayers > 1; } size_t mLibretroMultiplayerSerializeSize(void) { @@ -473,15 +587,18 @@ size_t mLibretroMultiplayerSerializeSize(void) { return 0; } - struct VFile* vfm = VFileMemChunk(NULL, 0); - if (!vfm) { - return 0; + size_t total = 0; + int i; + for (i = 1; i < sMultiplayer->numPlayers; ++i) { + struct VFile* vfm = VFileMemChunk(NULL, 0); + if (!vfm) { + return 0; + } + mCoreSaveStateNamed(sMultiplayer->cores[i], vfm, SAVESTATE_SAVEDATA | SAVESTATE_RTC); + total += vfm->size(vfm); + vfm->close(vfm); } - - mCoreSaveStateNamed(sMultiplayer->secondaryCore, vfm, SAVESTATE_SAVEDATA | SAVESTATE_RTC); - size_t size = vfm->size(vfm); - vfm->close(vfm); - return size; + return total; } bool mLibretroMultiplayerSerialize(void* data, size_t size) { @@ -490,21 +607,26 @@ bool mLibretroMultiplayerSerialize(void* data, size_t size) { return false; } - struct VFile* vfm = VFileMemChunk(NULL, 0); - if (!vfm) { - return false; - } - - mCoreSaveStateNamed(sMultiplayer->secondaryCore, vfm, SAVESTATE_SAVEDATA | SAVESTATE_RTC); - ssize_t stateSize = vfm->size(vfm); - if ((ssize_t) size != stateSize) { + uint8_t* cursor = data; + size_t remaining = size; + int i; + for (i = 1; i < sMultiplayer->numPlayers; ++i) { + struct VFile* vfm = VFileMemChunk(NULL, 0); + if (!vfm) { + return false; + } + mCoreSaveStateNamed(sMultiplayer->cores[i], vfm, SAVESTATE_SAVEDATA | SAVESTATE_RTC); + ssize_t stateSize = vfm->size(vfm); + if (stateSize <= 0 || (size_t) stateSize > remaining) { + vfm->close(vfm); + return false; + } + vfm->seek(vfm, 0, SEEK_SET); + vfm->read(vfm, cursor, (size_t) stateSize); vfm->close(vfm); - return false; + cursor += stateSize; + remaining -= stateSize; } - - vfm->seek(vfm, 0, SEEK_SET); - vfm->read(vfm, data, size); - vfm->close(vfm); return true; } @@ -514,12 +636,36 @@ bool mLibretroMultiplayerUnserialize(const void* data, size_t size) { return false; } - struct VFile* vfm = VFileFromConstMemory(data, size); - if (!vfm) { + int numSecondaries = sMultiplayer->numPlayers - 1; + + if (numSecondaries == 1) { + struct VFile* vfm = VFileFromConstMemory(data, size); + if (!vfm) { + return false; + } + bool success = mCoreLoadStateNamed(sMultiplayer->cores[1], vfm, SAVESTATE_RTC); + vfm->close(vfm); + return success; + } + + size_t perCore = size / numSecondaries; + if (perCore * numSecondaries != size) { return false; } - bool success = mCoreLoadStateNamed(sMultiplayer->secondaryCore, vfm, SAVESTATE_RTC); - vfm->close(vfm); - return success; + const uint8_t* cursor = data; + int i; + for (i = 1; i < sMultiplayer->numPlayers; ++i) { + struct VFile* vfm = VFileFromConstMemory(cursor, perCore); + if (!vfm) { + return false; + } + if (!mCoreLoadStateNamed(sMultiplayer->cores[i], vfm, SAVESTATE_RTC)) { + vfm->close(vfm); + return false; + } + vfm->close(vfm); + cursor += perCore; + } + return true; } diff --git a/src/platform/libretro/libretro_multiplayer.h b/src/platform/libretro/libretro_multiplayer.h index 2334bcc15ed..dd6f69835f3 100644 --- a/src/platform/libretro/libretro_multiplayer.h +++ b/src/platform/libretro/libretro_multiplayer.h @@ -5,6 +5,7 @@ #include #include +#include #include #include "libretro.h" @@ -15,6 +16,7 @@ enum mLibretroSplitscreenMode { mLIBRETRO_SPLITSCREEN_OFF = 0, mLIBRETRO_SPLITSCREEN_2P_VERTICAL, mLIBRETRO_SPLITSCREEN_2P_HORIZONTAL, + mLIBRETRO_SPLITSCREEN_4P_GRID, }; struct mLibretroLockstepUser { @@ -27,34 +29,37 @@ struct mLibretroLockstepUser { struct mLibretroMultiplayer { enum mLibretroSplitscreenMode mode; - bool active; unsigned maxVideoWidth; unsigned maxVideoHeight; - struct mCore* primaryCore; - struct mCore* secondaryCore; - mColor* secondaryOutputBuffer; + int numPlayers; + struct mCore* cores[MAX_GBAS]; + mColor* outputBuffers[MAX_GBAS]; + struct mAVStream streams[MAX_GBAS]; + mColor* compositeBuffer; size_t compositeBufferPixels; - void* secondaryRomData; - size_t secondaryRomSize; - - struct mAVStream secondaryStream; + void* romData; + size_t romSize; - struct mLibretroLockstepUser users[2]; + struct mLibretroLockstepUser users[MAX_GBAS]; struct GBASIOLockstepCoordinator coordinator; - struct GBASIOLockstepDriver drivers[2]; + struct GBASIOLockstepDriver drivers[MAX_GBAS]; bool coordinatorInitialized; }; +static inline struct mCore* mLibretroMultiplayerGetPrimaryCore(struct mLibretroMultiplayer* mp) { + return mp->cores[0]; +} + void mLibretroMultiplayerInit(struct mLibretroMultiplayer* multiplayer, unsigned maxVideoWidth, unsigned maxVideoHeight); void mLibretroMultiplayerSetPrimaryCore(struct mCore* primaryCore); void mLibretroMultiplayerDeinit(void); void mLibretroMultiplayerUpdateMode(retro_environment_t environCallback); bool mLibretroMultiplayerApplyMode(const void* romData, size_t romSize, const char* romPath, retro_log_printf_t logCallback); void mLibretroMultiplayerReset(void); -void mLibretroMultiplayerSetKeys(uint16_t player1Keys, uint16_t player2Keys); +void mLibretroMultiplayerSetKeys(uint16_t keys[MAX_GBAS]); void mLibretroMultiplayerRunFrame(void); void mLibretroMultiplayerAdjustGeometry(struct retro_game_geometry* geometry); const mColor* mLibretroMultiplayerComposeFrame(const mColor* primaryFrame, unsigned primaryWidth, unsigned primaryHeight, size_t* outPitch, unsigned* outWidth, unsigned* outHeight); From 1723eabe5f3c64936ba5eb0f7c4573105fbabe5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Mon, 9 Mar 2026 01:27:26 +0300 Subject: [PATCH 09/11] libretro_lockstep: add _sanitizeDelay function to clamp negative delays --- src/platform/libretro/libretro_lockstep.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/platform/libretro/libretro_lockstep.c b/src/platform/libretro/libretro_lockstep.c index 7b676115937..07676ff4a62 100644 --- a/src/platform/libretro/libretro_lockstep.c +++ b/src/platform/libretro/libretro_lockstep.c @@ -112,9 +112,19 @@ static void _enqueueEvent(struct GBASIOLockstepCoordinator*, const struct GBASIO static void _setData(struct GBASIOLockstepCoordinator*, uint32_t id, struct GBASIO* sio); static void _setReady(struct GBASIOLockstepCoordinator*, struct GBASIOLockstepPlayer* activePlayer, int playerId, enum GBASIOMode mode); static void _hardSync(struct GBASIOLockstepCoordinator*, struct GBASIOLockstepPlayer*); +static int32_t _sanitizeDelay(int32_t delay, const char* source, int playerId); static void _lockstepEvent(struct mTiming*, void* context, uint32_t cyclesLate); +static int32_t _sanitizeDelay(int32_t delay, const char* source, int playerId) { + if (delay > 0) { + return delay; + } + mLOG(GBA_SIO, DEBUG, "%s produced delay %d for player %d; clamping to 1", + source, delay, playerId); + return 1; +} + static void _verifyAwake(struct GBASIOLockstepCoordinator* coordinator) { #ifdef NDEBUG UNUSED(coordinator); @@ -230,6 +240,7 @@ static void GBASIOLockstepDriverReset(struct GBASIODriver* driver) { _setReady(coordinator, player, 0, coordinator->transferMode); nextEvent = _untilNextSync(lockstep->coordinator, player); } + nextEvent = _sanitizeDelay(nextEvent, "reset schedule", player->playerId); mTimingSchedule(&lockstep->d.p->p->timing, &lockstep->event, nextEvent); } @@ -301,9 +312,12 @@ static bool GBASIOLockstepDriverLoadState(struct GBASIODriver* driver, const voi player->otherModes[2] = _modeIntToEnum(GBASIOLockstepSerializedFlagsGetPlayer2Mode(flags)); player->otherModes[3] = _modeIntToEnum(GBASIOLockstepSerializedFlagsGetPlayer3Mode(flags)); + mTimingDeschedule(&driver->p->p->timing, &lockstep->event); + if (GBASIOLockstepSerializedFlagsGetEventScheduled(flags)) { int32_t when; LOAD_32LE(when, 0, &state->driver.nextEvent); + when = _sanitizeDelay(when, "load state schedule", player->playerId); mTimingSchedule(&driver->p->p->timing, &lockstep->event, when); } @@ -333,6 +347,7 @@ static bool GBASIOLockstepDriverLoadState(struct GBASIODriver* driver, const voi player->freeList = player->freeList->next; *lastEvent = event; lastEvent = &event->next; + event->next = NULL; GBASIOLockstepSerializedEventFlags flags; LOAD_32LE(flags, 0, &stateEvent->flags); @@ -352,6 +367,7 @@ static bool GBASIOLockstepDriverLoadState(struct GBASIODriver* driver, const voi break; } } + *lastEvent = NULL; if (player->playerId == 0) { LOAD_32LE(coordinator->cycle, 0, &state->coordinator.cycle); @@ -930,6 +946,7 @@ void _lockstepEvent(struct mTiming* timing, void* context, uint32_t cyclesLate) case SIO_EV_TRANSFER_START: _setData(coordinator, player->playerId, sio); nextEvent = event->finishCycle - GBASIOLockstepTime(player) - cyclesLate; + nextEvent = _sanitizeDelay(nextEvent, "transfer completion", player->playerId); player->driver->d.p->siocnt |= 0x80; mTimingDeschedule(&sio->p->timing, &sio->completeEvent); mTimingSchedule(&sio->p->timing, &sio->completeEvent, nextEvent); @@ -971,6 +988,7 @@ void _lockstepEvent(struct mTiming* timing, void* context, uint32_t cyclesLate) _verifyAwake(coordinator); } } + nextEvent = _sanitizeDelay(nextEvent, "lockstep wake", player->playerId); mASSERT_DEBUG(nextEvent > 0); mTimingSchedule(timing, &lockstep->event, nextEvent); } From 4e5a3b4b6d8e954db9c2afce58ecbbdc9f4c773a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Mon, 9 Mar 2026 02:16:44 +0300 Subject: [PATCH 10/11] libretro: add syncArmed to determine wether to enforce lockstep sync or not --- src/platform/libretro/libretro_lockstep.c | 37 ++++++-- src/platform/libretro/libretro_lockstep.h | 96 ++++++++++++++++++++ src/platform/libretro/libretro_multiplayer.c | 2 +- src/platform/libretro/libretro_multiplayer.h | 2 +- 4 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 src/platform/libretro/libretro_lockstep.h diff --git a/src/platform/libretro/libretro_lockstep.c b/src/platform/libretro/libretro_lockstep.c index 07676ff4a62..725e5338f3e 100644 --- a/src/platform/libretro/libretro_lockstep.c +++ b/src/platform/libretro/libretro_lockstep.c @@ -3,7 +3,7 @@ * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#include +#include "libretro_lockstep.h" #include #include @@ -28,6 +28,7 @@ DECL_BITS(GBASIOLockstepSerializedFlags, Player0Mode, 10, 3); DECL_BITS(GBASIOLockstepSerializedFlags, Player1Mode, 13, 3); DECL_BITS(GBASIOLockstepSerializedFlags, Player2Mode, 16, 3); DECL_BITS(GBASIOLockstepSerializedFlags, Player3Mode, 19, 3); +DECL_BIT(GBASIOLockstepSerializedFlags, SyncArmed, 22); DECL_BITS(GBASIOLockstepSerializedFlags, TransferMode, 28, 3); DECL_BIT(GBASIOLockstepSerializedFlags, TransferActive, 31); @@ -186,6 +187,7 @@ static void GBASIOLockstepDriverDeinit(struct GBASIODriver* driver) { static void GBASIOLockstepDriverReset(struct GBASIODriver* driver) { struct GBASIOLockstepDriver* lockstep = (struct GBASIOLockstepDriver*) driver; struct GBASIOLockstepCoordinator* coordinator = lockstep->coordinator; + coordinator->syncArmed = false; struct GBASIOLockstepPlayer* player; if (!lockstep->lockstepId) { unsigned id; @@ -379,6 +381,7 @@ static bool GBASIOLockstepDriverLoadState(struct GBASIODriver* driver, const voi } coordinator->transferMode = _modeIntToEnum(GBASIOLockstepSerializedFlagsGetTransferMode(flags)); coordinator->transferActive = GBASIOLockstepSerializedFlagsGetTransferActive(flags); + coordinator->syncArmed = GBASIOLockstepSerializedFlagsGetSyncArmed(flags); } out: if (!error) { @@ -443,6 +446,7 @@ static void GBASIOLockstepDriverSaveState(struct GBASIODriver* driver, void** st } flags = GBASIOLockstepSerializedFlagsSetTransferMode(flags, _modeEnumToInt(coordinator->transferMode)); flags = GBASIOLockstepSerializedFlagsSetTransferActive(flags, coordinator->transferActive); + flags = GBASIOLockstepSerializedFlagsSetSyncArmed(flags, coordinator->syncArmed); } STORE_32LE(flags, 0, &state->flags); *stateOut = state; @@ -469,9 +473,9 @@ static void GBASIOLockstepDriverSetMode(struct GBASIODriver* driver, enum GBASIO } _setReady(coordinator, player, player->playerId, mode); _enqueueEvent(coordinator, &event, TARGET_ALL & ~TARGET(player->playerId)); - if (waitOnPlayers) { + if (waitOnPlayers && coordinator->syncArmed) { GBASIOLockstepCoordinatorWaitOnPlayers(coordinator, player); - } else if (player->playerId == 0) { + } else if (player->playerId == 0 && coordinator->syncArmed) { mLOG(GBA_SIO, DEBUG, "Deferring mode wait while barrier %X is active", coordinator->waiting); } } @@ -542,11 +546,16 @@ static bool GBASIOLockstepDriverStart(struct GBASIODriver* driver) { }; _enqueueEvent(coordinator, &event, TARGET_SECONDARY); coordinator->transferActive = true; + if (player->mode == GBA_SIO_MULTI && !coordinator->syncArmed) { + coordinator->syncArmed = true; + coordinator->nextHardSync = HARD_SYNC_INTERVAL; + mLOG(GBA_SIO, DEBUG, "Arming lockstep sync/watchdog after first MULTI transfer start"); + } if (coordinator->waiting) { waitOnPlayers = false; mLOG(GBA_SIO, DEBUG, "Deferring transfer wait while barrier %X is active", coordinator->waiting); } - if (waitOnPlayers) { + if (waitOnPlayers && coordinator->syncArmed) { GBASIOLockstepCoordinatorWaitOnPlayers(coordinator, player); } ret = true; @@ -572,7 +581,9 @@ static void GBASIOLockstepDriverFinishMultiplayer(struct GBASIODriver* driver, u } player->dataReceived = false; if (player->playerId == 0) { - if (coordinator->waiting) { + if (!coordinator->syncArmed) { + // Sync/watchdog is intentionally inert until first MULTI transfer start. + } else if (coordinator->waiting) { mLOG(GBA_SIO, DEBUG, "Deferring hard sync while barrier %X is active", coordinator->waiting); coordinator->nextHardSync = -1; } else { @@ -598,7 +609,9 @@ static uint8_t GBASIOLockstepDriverFinishNormal8(struct GBASIODriver* driver) { } player->dataReceived = false; if (player->playerId == 0) { - if (coordinator->waiting) { + if (!coordinator->syncArmed) { + // Sync/watchdog is intentionally inert until first MULTI transfer start. + } else if (coordinator->waiting) { mLOG(GBA_SIO, DEBUG, "Deferring hard sync while barrier %X is active", coordinator->waiting); coordinator->nextHardSync = -1; } else { @@ -625,7 +638,9 @@ static uint32_t GBASIOLockstepDriverFinishNormal32(struct GBASIODriver* driver) } player->dataReceived = false; if (player->playerId == 0) { - if (coordinator->waiting) { + if (!coordinator->syncArmed) { + // Sync/watchdog is intentionally inert until first MULTI transfer start. + } else if (coordinator->waiting) { mLOG(GBA_SIO, DEBUG, "Deferring hard sync while barrier %X is active", coordinator->waiting); coordinator->nextHardSync = -1; } else { @@ -667,6 +682,9 @@ void GBASIOLockstepCoordinatorDetach(struct GBASIOLockstepCoordinator* coordinat } int32_t _untilNextSync(struct GBASIOLockstepCoordinator* coordinator, struct GBASIOLockstepPlayer* player) { + if (!coordinator->syncArmed) { + return UNLOCKED_INTERVAL; + } int32_t cycle = coordinator->cycle - GBASIOLockstepTime(player); if (player->playerId == 0) { if (coordinator->nAttached < 2) { @@ -695,6 +713,7 @@ void _removePlayer(struct GBASIOLockstepCoordinator* coordinator, struct GBASIOL coordinator->waiting = 0; coordinator->transferActive = false; + coordinator->syncArmed = false; TableRemove(&coordinator->players, player->driver->lockstepId); _reconfigPlayers(coordinator); @@ -905,7 +924,7 @@ void _lockstepEvent(struct mTiming* timing, void* context, uint32_t cyclesLate) if (!coordinator->transferActive) { GBASIOLockstepCoordinatorWakePlayers(coordinator); } - if (coordinator->nextHardSync < 0) { + if (coordinator->syncArmed && coordinator->nextHardSync < 0) { if (!coordinator->waiting) { _hardSync(coordinator, player); } @@ -978,7 +997,7 @@ void _lockstepEvent(struct mTiming* timing, void* context, uint32_t cyclesLate) nextEvent = player->queue->timestamp - GBASIOLockstepTime(player); } - if (player->playerId != 0 && nextEvent <= LOCKSTEP_INTERVAL) { + if (coordinator->syncArmed && player->playerId != 0 && nextEvent <= LOCKSTEP_INTERVAL) { if (!player->queue || wasDetach) { GBASIOLockstepPlayerSleep(player); // XXX: Is there a better way to gain sync lock at the beginning? diff --git a/src/platform/libretro/libretro_lockstep.h b/src/platform/libretro/libretro_lockstep.h new file mode 100644 index 00000000000..eb0f19b2c25 --- /dev/null +++ b/src/platform/libretro/libretro_lockstep.h @@ -0,0 +1,96 @@ +/* Copyright (c) 2013-2024 Jeffrey Pfau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef MGBA_LIBRETRO_LOCKSTEP_H +#define MGBA_LIBRETRO_LOCKSTEP_H + +#include + +CXX_GUARD_START + +#include +#include +#include +#include +#include +#include + +#define MAX_LOCKSTEP_EVENTS 8 + +enum GBASIOLockstepEventType { + SIO_EV_ATTACH, + SIO_EV_DETACH, + SIO_EV_HARD_SYNC, + SIO_EV_MODE_SET, + SIO_EV_TRANSFER_START, +}; + +struct GBASIOLockstepCoordinator { + struct Table players; + Mutex mutex; + + unsigned nextId; + + unsigned attachedPlayers[MAX_GBAS]; + int nAttached; + uint32_t waiting; + + bool transferActive; + bool syncArmed; + enum GBASIOMode transferMode; + + int32_t cycle; + int32_t nextHardSync; + + uint16_t multiData[4]; + uint32_t normalData[4]; +}; + +struct GBASIOLockstepEvent { + enum GBASIOLockstepEventType type; + int32_t timestamp; + struct GBASIOLockstepEvent* next; + int playerId; + union { + enum GBASIOMode mode; + int32_t finishCycle; + }; +}; + +struct GBASIOLockstepPlayer { + struct GBASIOLockstepDriver* driver; + int playerId; + enum GBASIOMode mode; + enum GBASIOMode otherModes[MAX_GBAS]; + bool asleep; + int32_t cycleOffset; + struct GBASIOLockstepEvent* queue; + bool dataReceived; + + struct GBASIOLockstepEvent buffer[MAX_LOCKSTEP_EVENTS]; + struct GBASIOLockstepEvent* freeList; +}; + +struct GBASIOLockstepDriver { + struct GBASIODriver d; + struct GBASIOLockstepCoordinator* coordinator; + struct mTimingEvent event; + unsigned lockstepId; + + struct mLockstepUser* user; +}; + +void GBASIOLockstepCoordinatorInit(struct GBASIOLockstepCoordinator*); +void GBASIOLockstepCoordinatorDeinit(struct GBASIOLockstepCoordinator*); + +void GBASIOLockstepCoordinatorAttach(struct GBASIOLockstepCoordinator*, struct GBASIOLockstepDriver*); +void GBASIOLockstepCoordinatorDetach(struct GBASIOLockstepCoordinator*, struct GBASIOLockstepDriver*); +size_t GBASIOLockstepCoordinatorAttached(struct GBASIOLockstepCoordinator*); + +void GBASIOLockstepDriverCreate(struct GBASIOLockstepDriver*, struct mLockstepUser*); + +CXX_GUARD_END + +#endif diff --git a/src/platform/libretro/libretro_multiplayer.c b/src/platform/libretro/libretro_multiplayer.c index 65cc0824a62..4cf70fd03d4 100644 --- a/src/platform/libretro/libretro_multiplayer.c +++ b/src/platform/libretro/libretro_multiplayer.c @@ -1,9 +1,9 @@ #include "libretro_multiplayer.h" +#include "libretro_lockstep.h" #include #include #include -#include #include #include diff --git a/src/platform/libretro/libretro_multiplayer.h b/src/platform/libretro/libretro_multiplayer.h index dd6f69835f3..d3f527ece06 100644 --- a/src/platform/libretro/libretro_multiplayer.h +++ b/src/platform/libretro/libretro_multiplayer.h @@ -6,7 +6,7 @@ #include #include #include -#include +#include "libretro_lockstep.h" #include "libretro.h" From fa743c965939f091350df094f57e639933bc17e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Fri, 20 Mar 2026 05:20:18 +0300 Subject: [PATCH 11/11] libretro_lockstep: removed syncArmed checks from mode switches --- src/platform/libretro/libretro_lockstep.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platform/libretro/libretro_lockstep.c b/src/platform/libretro/libretro_lockstep.c index 725e5338f3e..fc958a40984 100644 --- a/src/platform/libretro/libretro_lockstep.c +++ b/src/platform/libretro/libretro_lockstep.c @@ -473,9 +473,9 @@ static void GBASIOLockstepDriverSetMode(struct GBASIODriver* driver, enum GBASIO } _setReady(coordinator, player, player->playerId, mode); _enqueueEvent(coordinator, &event, TARGET_ALL & ~TARGET(player->playerId)); - if (waitOnPlayers && coordinator->syncArmed) { + if (waitOnPlayers) { GBASIOLockstepCoordinatorWaitOnPlayers(coordinator, player); - } else if (player->playerId == 0 && coordinator->syncArmed) { + } else if (player->playerId == 0) { mLOG(GBA_SIO, DEBUG, "Deferring mode wait while barrier %X is active", coordinator->waiting); } } @@ -555,7 +555,7 @@ static bool GBASIOLockstepDriverStart(struct GBASIODriver* driver) { waitOnPlayers = false; mLOG(GBA_SIO, DEBUG, "Deferring transfer wait while barrier %X is active", coordinator->waiting); } - if (waitOnPlayers && coordinator->syncArmed) { + if (waitOnPlayers) { GBASIOLockstepCoordinatorWaitOnPlayers(coordinator, player); } ret = true;