Skip to content

Splitscreen multiplayer + Netplay (Made with Claude) - #318

Open
SomeoneIsWorking wants to merge 18 commits into
libretro:masterfrom
SomeoneIsWorking:splitscreen-new
Open

Splitscreen multiplayer + Netplay (Made with Claude)#318
SomeoneIsWorking wants to merge 18 commits into
libretro:masterfrom
SomeoneIsWorking:splitscreen-new

Conversation

@SomeoneIsWorking

@SomeoneIsWorking SomeoneIsWorking commented Feb 22, 2026

Copy link
Copy Markdown

I wanted to play Kirby & The Amazing Mirror split-screen co-op.
This works but I need a second opinion.
I have no idea if it's implemented correctly.

@hizzlekizzle

Copy link
Copy Markdown

I'm not sure it makes sense to have the core_controller, CoreController and multiplayer_controller all split up like that.

Looks like a lot of this would simpler if we took the core-level turbo out, which endrift would rather we did, as well.

@SomeoneIsWorking

SomeoneIsWorking commented Feb 22, 2026

Copy link
Copy Markdown
Author

I'm not sure it makes sense to have the core_controller, CoreController and multiplayer_controller all split up like that.

I don't really understand what's going on. I'm not an emulator guy.
I just ported CoreController and MultiplayerController from the Qt app.

I actually had a better working version for SDL client, not in this repo.
Maybe I should try that instead.

@SomeoneIsWorking
SomeoneIsWorking marked this pull request as draft February 22, 2026 22:31
@endrift

endrift commented Feb 23, 2026

Copy link
Copy Markdown
Collaborator

Judiciously avoiding mentioning that this is vibe coded this time I see.

@SomeoneIsWorking

SomeoneIsWorking commented Feb 23, 2026

Copy link
Copy Markdown
Author

I wasn't trying to avoid it. I'm just trying to figure out.
But you are right, I should've mentioned it.
I don't remember why I did that but I think I thought it wasn't worth mentioning.
I might have gotten a bit excited to see it was working.

@hizzlekizzle

Copy link
Copy Markdown

I assumed it was, based on (the code itself, obviously, but also) the remark about not knowing if it was implemented correctly. I had also assumed that was why it was split into 3 separate headers and files, but it sounds like it's that way because the Qt frontend does it that way, too. That makes it much more acceptable, IMO, if it makes it easier to track upstream changes to those Qt frontend files.

@endrift

endrift commented Feb 23, 2026

Copy link
Copy Markdown
Collaborator

Generally speaking, tracking reimplementations of those files is suboptimal. One of the things I'm planning to do in 0.12 with the netplay support code is lower-level implementations of core coordination for multiplayer purpose, for cases like this. It should be possible to do with even single-threaded code, akin to how TGB-Dual swaps between cores every so often. But it may be a year before I even get there, so who knows i it's worth waiting or just yanking it out and replacing it later.

@SomeoneIsWorking

Copy link
Copy Markdown
Author

It should be possible to do with even single-threaded code, akin to how TGB-Dual swaps between cores every so often.

I tried this a while ago because it makes perfect sense. Why do lockstep and mutex on the same thread, just run the cores one after another. But I could NOT get it working.

@SomeoneIsWorking

Copy link
Copy Markdown
Author

What the hell I'm going to try anyway.
I attempted so many different multiplayer implementations.
In my forks there is multiplayer via ffmpeg streams, Qt split screen, SDL split screen.
I also made a Python GUI and tried to add splitscreen which failed.

By "I" you get the point. 1% I, 99% Autocomplete bots.

@SomeoneIsWorking

SomeoneIsWorking commented Feb 23, 2026

Copy link
Copy Markdown
Author

Hey I got lucky. Working singlethreaded lockstep.
I started fresh and focused on single-thread without copying MultiplayerController.
I instead copied lockstep.c as libretro_lockstep.c to make it mutex-free.

I faced timing and desync issues and solved it with "pumping" (and cycle skips if pumping occurred)
Which is not super ideal but I tried playing for a while and nothing went wrong.

@SomeoneIsWorking
SomeoneIsWorking marked this pull request as ready for review February 23, 2026 18:27
@SomeoneIsWorking

Copy link
Copy Markdown
Author

