diff --git a/libretro-build/Makefile.common b/libretro-build/Makefile.common index 18a4065168a..20a6330a31a 100644 --- a/libretro-build/Makefile.common +++ b/libretro-build/Makefile.common @@ -112,6 +112,10 @@ 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_savestate.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..20fc2a1abd0 100644 --- a/src/platform/libretro/libretro.c +++ b/src/platform/libretro/libretro.c @@ -42,6 +42,9 @@ FS_Archive sdmcArchive; #endif #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; @@ -135,19 +138,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 +1357,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(&info->geometry); info->timing.fps = core->frequency(core) / (float) core->frameCycles(core); #ifdef M_CORE_GBA @@ -1473,9 +1467,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(); + if (outputBuffer) { #ifdef _3DS linearFree(outputBuffer); @@ -1515,43 +1514,14 @@ 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 playerKeys[MAX_GBAS]; bool skipFrame = false; inputPollCallback(); @@ -1580,31 +1550,11 @@ void retro_run(void) { #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) - ); + int p; + for (p = 0; p < MAX_GBAS; ++p) { + playerKeys[p] = mLibretroInputReadKeys(p, inputCallback, useBitmasks, &turboState); } - - core->setKeys(core, keys); + mLibretroMultiplayerSetKeys(playerKeys); if (!luxSensorUsed) { static bool wasAdjustingLux = false; @@ -1683,7 +1633,7 @@ void retro_run(void) { updateAudioLatency = false; } - core->runFrame(core); + mLibretroMultiplayerRunFrame(); unsigned width, height; core->currentVideoSize(core, &width, &height); @@ -1707,15 +1657,27 @@ void retro_run(void) { } if (!skipFrame) { + 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); - 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(frame, outWidth, outHeight, outPitch); + } } else { - videoCallback(NULL, width, height, VIDEO_WIDTH_MAX * sizeof(mColor)); + size_t outPitch = VIDEO_WIDTH_MAX * sizeof(mColor); + unsigned outWidth = width; + unsigned outHeight = height; + mLibretroMultiplayerComposeFrame(outputBuffer, width, height, &outPitch, &outWidth, &outHeight); + videoCallback(NULL, outWidth, outHeight, outPitch); } /* Check whether audio sample rate has changed */ @@ -1948,9 +1910,16 @@ static void _setupMaps(struct mCore* core) { } void retro_reset(void) { - core->reset(core); + mLibretroMultiplayerSetPrimaryCore(core); + mLibretroMultiplayerUpdateMode(environCallback); + mLibretroMultiplayerApplyMode(data, dataSize, loadedRomPath, logCallback); + 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 @@ -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; @@ -2025,6 +2000,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); @@ -2087,6 +2063,8 @@ bool retro_load_game(const struct retro_game_info* game) { _reloadSettings(); core->loadROM(core, rom); deferredSetup = true; + mLibretroMultiplayerUpdateMode(environCallback); + mLibretroMultiplayerApplyMode(data, dataSize, loadedRomPath, logCallback); const char* sysDir = 0; const char* biosName = 0; @@ -2154,51 +2132,37 @@ void retro_unload_game(void) { if (!core) { return; } + mLibretroMultiplayerDeinit(); mCoreConfigDeinit(&core->config); core->deinit(core); + core = NULL; + mLibretroMultiplayerSetPrimaryCore(NULL); mappedMemoryFree(data, dataSize); data = 0; mappedMemoryFree(savedata, GBA_SIZE_FLASH1M); savedata = 0; + loadedRomPath[0] = '\0'; } 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_core_options.h b/src/platform/libretro/libretro_core_options.h index 9bd553bdf23..caaf8d31c96 100644 --- a/src/platform/libretro/libretro_core_options.h +++ b/src/platform/libretro/libretro_core_options.h @@ -101,6 +101,22 @@ struct retro_core_option_v2_definition option_defs_us[] = { }, "Autodetect" }, + { + "mgba_multiplayer_splitscreen", + "Multiplayer Splitscreen (Restart)", + NULL, + "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" + }, { "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..fc958a40984 --- /dev/null +++ b/src/platform/libretro/libretro_lockstep.c @@ -0,0 +1,1101 @@ +/* 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 "libretro_lockstep.h" + +#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_BIT(GBASIOLockstepSerializedFlags, SyncArmed, 22); +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 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); +#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; + coordinator->syncArmed = false; + 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); + } + nextEvent = _sanitizeDelay(nextEvent, "reset schedule", player->playerId); + 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)); + + 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); + } + + 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; + event->next = NULL; + + 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; + } + } + *lastEvent = NULL; + + 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); + coordinator->syncArmed = GBASIOLockstepSerializedFlagsGetSyncArmed(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); + flags = GBASIOLockstepSerializedFlagsSetSyncArmed(flags, coordinator->syncArmed); + } + 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 (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) { + 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->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 { + _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->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 { + _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->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 { + _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) { + if (!coordinator->syncArmed) { + return UNLOCKED_INTERVAL; + } + 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; + coordinator->syncArmed = 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->syncArmed && 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; + 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); + 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 (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? + if (nextEvent < 4) { + nextEvent = 4; + } + _verifyAwake(coordinator); + } + } + nextEvent = _sanitizeDelay(nextEvent, "lockstep wake", player->playerId); + 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_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 new file mode 100644 index 00000000000..4cf70fd03d4 --- /dev/null +++ b/src/platform/libretro/libretro_multiplayer.c @@ -0,0 +1,671 @@ +#include "libretro_multiplayer.h" +#include "libretro_lockstep.h" + +#include +#include +#include +#include +#include + +#include +#include + +#define VIDEO_BYTES_PER_PIXEL sizeof(mColor) +#define COOPERATIVE_WATCHDOG 4000000 + +static struct mLibretroMultiplayer* sMultiplayer; + +static void _lockstepSleep(struct mLockstepUser* user) { + struct mLibretroLockstepUser* lockstepUser = (struct mLibretroLockstepUser*) user; + if (!lockstepUser->multiplayer || lockstepUser->multiplayer->numPlayers < 2) { + return; + } + + lockstepUser->blocked = true; +} + +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 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", + .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; + } + + if (strcmp(var.value, "4-Player Grid") == 0) { + return mLIBRETRO_SPLITSCREEN_4P_GRID; + } + + return mLIBRETRO_SPLITSCREEN_OFF; +} + +static size_t _compositePixels(unsigned maxVideoWidth, unsigned maxVideoHeight) { + return (size_t) maxVideoWidth * 2 * maxVideoHeight * 2; +} + +static void _clearPrimaryLinkPeripheral(void) { + struct mCore* primary = sMultiplayer->cores[0]; + if (!primary || primary->platform(primary) != mPLATFORM_GBA) { + return; + } + primary->setPeripheral(primary, mPERIPH_GBA_LINK_PORT, NULL); +} + +static void _detachLockstep(void) { + if (!sMultiplayer->coordinatorInitialized) { + return; + } + + 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 _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; + } + } +} + +static void _destroyBuffers(void) { + int i; + for (i = 1; i < MAX_GBAS; ++i) { + if (sMultiplayer->outputBuffers[i]) { + free(sMultiplayer->outputBuffers[i]); + sMultiplayer->outputBuffers[i] = NULL; + } + } + + if (sMultiplayer->compositeBuffer) { + free(sMultiplayer->compositeBuffer); + sMultiplayer->compositeBuffer = NULL; + sMultiplayer->compositeBufferPixels = 0; + } +} + +static void _destroyRomData(void) { + if (sMultiplayer->romData) { + mappedMemoryFree(sMultiplayer->romData, sMultiplayer->romSize); + sMultiplayer->romData = NULL; + sMultiplayer->romSize = 0; + } +} + +static void _stopSession(void) { + _clearPrimaryLinkPeripheral(); + _detachLockstep(); + _destroySecondaryCores(); + _destroyBuffers(); + _destroyRomData(); + sMultiplayer->numPlayers = sMultiplayer->cores[0] ? 1 : 0; +} + +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->romData = anonymousMemoryMap(romSize); + if (!sMultiplayer->romData) { + return false; + } + sMultiplayer->romSize = romSize; + memcpy(sMultiplayer->romData, romData, romSize); + } else if (!(romPath && *romPath)) { + return false; + } + + 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; + } + + sMultiplayer->cores[i] = mCoreFindVF(rom); + if (!sMultiplayer->cores[i]) { + rom->close(rom); + return false; + } + + mCoreInitConfig(sMultiplayer->cores[i], NULL); + sMultiplayer->cores[i]->init(sMultiplayer->cores[i]); + + 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); + + memset(&sMultiplayer->streams[i], 0, sizeof(sMultiplayer->streams[i])); + sMultiplayer->cores[i]->setAVStream(sMultiplayer->cores[i], &sMultiplayer->streams[i]); + + if (!sMultiplayer->cores[i]->loadROM(sMultiplayer->cores[i], rom)) { + rom->close(rom); + return false; + } + + sMultiplayer->cores[i]->reset(sMultiplayer->cores[i]); + sMultiplayer->numPlayers = i + 1; + } + + return true; +} + +static bool _attachLockstep(void) { + struct mCore* primary = sMultiplayer->cores[0]; + if (!primary || primary->platform(primary) != 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; + + 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(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 (!_initSecondaryCores(numPlayers, romData, romSize, romPath)) { + _stopSession(); + return false; + } + + sMultiplayer->compositeBufferPixels = _compositePixels(sMultiplayer->maxVideoWidth, sMultiplayer->maxVideoHeight); + sMultiplayer->compositeBuffer = malloc(sMultiplayer->compositeBufferPixels * VIDEO_BYTES_PER_PIXEL); + if (!sMultiplayer->compositeBuffer) { + _stopSession(); + return false; + } + memset(sMultiplayer->compositeBuffer, 0xFF, sMultiplayer->compositeBufferPixels * VIDEO_BYTES_PER_PIXEL); + + if (!_attachLockstep()) { + _stopSession(); + return false; + } + + 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; + sMultiplayer = multiplayer; +} + +void mLibretroMultiplayerSetPrimaryCore(struct mCore* primaryCore) { + mASSERT(sMultiplayer); + sMultiplayer->cores[0] = primaryCore; + if (!primaryCore) { + sMultiplayer->numPlayers = 0; + } else if (sMultiplayer->numPlayers < 1) { + sMultiplayer->numPlayers = 1; + } +} + +void mLibretroMultiplayerDeinit(void) { + mASSERT(sMultiplayer); + _stopSession(); + sMultiplayer->mode = mLIBRETRO_SPLITSCREEN_OFF; +} + +void mLibretroMultiplayerUpdateMode(retro_environment_t environCallback) { + mASSERT(sMultiplayer); + sMultiplayer->mode = _parseMode(environCallback); +} + +bool mLibretroMultiplayerApplyMode(const void* romData, size_t romSize, const char* romPath, retro_log_printf_t logCallback) { + mASSERT(sMultiplayer); + 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->numPlayers == desiredPlayers) { + return true; + } + + 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"); + } + sMultiplayer->mode = mLIBRETRO_SPLITSCREEN_OFF; + return false; + } + + if (logCallback) { + logCallback(RETRO_LOG_INFO, "libretro: started %d-player splitscreen multiplayer session\n", sMultiplayer->numPlayers); + } + + return true; +} + +void mLibretroMultiplayerReset(void) { + mASSERT(sMultiplayer); + if (!sMultiplayer->cores[0]) { + return; + } + + int i; + for (i = 1; i < sMultiplayer->numPlayers; ++i) { + if (sMultiplayer->cores[i]) { + sMultiplayer->cores[i]->reset(sMultiplayer->cores[i]); + } + } + + sMultiplayer->cores[0]->reset(sMultiplayer->cores[0]); +} + +void mLibretroMultiplayerSetKeys(uint16_t keys[MAX_GBAS]) { + mASSERT(sMultiplayer); + if (!sMultiplayer->cores[0]) { + return; + } + + int i; + for (i = 0; i < sMultiplayer->numPlayers; ++i) { + if (sMultiplayer->cores[i]) { + sMultiplayer->cores[i]->setKeys(sMultiplayer->cores[i], keys[i]); + } + } +} + +void mLibretroMultiplayerRunFrame(void) { + mASSERT(sMultiplayer); + struct mCore* primary = sMultiplayer->cores[0]; + if (!primary) { + return; + } + + if (sMultiplayer->numPlayers < 2) { + primary->runFrame(primary); + return; + } + + 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; + bool allDone = false; + while (!allDone && watchdog > 0) { + bool ranAny = false; + int blockedCount = 0; + + for (i = 0; i < sMultiplayer->numPlayers; ++i) { + if (sMultiplayer->users[i].blocked) { + ++blockedCount; + } + } + + 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 (!done[i] && sMultiplayer->cores[i]->frameCounter(sMultiplayer->cores[i]) != startFrames[i]) { + done[i] = true; + } + } + + if (!ranAny) { + 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"); + mASSERT(false); + } +} + +void mLibretroMultiplayerAdjustGeometry(struct retro_game_geometry* geometry) { + mASSERT(sMultiplayer); + if (sMultiplayer->numPlayers < 2 || !geometry) { + return; + } + + switch (sMultiplayer->mode) { + case mLIBRETRO_SPLITSCREEN_2P_VERTICAL: + geometry->base_width *= 2; + geometry->max_width *= 2; + geometry->aspect_ratio *= 2.0; + 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->numPlayers < 2 || !sMultiplayer->compositeBuffer) { + *outPitch = (size_t) primaryWidth * VIDEO_BYTES_PER_PIXEL; + *outWidth = primaryWidth; + *outHeight = primaryHeight; + return primaryFrame; + } + + 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; + *outWidth = primaryWidth; + *outHeight = primaryHeight; + return primaryFrame; +} + +bool mLibretroMultiplayerStateActive(void) { + mASSERT(sMultiplayer); + return sMultiplayer->numPlayers > 1; +} + +size_t mLibretroMultiplayerSerializeSize(void) { + mASSERT(sMultiplayer); + if (!mLibretroMultiplayerStateActive()) { + 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); + } + return total; +} + +bool mLibretroMultiplayerSerialize(void* data, size_t size) { + mASSERT(sMultiplayer); + if (!mLibretroMultiplayerStateActive() || !data) { + return false; + } + + 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); + cursor += stateSize; + remaining -= stateSize; + } + return true; +} + +bool mLibretroMultiplayerUnserialize(const void* data, size_t size) { + mASSERT(sMultiplayer); + if (!mLibretroMultiplayerStateActive() || !data) { + return false; + } + + 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; + } + + 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 new file mode 100644 index 00000000000..d3f527ece06 --- /dev/null +++ b/src/platform/libretro/libretro_multiplayer.h @@ -0,0 +1,73 @@ +#ifndef MGBA_LIBRETRO_MULTIPLAYER_H +#define MGBA_LIBRETRO_MULTIPLAYER_H + +#include + +#include +#include +#include +#include "libretro_lockstep.h" + +#include "libretro.h" + +CXX_GUARD_START + +enum mLibretroSplitscreenMode { + mLIBRETRO_SPLITSCREEN_OFF = 0, + mLIBRETRO_SPLITSCREEN_2P_VERTICAL, + mLIBRETRO_SPLITSCREEN_2P_HORIZONTAL, + mLIBRETRO_SPLITSCREEN_4P_GRID, +}; + +struct mLibretroLockstepUser { + struct mLockstepUser d; + int requestedId; + struct mLibretroMultiplayer* multiplayer; + unsigned playerIndex; + bool blocked; +}; + +struct mLibretroMultiplayer { + enum mLibretroSplitscreenMode mode; + unsigned maxVideoWidth; + unsigned maxVideoHeight; + + int numPlayers; + struct mCore* cores[MAX_GBAS]; + mColor* outputBuffers[MAX_GBAS]; + struct mAVStream streams[MAX_GBAS]; + + mColor* compositeBuffer; + size_t compositeBufferPixels; + + void* romData; + size_t romSize; + + struct mLibretroLockstepUser users[MAX_GBAS]; + struct GBASIOLockstepCoordinator coordinator; + 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 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); +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 + +#endif 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