Skip to content

Add network-gamepad output (RetroArch) — no SDL needed - #1

Open
vialoh wants to merge 5 commits into
Peterksharma:mainfrom
vialoh:retroarch-network-gamepad
Open

vialoh wants to merge 5 commits into
Peterksharma:mainfrom
vialoh:retroarch-network-gamepad

Conversation

@vialoh

@vialoh vialoh commented Aug 23, 2026

Copy link
Copy Markdown

Hey @Peterksharma! Sorry about Claude's verbosity in the original post (below).

Long story short, yesterday I asked Claude (Fable) if it could help me get switch2mac working with various emulators and eventually arrived at using RetroArch's network gamepad option. This makes it super easy to use it with any of the 200+ emulator cores supported by RetroArch while you/we wait for Apple's approval on the com.apple.developer.hid.virtual.device entitlement.

I actually haven't bothered building the Swift version of this as I'm currently on a semi-fresh MacOS install and haven't set up that dev environment yet, but this PR is based on a Python script that Fable one-shotted for me that converts your original SDL UDP implementation to RetroArch's network gamepad input in real-time... and it works perfectly, so I assume Fable's proper Swift code works perfectly as well. I'm happy to iterate on this further if necessary!

I appreciate you open sourcing your work on this. You made it super easy to get things working and allowed me to reminisce with some old consoles using a proper controller. Thanks!

What

Adds an optional "network gamepad" output sink that publishes controller
state over localhost UDP in the libretro remote-gamepad format — which
RetroArch's built-in Network Gamepad (a.k.a. Remote RetroPad) consumes
— with a toggle and base-port field in the dashboard's Configuration
section. Off by default.

RetroArch is probably the most common emulator people will want to use
these controllers with, but its macOS build has no SDL input driver (only
cocoa, mfi and hid), so the patched-SDL bridge can't reach it — and
since OpenEmu is also IOKit-only, it can't either. RetroArch does, however,
ship a network input path on every platform: it listens on UDP ports
55400+N for struct remote_message packets and injects them as RetroPad
state for player N. This sink speaks that protocol.

How it works

  • New Output/NetworkGamepadSink.swift implementing ControllerOutputSink,
    modelled on UDPHub (same Darwin socket + dispatch-queue style).
  • Wire format is RetroArch's struct remote_message from
    input/input_driver.h (20 bytes LE: i32 port, device, index, id; u16 state; 2 pad). Verified against RetroArch master.
  • Mapping: A/B/X/Y, D-pad, +/−, L/R, ZL/ZR, stick clicks → RetroPad ids;
    both sticks as analog (Y inverted to RetroPad's +Y-down). GameCube-style
    analog triggers (lt/rt ≥ 128) also register as L2/R2. Home, Capture,
    C, GL, GR have no RetroPad slot and are left to the existing remapper.
  • RetroArch reads one datagram per player per frame and discards the
    rest, so the sink sends diffs only, paced at 60/s per player, button
    edges before analog updates, with axes quantized so stick jitter can't
    starve button events. Held state is re-asserted every 2 s so a
    RetroArch launched mid-hold converges; a disconnect (or toggling the
    feature off) releases everything. The timer only runs while something
    is pending or held, so the sink is free when idle or disabled.
  • AppConfig gains networkGamepadEnabled / networkGamepadBasePort
    (UserDefaults, read on the sink's queue like the other settings).
  • Naming is deliberately generic (sink, keys, UI: "Network gamepad output
    (RetroArch)") — the protocol is libretro's, but nothing else in the code
    is tied to one consumer; RetroArch is named in the README directions and
    as a hint on the toggle.
  • README: new "Using it with RetroArch" section, features list, and
    architecture diagram updated.

Limitations

  • RetroArch's protocol is one-way: no rumble.
  • RetroArch binds its network-gamepad ports on all interfaces with no
    authentication (noted in the README).

RetroArch on macOS has no SDL input driver, so the SDL bridge can't reach
it. Add an optional ControllerOutputSink that publishes controller state
over localhost UDP in the libretro remote-gamepad format, which RetroArch's
built-in Network Gamepad consumes: one port per player (base port + slot,
default 55400-55403), with a Configuration toggle and base-port field. Off
by default. Naming is generic (sink, settings keys, UI) with RetroArch as a
hint on the toggle and in the README directions.

The receiver drains one datagram per player per frame, so the sink sends
diffs only, paced at 60/s, button edges before analog, and re-asserts held
state every 2 s so a receiver launched mid-game converges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vialoh

vialoh commented Aug 23, 2026

Copy link
Copy Markdown
Author

Consolidated into original post above.

@vialoh
vialoh marked this pull request as ready for review August 23, 2026 08:48
vialoh added 4 commits August 23, 2026 15:55
Only diffs are ever sent, so a datagram dropped by a transient sendto
failure (ENOBUFS etc.) was lost for good — a lost button release left the
direction held until the button was pressed again. Commit sent-state only
on success and retry on the next tick, logging failures at most once a
second.
The refresh cleared sent-bits with sent &= want, which also cleared the bit
of a button released since the previous tick — so its release was never
sent and the direction stayed held in the receiver until the button was
pressed again. Clear only the bits of currently held buttons (sent &=
~want) so pending releases still go out.
The periodic refresh forced a resend of held buttons by clearing their
sent-bits — but sent-bits are also the only record of what the receiver
currently holds. A button released between the refresh and the resend
then looked already-released (want 0, sent 0), so no release was sent
and the direction stayed held. Keep sent* as the true receiver model and
track pending re-asserts in separate refresh* masks, sent after edges and
analog changes.
@Peterksharma

Copy link
Copy Markdown
Owner

I am just getting back to this project. Will review.

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.

2 participants