Don't mind the last two force pushes, they are just cosmetics.

@hizzlekizzle

Copy link
Copy Markdown

Sounds good. I'll see if we can get some testers.

@SomeoneIsWorking

SomeoneIsWorking commented Feb 24, 2026

Copy link
Copy Markdown
Author

I tested it on macOS when developing.
I just tested it on Bazzite also (used distrobox to build it)

@SomeoneIsWorking

SomeoneIsWorking commented Feb 24, 2026

Copy link
Copy Markdown
Author

One thing eating my brain is Network possibility because technically let's say.

You have 2 people running the game.
Both players run both instances locally (only their own view is visible)
They send inputs to each other using rollback.
Theoretically I don't see why this can't work.

But I have no idea where someone would even begin with this.
I also do not know a single thing about how libretro network works.

Edit: I got netplay working but uhhh that discussion should be for another PR...

@SomeoneIsWorking

SomeoneIsWorking commented Feb 27, 2026

Copy link
Copy Markdown
Author

Okay I said I want to keep netplay related comments out of this PR but I had a problem regarding RA rollback netplay not exposing player index and I made a PR to RetroArch which got merged so now rollback netplay is playable but I still want to make it a separate PR because it has some parts that I want to talk about separately.

image

@SomeoneIsWorking

Copy link
Copy Markdown
Author

Replaced "pumping" with individual running based on state
Replaced player1 player2 with cores array
Added 4 players

@hizzlekizzle

Copy link
Copy Markdown

@SomeoneIsWorking is this ready to go, from your perspective? or are you still actively working on it?

@endrift do you have any thoughts on any of it? I/we don't want to step on your toes merging something you think would be a problem down the road. I haven't actually done any testing on it myself, yet, so it wouldn't be merged immediately anyway, just to be clear.

@SomeoneIsWorking

SomeoneIsWorking commented Mar 7, 2026

Copy link
Copy Markdown
Author

@hizzlekizzle it is ready to go IMO. I'm going to submit the netplay PR after this which contains some further refactoring.

@hizzlekizzle

Copy link
Copy Markdown

FWIW, I just compiled this for Windows and played Advance Wars with myself with no issues.

@kaysedwards what sort of platform/device/OS were you using?

@SomeoneIsWorking

Copy link
Copy Markdown
Author

@hizzlekizzle syncArmed or without it?

@hizzlekizzle

Copy link
Copy Markdown

Oh, I didn't even think to test that. I was the non-syncArmed commit.

@SomeoneIsWorking

SomeoneIsWorking commented Mar 17, 2026

Copy link
Copy Markdown
Author

Okay then I think @kaysedwards ran into the same problem you did

Edit: I just did a fresh build with latest commit on a Fedora machine and it worked fine.

By fresh build I mean. This is an entirely new setup with no RA installed.

So I downloaded RetroArch.AppImage.
Cloned the repo.
Ran make.
Copied the core.
Ran Kirbo > Enabled split-screen > reset game.
Went into multiplayer with both players and they connected fine.

@hizzlekizzle

Copy link
Copy Markdown

I just re-compiled with the syncArmed commit and it worked perfectly on Windows but failed in Linux, where it just spins on the connection screen with this output repeated indefinitely:

[libretro DEBUG] GBA Serial I/O: MULTI 1 write: SIOMLT_SEND <- EDCB
[libretro DEBUG] GBA Serial I/O: Lockstep: SIOCNT <- 601F
[libretro DEBUG] GBA Serial I/O: MULTI 0 write: SIOMLT_SEND <- EDCB
[libretro DEBUG] GBA Serial I/O: Lockstep: SIOCNT <- 608B
[libretro DEBUG] GBA Serial I/O: MULTI 1 write: SIOMLT_SEND <- EDCB
[libretro DEBUG] GBA Serial I/O: Lockstep: SIOCNT <- 601F
[libretro DEBUG] GBA DMA: Starting DMA 1 0x03005540 -> 0x040000A0 (B600:0004)
[libretro DEBUG] GBA DMA: Starting DMA 2 0x03005B70 -> 0x040000A4 (B600:0004)
[libretro DEBUG] GBA Serial I/O: MULTI 0 write: SIOMLT_SEND <- EDCB
[libretro DEBUG] GBA Serial I/O: Lockstep: SIOCNT <- 608B
[libretro DEBUG] GBA Serial I/O: MULTI 1 write: SIOMLT_SEND <- EDCB
[libretro DEBUG] GBA Serial I/O: Lockstep: SIOCNT <- 601F
[libretro DEBUG] GBA DMA: Starting DMA 1 0x03005540 -> 0x040000A0 (B600:0004)
[libretro DEBUG] GBA DMA: Starting DMA 2 0x03005B70 -> 0x040000A4 (B600:0004)
[libretro DEBUG] GBA Serial I/O: MULTI 0 write: SIOMLT_SEND <- EDCB
[libretro DEBUG] GBA Serial I/O: Lockstep: SIOCNT <- 608B
[libretro DEBUG] GBA Serial I/O: MULTI 1 write: SIOMLT_SEND <- EDCB
[libretro DEBUG] GBA Serial I/O: Lockstep: SIOCNT <- 601F
[libretro DEBUG] GBA Serial I/O: MULTI 0 write: SIOMLT_SEND <- EDCB
[libretro DEBUG] GBA Serial I/O: Lockstep: SIOCNT <- 608B
[libretro DEBUG] GBA Serial I/O: MULTI 1 write: SIOMLT_SEND <- EDCB
[libretro DEBUG] GBA Serial I/O: Lockstep: SIOCNT <- 601F
[libretro DEBUG] GBA Serial I/O: MULTI 1 write: SIOMLT_SEND <- EDCB
[libretro DEBUG] GBA Serial I/O: Lockstep: SIOCNT <- 601F
[libretro DEBUG] GBA Serial I/O: MULTI 0 write: SIOMLT_SEND <- EDCB
[libretro DEBUG] GBA Serial I/O: Lockstep: SIOCNT <- 608B
[libretro DEBUG] GBA Serial I/O: MULTI 0 write: SIOMLT_SEND <- EDCB
[libretro DEBUG] GBA Serial I/O: Lockstep: SIOCNT <- 608B
[libretro DEBUG] GBA Serial I/O: MULTI 1 write: SIOMLT_SEND <- EDCB
[libretro DEBUG] GBA Serial I/O: Lockstep: SIOCNT <- 601F

@SomeoneIsWorking

SomeoneIsWorking commented Mar 19, 2026

Copy link
Copy Markdown
Author

@hizzlekizzle I hate to reiterate this but please try a clean build, run a make clean before otherwise this will happen 100%

Also. Which linux? Maybe I can test it in a VM

@hizzlekizzle

hizzlekizzle commented Mar 19, 2026

Copy link
Copy Markdown

Ubuntu linux. I clean between every build. I can try a fresh pull in case the clean routine isn't actually cleaning everything.

EDIT: yeah, same behavior with a fresh pull. Tested Kirby this time and it just sits there at the multiplayer connection screen with the connection data spinning forever.

@SomeoneIsWorking

Copy link
Copy Markdown
Author

I removed syncArmed checks from mode switches. I tested both games again and I tested the scenarios that crashed for me without syncArmed.

@hizzlekizzle

Copy link
Copy Markdown

Just tested the latest commit with linux and windows and both worked great.

@endrift

endrift commented Mar 20, 2026

Copy link
Copy Markdown
Collaborator

I've been waiting until you got something that worked before taking too close a look at it. I'll try to find some time to look at this in the next few days

@SomeoneIsWorking

Copy link
Copy Markdown
Author

Thank you @endrift (consider me a fan)

@hizzlekizzle

Copy link
Copy Markdown

hey @endrift did you ever get a chance to peek at this? it seemed solid/reliable in my testing, but I'm not that familiar with the mGBA codebase to know if it's implemented sensibly. OTOH, it does live all in the libretro directory, so any mess is confined there :)

@endrift

endrift commented May 3, 2026

Copy link
Copy Markdown
Collaborator

Sorry, I've been distracted and it fell off my radar. I'll try to look at this soon

@SuperDavid2

SuperDavid2 commented Aug 6, 2026

Copy link
Copy Markdown

I have tried this out a little, and I have observed a few things. First, from what I've tested, it works perfectly online through Retroarch Kaillera, although I have not tried the standard netplay version. I found that in Sonic Advance 3, at the boss of the Toy Kingdom world, there is a relatively high chance the emulator will crash. I have a replay file that consistently causes this crash if anyone wants it.

In Kirby Nightmare in Dream Land, it seems that most of the time you start up a game in multiplayer, as the star is crashing into the overworld area, the game feels laggy. If I stay in that area, I find that it takes a random amount of time before going back to full speed. Every time I go through a door, this lag returns before going away again. It seems to be running at about 45 fps when this happens (in game, since it's not my computer or Retroarch slowing down). I think observing how long it takes to jump and hit the ground is an especially easy way to check if it's lagging.

Finally, there are a few features that I personally consider missing. These are the ability to choose which screen(s) are seen in the main window, which GBAs the audio is coming from, and targeted cheats. I'm unsure if Retroarch itself is able to choose which GBA a cheat is used on, but I currently have it set for myself so that cheats affect either players 2 to 4 or all 4 players. I also was able to get those audio and videos features I mentioned working, but this was just low quality vibe coded stuff that I don't think would be helpful to share.

Edit: I also forgot to mention individual save files. Currently, it only loads a save file for the first GBA.

@SomeoneIsWorking

SomeoneIsWorking commented Aug 8, 2026

Copy link
Copy Markdown
Author

@SuperDavid2

Thank you.

It has been a very long time I tried this.
There is a separate netplay branch that I was planning to make a PR on if/when this gets merged.
Which solves the screens/audio issues you mentioned, no idea about cheats.
I didn't even know this branch had a working netplay.

And the game issues you mentioned, also not sure if netplay branch resolves them.
I'll try to test those when I get the chance.

@SuperDavid2

Copy link
Copy Markdown

@SomeoneIsWorking
The Kaillera netplay version of Retroarch, although it works well enough for many systems that I consider it underappreciated. I forgot to mention that it was only because I use Kaillera that I wanted some of those features. I record gameplay using Kaillera replay files, in which case, I want to be able to record all 4 screens, but not necessarily see all 4 screens while I'm playing online.
If you want to see the crash I'm referring to in Sonic Advance 3, it's here. The file just goes in the states folder and then you hit the play replay key, and then press the speed up button to speed through to where it crashes.
https://mega.nz/file/ltpSnRqR#vTsmFEcmtBYUEnhpBzzLhLXVBO692HTG4z75S2R1PO8

I appreciate the work done so far and am looking forward to future updates.

@SomeoneIsWorking

SomeoneIsWorking commented Sep 9, 2026

Copy link
Copy Markdown
Author

@SuperDavid2 Sorry I neglected this a lot as it doesn't look like it will get merged

sonic_2p_linked_last20s.mp4

I think I fixed your crash case with Sonic, does it look right to you?

Edit: This is also "vibe-coded" so don't be reluctant to share if you could make any fixes please. And LLMs have evolved a good deal since I posted the PR.

@SomeoneIsWorking

Copy link
Copy Markdown
Author

Also recorded videos for Kirby & The Amazing Mirror and Advance Wars

kirby_link.mp4
aw_link.mp4

SomeoneIsWorking and others added 2 commits September 9, 2026 16:24
mTiming's masterCycles is a free-running uint32 read back through an
int32 API, so every cycle stamp in the lockstep coordinator goes negative
once per 2^31 cycles - roughly 128 seconds of emulated time. Comparing
two of them with a plain int32 subtraction overflows whenever the pair
straddles that wrap, which is undefined behaviour, and at -O2/-O3 the
compiler folds the comparison to a constant.

The one that mattered is the event-due test in _lockstepEvent: past the
wrap it decided no queued event was ever due, the event queue stopped
draining, and the cooperative scheduler's watchdog aborted the core. That
is the Sonic Advance 3 crash - reproducible at about 11 seconds of link
play with an -O3 build, absent with DEBUG=1 (-O0), and inconsistent
across platforms because each compiler folds it differently.

Route every such comparison through _cyclesBetween(), which does the
subtraction in uint32 and converts back, so the difference is well
defined across the wrap. Also restores the indentation in
GBASIOLockstepDriverSetMode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three conflicts, resolved as follows.

* Makefile.libretro and libretro-build/Makefile.common: upstream moved the
  libretro build to cmake (mgba-emu#330) and deleted both. Took the deletion; the
  macOS build fix this branch carried for Makefile.libretro is no longer
  needed, because the cmake build probes HAVE_LOCALE / HAVE_STRTOF_L
  through find_function instead of hardcoding them per platform.

* retro_get_system_av_info: upstream dropped a duplicated aspect_ratio
  assignment. Took the deletion and kept mLibretroMultiplayerAdjustGeometry,
  which scales the aspect ratio itself for the split layouts.

* retro_run audio: upstream replaced mAudioResampler with the new
  audioConverter, and the cmake libretro target now excludes
  util/audio-resampler.c outright, so the old path could not have linked.
  Took upstream's converter loop and kept this branch's two additions on
  top of it - feeding it from the local player's core in "Self" mode, and
  draining the other players' audio buffers so they do not back up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@SomeoneIsWorking SomeoneIsWorking changed the title Splitscreen multiplayer Splitscreen multiplayer (Made with Claude) Sep 9, 2026
SomeoneIsWorking and others added 2 commits September 9, 2026 17:00
syncArmed held the lockstep machinery inert until the first MULTI transfer
start, so two attached cores ran free until the games actually began
talking. It was introduced to stop a hang, but the hang was the signed
overflow in the cycle comparisons, fixed in the preceding commit: past the
2^31-cycle wrap the event-due test folded to a constant, the queue stopped
draining, and the cooperative watchdog aborted. Keeping the coordinator
inert until a transfer simply delayed reaching the wrap in the cases people
happened to test.

Measured on the case it was meant to help - Advance Wars in link mode with
no transfer yet initiated, player 1 driven into the menus while player 2
sits on the name entry screen, saved as a state so both builds start
identically. Counting core interleavings (runLoop calls) per frame, which
is deterministic and so immune to machine noise, over 12000 frames:

  with syncArmed     35843047 runloops  2986.92 per frame
  without syncArmed  35986923 runloops  2998.91 per frame   +0.40%

Wall-clock throughput could not distinguish the two at all; repeated runs
of the same binary varied by more than 2x on this machine.

Neither build crashes or stalls, in that scenario or in 12000-40000 frame
soaks of Sonic Advance 3, Kirby & The Amazing Mirror (2P and 4P) and
Advance Wars. Both still complete a real link: Kirby through MULTI-PAK to
a running 2-player game, Advance Wars through Multi-Pak to day 1 of a
2-player battle.

Serialized flag bit 22 is now unused; bits 23-27 were already free, so
nothing shifts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_multiplayerDisplayLabel was declared but never defined or called, which
clang reports as an unused function on every build of this branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@SomeoneIsWorking

SomeoneIsWorking commented Sep 9, 2026

Copy link
Copy Markdown
Author

I removed syncArmed checks from mode switches. I tested both games again and I tested the scenarios that crashed for me without syncArmed.

Removed syncArmed entirely, it was apparently a bandaid for a wrap bug.
Now this PR has the netplay changes too.

Update: Multisave also should be working now.

@SomeoneIsWorking SomeoneIsWorking changed the title Splitscreen multiplayer (Made with Claude) Splitscreen multiplayer + Netplay (Made with Claude) Sep 9, 2026
SomeoneIsWorking and others added 2 commits September 9, 2026 17:55
Only player 1 had a save: the libretro API exposes a single SAVE_RAM
region and the frontend manages that one for the primary core, so the
extra cores booted with empty cartridge memory every time.

The extra players now open their own save next to player 1's, named
"<rom>.p2.srm", "<rom>.p3.srm", "<rom>.p4.srm", and hand it to the core
the same way the deferred setup does for player 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
libretro.c was reaching into the multiplayer struct directly to work out
which display mode was in effect, which player is local and whose audio
to play. That state now stays behind the module's own interface:

* the module owns its instance, so libretro.c no longer holds one
* session and display refresh each become a single call that says
  whether the frontend needs new AV info
* picking the audible core and draining the others moves next to the
  rest of the per-player core handling

Also drops three things nothing calls: the primary-core accessor, the
netplay rollback-context getter and its contextKnown field.

No behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants